Files
homelab/opentofu/sync_state_from_pve.py
T

210 lines
7.3 KiB
Python

import json
import os
import sys
import ssl
import re
import urllib.request
import urllib.error
# Configuration
# Expects env vars or defaults
PM_API_URL = os.environ.get("PM_API_URL") or os.environ.get("TF_VAR_proxmox_api_url") or "https://10.2.1.10:8006/api2/json"
# Credentials construction
_pm_user = os.environ.get("TF_VAR_proxmox_user", "root@pam")
_pm_token_id = os.environ.get("TF_VAR_proxmox_token_id") or os.environ.get("PROXMOX_TOKEN_ID", "ansible")
# Full Token ID: user@pam!tokenid
PM_TOKEN_ID = f"{_pm_user}!{_pm_token_id}"
PM_TOKEN_SECRET = os.environ.get("PM_API_TOKEN_SECRET") or os.environ.get("TF_VAR_proxmox_token_secret") or os.environ.get("PROXMOX_TOKEN_SECRET")
# Allow insecure TLS
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def pve_request(endpoint, method="GET", data=None):
url = f"{PM_API_URL}{endpoint}"
req = urllib.request.Request(url, method=method)
# Auth headers
if PM_TOKEN_ID and PM_TOKEN_SECRET:
req.add_header("Authorization", f"PVEAPIToken={PM_TOKEN_ID}={PM_TOKEN_SECRET}")
elif PM_USER and PM_PASS:
# Ticket auth requires a separate flow, focusing on Token for now as it's in providers.tf
print("Error: Ticket auth not fully implemented, please utilize PM_API_TOKEN_ID and PM_API_TOKEN_SECRET env vars.")
sys.exit(1)
else:
print("Error: No credentials provided (PM_API_TOKEN_ID/SECRET).")
sys.exit(1)
try:
with urllib.request.urlopen(req, context=ctx) as response:
return json.loads(response.read().decode())['data']
except urllib.error.HTTPError as e:
print(f"HTTP Error {e.code}: {e.reason} for {url}")
return None
except Exception as e:
print(f"Error requesting {url}: {e}")
return None
def parse_lxc_network(net_str):
"""
Parses PVE network string: "name=eth0,bridge=vmbr0,firewall=1,hwaddr=...,ip=dhcp,type=veth"
Returns Telmate/proxmox compatible object
"""
parts = net_str.split(',')
config = {}
for part in parts:
if '=' in part:
k, v = part.split('=', 1)
config[k] = v
# Map to Terraform schema
return {
"name": config.get("name", "eth0"),
"bridge": config.get("bridge"),
"ip": config.get("ip", "dhcp"),
"ip6": config.get("ip6", "auto"),
"gw": config.get("gw"),
"gw6": config.get("gw6"),
"hwaddr": config.get("hwaddr", "").upper(),
"firewall": config.get("firewall") == "1",
"tag": int(config.get("tag", 0)),
"rate": int(config.get("rate", 0)),
"mtu": int(config.get("mtu", 0)),
"type": "veth", # Usually
"trunks": config.get("trunks", "")
}
def parse_lxc_mountpoint(mp_str, key):
"""
Parses PVE mountpoint string: "local-zfs:subvol-100-disk-1,mp=/mnt/data,size=8G"
Returns Telmate/proxmox compatible object
"""
parts = mp_str.split(',')
config = {}
# First part is usually storage:vol
vol_part = parts[0]
if ':' in vol_part and '=' not in vol_part:
storage, volume = vol_part.split(':', 1)
config['storage'] = storage
config['volume'] = f"{storage}:{volume}"
# Remove it from parts to process kv pairs
parts = parts[1:]
else:
# Case where it might be bind mount? "mp=/foo,..."
pass
for part in parts:
if '=' in part:
k, v = part.split('=', 1)
config[k] = v
# Extract slot from key "mp0" -> 0
slot = int(key.replace("mp", ""))
return {
"key": key,
"slot": slot,
"storage": config.get("storage", ""),
"mp": config.get("mp"),
"size": config.get("size"),
"acl": config.get("acl") == "1",
"backup": config.get("backup") != "0", # Default is 1/true in PVE usually? Or need check
"quota": config.get("quota") == "1",
"replicate": config.get("replicate") != "0",
"ro": config.get("ro") == "1",
"shared": config.get("shared") == "1",
"volume": config.get("volume") # Important for state consistency
}
def sync_state_from_pve(state_file, output_file):
with open(state_file, 'r') as f:
state_data = json.load(f)
modified_count = 0
for resource in state_data.get('resources', []):
if resource.get('type') == 'proxmox_lxc' and resource.get('name') == 'this':
# Identify ID
instances = resource.get('instances', [])
if not instances:
continue
# Assume one instance per resource in this structure
instance = instances[0]
tf_id = instance.get('attributes', {}).get('id')
# ID format: node/lxc/vmid
if not tf_id or len(tf_id.split('/')) != 3:
print(f"Skipping Invalid ID: {tf_id}")
continue
node, _, vmid = tf_id.split('/')
print(f"Syncing LXC {vmid} on {node}...")
# Fetch config
config = pve_request(f"/nodes/{node}/lxc/{vmid}/config")
if not config:
print(f" Failed to fetch config for {vmid}")
continue
attributes = instance['attributes']
# 1. Update Networks
networks = []
# Scan net0 to net9 (arbitrary limit, usually enough)
found_nets = False
for i in range(10):
key = f"net{i}"
if key in config:
networks.append(parse_lxc_network(config[key]))
found_nets = True
if found_nets:
print(f" - Found {len(networks)} network interfaces")
attributes['network'] = networks
modified_count += 1
# 2. Update Mountpoints
mountpoints = []
found_mps = False
for i in range(10):
key = f"mp{i}"
if key in config:
mountpoints.append(parse_lxc_mountpoint(config[key], key))
found_mps = True
if found_mps:
print(f" - Found {len(mountpoints)} mountpoints")
attributes['mountpoint'] = mountpoints
modified_count += 1
elif 'mountpoint' in attributes and not found_mps:
# If TF has mountpoints but PVE doesn't, we might need to clear them?
# Or keep them if they are just not active. Safer to sync reality.
# attributes['mountpoint'] = []
pass
# 3. Sync other simple attributes if needed (e.g. onboot, startup)
if 'onboot' in config:
attributes['onboot'] = config['onboot'] == 1
if modified_count > 0:
state_data['serial'] += 1
with open(output_file, 'w') as f:
json.dump(state_data, f, indent=2)
print(f"Successfully patched {modified_count} resources. Saved to {output_file}")
else:
print("No resources updated.")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 sync_from_pve.py <path_to_tfstate.json>")
sys.exit(1)
sync_state_from_pve(sys.argv[1], "tfstate_pve_synced.json")