210 lines
7.3 KiB
Python
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")
|