claude-configs/skills/proxmox/scripts/migrate_vm_to_lxc.py
Cal Corum 8a1d15911f Initial commit: Claude Code configuration backup
Version control Claude Code configuration including:
- Global instructions (CLAUDE.md)
- User settings (settings.json)
- Custom agents (architect, designer, engineer, etc.)
- Custom skills (create-skill templates and workflows)

Excludes session data, secrets, cache, and temporary files per .gitignore.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 16:34:21 -06:00

425 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""
VM to LXC Migration Helper
Assists with migrating Docker-based VMs to LXC containers
"""
import sys
import os
import json
import argparse
from typing import Dict, List, Optional
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from proxmox_client import ProxmoxClient
class VMLXCMigrator:
"""Helper class for VM to LXC migrations"""
def __init__(self):
self.client = ProxmoxClient()
def analyze_vm(self, vmid: int) -> Dict:
"""
Analyze a VM for migration suitability
Args:
vmid: VM ID to analyze
Returns:
Dictionary with VM info and migration recommendations
"""
print(f"\n🔍 Analyzing VM {vmid}...")
print("=" * 60)
try:
vm = self.client.get_vm(vmid)
status = self.client.get_vm_status(vmid)
except Exception as e:
return {"error": f"Failed to get VM info: {e}"}
analysis = {
"vmid": vmid,
"name": vm.get("name", "unknown"),
"status": status.get("status", "unknown"),
"memory_mb": vm.get("memory", 0),
"cores": vm.get("cores", 0),
"current_mem_usage_mb": status.get("mem", 0) / (1024**2) if status.get("mem") else 0,
"current_mem_percent": (status.get("mem", 0) / status.get("maxmem", 1) * 100) if status.get("maxmem") else 0,
"cpu_usage_percent": status.get("cpu", 0) * 100,
"uptime_hours": status.get("uptime", 0) / 3600,
"network_config": [],
"disk_config": [],
"migration_suitability": "unknown",
"recommendations": [],
"warnings": [],
"estimated_lxc_memory_mb": 0,
"estimated_savings_mb": 0
}
# Analyze network configuration
for key, value in vm.items():
if key.startswith("net"):
analysis["network_config"].append({key: value})
# Check migration suitability
vm_name_lower = analysis["name"].lower()
# High priority candidates
if "docker" in vm_name_lower:
analysis["migration_suitability"] = "excellent"
analysis["recommendations"].append("✅ Docker host - excellent LXC candidate")
analysis["recommendations"].append("✅ Docker-in-LXC works great with nesting enabled")
elif "bot" in vm_name_lower or "discord" in vm_name_lower:
analysis["migration_suitability"] = "excellent"
analysis["recommendations"].append("✅ Bot/application server - excellent LXC candidate")
analysis["recommendations"].append("✅ Lightweight workload perfect for containers")
elif "database" in vm_name_lower or "db" in vm_name_lower:
analysis["migration_suitability"] = "good"
analysis["recommendations"].append("🟢 Database - good LXC candidate")
analysis["recommendations"].append("✅ Better I/O performance in LXC")
analysis["warnings"].append("⚠️ Backup database before migration!")
elif "plex" in vm_name_lower:
analysis["migration_suitability"] = "conditional"
analysis["recommendations"].append("🟡 Plex - conditional LXC candidate")
analysis["recommendations"].append("✅ If CPU transcoding: migrate to LXC")
analysis["recommendations"].append("⚠️ If GPU transcoding: stay VM (easier GPU passthrough)")
elif "tdarr" in vm_name_lower:
analysis["migration_suitability"] = "poor"
analysis["recommendations"].append("❌ Tdarr with GPU - keep as VM")
analysis["recommendations"].append("❌ GPU passthrough much easier in VMs")
analysis["warnings"].append("🛑 NOT recommended for LXC migration")
elif "game" in vm_name_lower or "7d" in vm_name_lower:
analysis["migration_suitability"] = "poor"
analysis["recommendations"].append("❌ Game server - keep as VM")
analysis["recommendations"].append("❌ Game engines often need full VM environment")
analysis["warnings"].append("🛑 NOT recommended for LXC migration")
elif "hass" in vm_name_lower or "home-assistant" in vm_name_lower:
analysis["migration_suitability"] = "low"
analysis["recommendations"].append("🟡 Home Assistant - low priority for migration")
analysis["recommendations"].append("⚠️ VM is officially supported, works well")
analysis["warnings"].append("⚠️ Consider keeping as VM unless resource constrained")
elif "template" in vm_name_lower:
analysis["migration_suitability"] = "n/a"
analysis["recommendations"].append("📝 Template - create LXC equivalent")
analysis["recommendations"].append("💡 Maintain both VM and LXC templates")
else:
analysis["migration_suitability"] = "unknown"
analysis["recommendations"].append("🔍 Unknown workload - manual analysis needed")
# Estimate LXC resource usage
if analysis["current_mem_usage_mb"] > 0:
# LXC typically uses 10-20% less memory (no guest kernel, bootloader, etc.)
overhead_reduction = 0.15 # 15% average reduction
analysis["estimated_lxc_memory_mb"] = int(analysis["current_mem_usage_mb"] * (1 - overhead_reduction))
analysis["estimated_savings_mb"] = int(analysis["current_mem_usage_mb"] - analysis["estimated_lxc_memory_mb"])
else:
# VM is stopped, estimate based on allocation
analysis["estimated_lxc_memory_mb"] = int(analysis["memory_mb"] * 0.85)
analysis["estimated_savings_mb"] = int(analysis["memory_mb"] * 0.15)
# Resource efficiency warnings
if analysis["current_mem_percent"] > 80:
analysis["warnings"].append(f"⚠️ High memory usage ({analysis['current_mem_percent']:.1f}%) - migration will help!")
return analysis
def print_analysis(self, analysis: Dict):
"""Print formatted analysis report"""
if "error" in analysis:
print(f"{analysis['error']}")
return
print(f"\n📊 VM Analysis Report")
print("=" * 60)
print(f"VM ID: {analysis['vmid']}")
print(f"Name: {analysis['name']}")
print(f"Status: {analysis['status']}")
print(f"\n💾 Current Resources:")
print(f" Allocated Memory: {analysis['memory_mb']}MB")
print(f" CPU Cores: {analysis['cores']}")
if analysis['status'] == 'running':
print(f" Current Memory Usage: {analysis['current_mem_usage_mb']:.1f}MB ({analysis['current_mem_percent']:.1f}%)")
print(f" CPU Usage: {analysis['cpu_usage_percent']:.1f}%")
print(f" Uptime: {analysis['uptime_hours']:.1f} hours")
print(f"\n📈 Estimated LXC Resources:")
print(f" Estimated Memory: {analysis['estimated_lxc_memory_mb']}MB")
print(f" Memory Savings: ~{analysis['estimated_savings_mb']}MB ({analysis['estimated_savings_mb']/analysis['memory_mb']*100:.1f}%)")
print(f"\n🎯 Migration Suitability: {analysis['migration_suitability'].upper()}")
if analysis['recommendations']:
print(f"\n💡 Recommendations:")
for rec in analysis['recommendations']:
print(f" {rec}")
if analysis['warnings']:
print(f"\n⚠️ Warnings:")
for warn in analysis['warnings']:
print(f" {warn}")
print("\n" + "=" * 60)
def generate_migration_plan(self, vmid: int, new_ctid: int, static_ip: Optional[str] = None) -> Dict:
"""
Generate a migration plan for a specific VM
Args:
vmid: Source VM ID
new_ctid: Target container ID
static_ip: Static IP for the LXC (optional)
Returns:
Dictionary with migration plan
"""
analysis = self.analyze_vm(vmid)
if "error" in analysis:
return analysis
vm = self.client.get_vm(vmid)
# Extract network info
network_config = "ip=dhcp"
if static_ip:
network_config = f"ip={static_ip}/24,gw=10.10.0.1"
else:
# Try to extract from VM config
for key, value in vm.items():
if key.startswith("net") and "ip=" in str(value):
# Parse existing IP config
pass # TODO: parse VM network config
plan = {
"source_vm": {
"vmid": vmid,
"name": analysis["name"],
"memory": analysis["memory_mb"],
"cores": analysis["cores"]
},
"target_lxc": {
"ctid": new_ctid,
"hostname": f"{analysis['name']}-lxc",
"memory": analysis["estimated_lxc_memory_mb"],
"cores": analysis["cores"],
"network": network_config,
"features": "nesting=1,keyctl=1", # Docker support
"unprivileged": 0 # Privileged for Docker
},
"steps": [
{
"step": 1,
"action": "Backup VM",
"command": f"# Create VM snapshot\npython3 -c \"from proxmox_client import ProxmoxClient; c=ProxmoxClient(); c.create_snapshot({vmid}, 'pre-migration', 'Before LXC migration')\""
},
{
"step": 2,
"action": "Export Docker Compose configurations",
"command": f"# SSH into VM and backup Docker configs\nssh cal@<vm-ip> 'cd ~ && tar czf docker-backup.tar.gz ~/docker ~/services ~/*/docker-compose.yml 2>/dev/null || true'\nscp cal@<vm-ip>:~/docker-backup.tar.gz ./vm-{vmid}-docker-backup.tar.gz"
},
{
"step": 3,
"action": "Document running containers",
"command": f"ssh cal@<vm-ip> 'docker ps -a --format \"table {{{{.Names}}}}\\t{{{{.Image}}}}\\t{{{{.Status}}}}\" > ~/container-list.txt'\nscp cal@<vm-ip>:~/container-list.txt ./vm-{vmid}-containers.txt"
},
{
"step": 4,
"action": "Stop VM (when ready)",
"command": f"# Graceful shutdown\npython3 -c \"from proxmox_client import ProxmoxClient; c=ProxmoxClient(); c.shutdown_vm({vmid}, timeout=120)\""
},
{
"step": 5,
"action": "Create LXC from template",
"command": f"# Clone Docker LXC template\npct clone 9001 {new_ctid} --hostname {analysis['name']}-lxc --full"
},
{
"step": 6,
"action": "Configure LXC resources",
"command": f"pct set {new_ctid} --memory {analysis['estimated_lxc_memory_mb']} --cores {analysis['cores']}"
},
{
"step": 7,
"action": "Configure LXC network",
"command": f"pct set {new_ctid} --net0 name=eth0,bridge=vmbr0,{network_config}"
},
{
"step": 8,
"action": "Start LXC",
"command": f"pct start {new_ctid}"
},
{
"step": 9,
"action": "Restore Docker configurations",
"command": f"# Copy configs to LXC\nscp ./vm-{vmid}-docker-backup.tar.gz root@<lxc-ip>:/root/\nssh root@<lxc-ip> 'cd / && tar xzf /root/docker-backup.tar.gz'"
},
{
"step": 10,
"action": "Start Docker containers",
"command": f"ssh root@<lxc-ip> 'cd ~/docker && docker-compose up -d'"
},
{
"step": 11,
"action": "Test and validate",
"command": "# Run migration validation checklist\n# See: migration_checklist.md"
},
{
"step": 12,
"action": "Monitor for 24-48 hours",
"command": f"# Monitor LXC performance\nwatch -n 60 'pct status {new_ctid} && pct config {new_ctid}'"
}
],
"rollback_plan": [
f"1. Stop LXC: pct stop {new_ctid}",
f"2. Start original VM: qm start {vmid}",
f"3. Verify services are running",
f"4. Delete LXC if no longer needed: pct destroy {new_ctid}"
]
}
return plan
def print_migration_plan(self, plan: Dict):
"""Print formatted migration plan"""
if "error" in plan:
print(f"{plan['error']}")
return
print("\n📋 Migration Plan")
print("=" * 60)
print(f"\n🔵 Source VM:")
print(f" ID: {plan['source_vm']['vmid']}")
print(f" Name: {plan['source_vm']['name']}")
print(f" Memory: {plan['source_vm']['memory']}MB")
print(f" Cores: {plan['source_vm']['cores']}")
print(f"\n🟢 Target LXC:")
print(f" ID: {plan['target_lxc']['ctid']}")
print(f" Hostname: {plan['target_lxc']['hostname']}")
print(f" Memory: {plan['target_lxc']['memory']}MB (saving ~{plan['source_vm']['memory'] - plan['target_lxc']['memory']}MB)")
print(f" Cores: {plan['target_lxc']['cores']}")
print(f" Network: {plan['target_lxc']['network']}")
print(f"\n📝 Migration Steps:")
for step in plan['steps']:
print(f"\n Step {step['step']}: {step['action']}")
if step['command']:
print(f" Command:")
for line in step['command'].split('\n'):
if line.strip():
print(f" {line}")
print(f"\n🔄 Rollback Plan (if needed):")
for item in plan['rollback_plan']:
print(f" {item}")
print("\n" + "=" * 60)
print("💡 Tips:")
print(" - Take VM snapshot before starting")
print(" - Schedule migration during low-traffic period")
print(" - Keep VM stopped but don't delete for 1-2 weeks")
print(" - Monitor LXC performance and stability")
print(" - Have rollback plan ready")
def main():
"""Main CLI interface"""
parser = argparse.ArgumentParser(description="VM to LXC Migration Helper")
parser.add_argument("command", choices=["analyze", "plan", "batch-analyze"],
help="Command to run")
parser.add_argument("--vmid", type=int, help="VM ID to analyze or migrate")
parser.add_argument("--ctid", type=int, help="Target LXC container ID (for plan)")
parser.add_argument("--ip", help="Static IP for LXC (e.g., 10.10.0.50)")
parser.add_argument("--all", action="store_true", help="Analyze all VMs (for batch-analyze)")
args = parser.parse_args()
migrator = VMLXCMigrator()
if args.command == "analyze":
if not args.vmid:
print("❌ Error: --vmid required for analyze command")
sys.exit(1)
analysis = migrator.analyze_vm(args.vmid)
migrator.print_analysis(analysis)
elif args.command == "plan":
if not args.vmid or not args.ctid:
print("❌ Error: --vmid and --ctid required for plan command")
sys.exit(1)
plan = migrator.generate_migration_plan(args.vmid, args.ctid, args.ip)
migrator.print_migration_plan(plan)
# Save plan to file
filename = f"migration-plan-vm{args.vmid}-to-ct{args.ctid}.json"
with open(filename, 'w') as f:
json.dump(plan, f, indent=2)
print(f"\n💾 Migration plan saved to: {filename}")
elif args.command == "batch-analyze":
client = ProxmoxClient()
vms = client.list_vms()
excellent_candidates = []
good_candidates = []
conditional_candidates = []
poor_candidates = []
print("\n🔍 Analyzing all VMs for migration suitability...")
print("=" * 60)
for vm in vms:
analysis = migrator.analyze_vm(vm['vmid'])
suitability = analysis.get('migration_suitability', 'unknown')
if suitability == 'excellent':
excellent_candidates.append(analysis)
elif suitability == 'good':
good_candidates.append(analysis)
elif suitability == 'conditional':
conditional_candidates.append(analysis)
elif suitability == 'poor':
poor_candidates.append(analysis)
# Print summary
print("\n📊 Migration Suitability Summary")
print("=" * 60)
print(f"\n✅ Excellent Candidates ({len(excellent_candidates)}):")
total_savings = 0
for analysis in excellent_candidates:
print(f" VM {analysis['vmid']}: {analysis['name']}")
print(f" Savings: ~{analysis['estimated_savings_mb']}MB")
total_savings += analysis['estimated_savings_mb']
print(f"\n🟢 Good Candidates ({len(good_candidates)}):")
for analysis in good_candidates:
print(f" VM {analysis['vmid']}: {analysis['name']}")
print(f" Savings: ~{analysis['estimated_savings_mb']}MB")
total_savings += analysis['estimated_savings_mb']
print(f"\n🟡 Conditional Candidates ({len(conditional_candidates)}):")
for analysis in conditional_candidates:
print(f" VM {analysis['vmid']}: {analysis['name']} - {analysis['recommendations'][0]}")
print(f"\n❌ Poor Candidates (Keep as VM) ({len(poor_candidates)}):")
for analysis in poor_candidates:
print(f" VM {analysis['vmid']}: {analysis['name']}")
print(f"\n💰 Total Estimated Savings: ~{total_savings}MB (~{total_savings/1024:.1f}GB)")
print("\n💡 Recommendation: Start with 'Excellent Candidates' for safest wins!")
if __name__ == "__main__":
main()