#!/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@ 'cd ~ && tar czf docker-backup.tar.gz ~/docker ~/services ~/*/docker-compose.yml 2>/dev/null || true'\nscp cal@:~/docker-backup.tar.gz ./vm-{vmid}-docker-backup.tar.gz" }, { "step": 3, "action": "Document running containers", "command": f"ssh cal@ 'docker ps -a --format \"table {{{{.Names}}}}\\t{{{{.Image}}}}\\t{{{{.Status}}}}\" > ~/container-list.txt'\nscp cal@:~/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@:/root/\nssh root@ 'cd / && tar xzf /root/docker-backup.tar.gz'" }, { "step": 10, "action": "Start Docker containers", "command": f"ssh root@ '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()