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>
425 lines
18 KiB
Python
Executable File
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()
|