#!/usr/bin/env python3 """ Unraid VM Manager for ThrillWiki - Main Orchestrator Follows the Ubuntu autoinstall guide exactly: 1. Creates modified Ubuntu ISO with autoinstall configuration 2. Manages VM lifecycle on Unraid server 3. Handles ThrillWiki deployment automation """ import os import sys import time import logging import tempfile from pathlib import Path from typing import Optional # Import our modular components from iso_builder import UbuntuISOBuilder from vm_manager import UnraidVMManager # Configuration UNRAID_HOST = os***REMOVED***iron.get("UNRAID_HOST", "localhost") UNRAID_USER = os***REMOVED***iron.get("UNRAID_USER", "root") VM_NAME = os***REMOVED***iron.get("VM_NAME", "thrillwiki-vm") VM_MEMORY = int(os***REMOVED***iron.get("VM_MEMORY", 4096)) # MB VM_VCPUS = int(os***REMOVED***iron.get("VM_VCPUS", 2)) VM_DISK_SIZE = int(os***REMOVED***iron.get("VM_DISK_SIZE", 50)) # GB SSH_PUBLIC_KEY = os***REMOVED***iron.get("SSH_PUBLIC_KEY", "") # Network Configuration VM_IP = os***REMOVED***iron.get("VM_IP", "dhcp") VM_GATEWAY = os***REMOVED***iron.get("VM_GATEWAY", "192.168.20.1") VM_NETMASK = os***REMOVED***iron.get("VM_NETMASK", "255.255.255.0") VM_NETWORK = os***REMOVED***iron.get("VM_NETWORK", "192.168.20.0/24") # GitHub Configuration REPO_URL = os***REMOVED***iron.get("REPO_URL", "") GITHUB_USERNAME = os***REMOVED***iron.get("GITHUB_USERNAME", "") GITHUB_TOKEN = os***REMOVED***iron.get("GITHUB_TOKEN", "") # Ubuntu version preference UBUNTU_VERSION = os***REMOVED***iron.get("UBUNTU_VERSION", "24.04") # Setup logging os.makedirs("logs", exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("logs/unraid-vm.log"), logging.StreamHandler()], ) logger = logging.getLogger(__name__) class ThrillWikiVMOrchestrator: """Main orchestrator for ThrillWiki VM deployment.""" def __init__(self): self.vm_manager = UnraidVMManager(VM_NAME, UNRAID_HOST, UNRAID_USER) self.iso_builder = None def create_autoinstall_user_data(self) -> str: """Create autoinstall user-data configuration.""" # Read autoinstall template template_path = Path(__file__).parent / "autoinstall-user-data.yaml" if not template_path.exists(): raise FileNotFoundError(f"Autoinstall template not found: {template_path}") with open(template_path, 'r', encoding='utf-8') as f: template = f.read() # Replace placeholders using string replacement (avoiding .format() due to curly braces in YAML) user_data = template.replace( "{SSH_PUBLIC_KEY}", SSH_PUBLIC_KEY if SSH_PUBLIC_KEY else "# No SSH key provided" ).replace( "{GITHUB_REPO}", REPO_URL if REPO_URL else "" ) # Update network configuration based on VM_IP setting if VM_IP.lower() == "dhcp": # Keep DHCP configuration as-is pass else: # Replace with static IP configuration network_config = f"""dhcp4: false addresses: - {VM_IP}/24 gateway4: {VM_GATEWAY} nameservers: addresses: - 8.8.8.8 - 8.8.4.4""" user_data = user_data.replace("dhcp4: true", network_config) return user_data def build_autoinstall_iso(self) -> Path: """Build Ubuntu autoinstall ISO following the guide.""" logger.info("🔨 Building Ubuntu autoinstall ISO...") # Create ISO builder self.iso_builder = UbuntuISOBuilder(VM_NAME) # Create user-data configuration user_data = self.create_autoinstall_user_data() # Build autoinstall ISO iso_output_path = Path(f"/tmp/{VM_NAME}-ubuntu-autoinstall.iso") success = self.iso_builder.build_autoinstall_iso( user_data=user_data, output_path=iso_output_path, ubuntu_version=UBUNTU_VERSION ) if not success: raise RuntimeError("Failed to build autoinstall ISO") logger.info(f"✅ Autoinstall ISO built successfully: {iso_output_path}") return iso_output_path def deploy_vm(self) -> bool: """Complete VM deployment process.""" try: logger.info("🚀 Starting ThrillWiki VM deployment...") # Step 1: Check SSH connectivity logger.info("📡 Testing Unraid connectivity...") if not self.vm_manager.authenticate(): logger.error("❌ Cannot connect to Unraid server") return False # Step 2: Build autoinstall ISO logger.info("🔨 Building Ubuntu autoinstall ISO...") iso_path = self.build_autoinstall_iso() # Step 3: Upload ISO to Unraid logger.info("📤 Uploading autoinstall ISO to Unraid...") remote_iso_path = self.vm_manager.upload_iso_to_unraid(iso_path) # Step 4: Create/update VM configuration logger.info("⚙️ Creating VM configuration...") success = self.vm_manager.create_vm( vm_memory=VM_MEMORY, vm_vcpus=VM_VCPUS, vm_disk_size=VM_DISK_SIZE, vm_ip=VM_IP ) if not success: logger.error("❌ Failed to create VM configuration") return False # Step 5: Start VM logger.info("🟢 Starting VM...") success = self.vm_manager.start_vm() if not success: logger.error("❌ Failed to start VM") return False logger.info("🎉 VM deployment completed successfully!") logger.info("") logger.info("📋 Next Steps:") logger.info("1. VM is now booting with Ubuntu autoinstall") logger.info("2. Installation will take 15-30 minutes") logger.info("3. Use 'python main.py ip' to get VM IP when ready") logger.info("4. SSH to VM and run /home/thrillwiki/deploy-thrillwiki.sh") logger.info("") return True except Exception as e: logger.error(f"❌ VM deployment failed: {e}") return False finally: # Cleanup ISO builder temp files if self.iso_builder: self.iso_builder.cleanup() def get_vm_info(self) -> dict: """Get VM information.""" return { "name": VM_NAME, "status": self.vm_manager.vm_status(), "ip": self.vm_manager.get_vm_ip(), "memory": VM_MEMORY, "vcpus": VM_VCPUS, "disk_size": VM_DISK_SIZE } def main(): """Main entry point.""" import argparse parser = argparse.ArgumentParser( description="ThrillWiki VM Manager - Ubuntu Autoinstall on Unraid", epilog=""" Examples: python main.py setup # Complete VM setup with autoinstall python main.py start # Start existing VM python main.py ip # Get VM IP address python main.py status # Get VM status python main.py delete # Remove VM completely """, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( "action", choices=["setup", "create", "start", "stop", "status", "ip", "delete", "info"], help="Action to perform" ) args = parser.parse_args() # Create orchestrator orchestrator = ThrillWikiVMOrchestrator() if args.action == "setup": logger.info("🚀 Setting up complete ThrillWiki VM environment...") success = orchestrator.deploy_vm() sys.exit(0 if success else 1) elif args.action == "create": logger.info("⚙️ Creating VM configuration...") success = orchestrator.vm_manager.create_vm(VM_MEMORY, VM_VCPUS, VM_DISK_SIZE, VM_IP) sys.exit(0 if success else 1) elif args.action == "start": logger.info("🟢 Starting VM...") success = orchestrator.vm_manager.start_vm() sys.exit(0 if success else 1) elif args.action == "stop": logger.info("🛑 Stopping VM...") success = orchestrator.vm_manager.stop_vm() sys.exit(0 if success else 1) elif args.action == "status": status = orchestrator.vm_manager.vm_status() print(f"VM Status: {status}") sys.exit(0) elif args.action == "ip": ip = orchestrator.vm_manager.get_vm_ip() if ip: print(f"VM IP: {ip}") print(f"SSH: ssh thrillwiki@{ip}") print(f"Deploy: ssh thrillwiki@{ip} '/home/thrillwiki/deploy-thrillwiki.sh'") sys.exit(0) else: print("❌ Failed to get VM IP (VM may not be ready yet)") sys.exit(1) elif args.action == "info": info = orchestrator.get_vm_info() print("🖥️ VM Information:") print(f" Name: {info['name']}") print(f" Status: {info['status']}") print(f" IP: {info['ip'] or 'Not available'}") print(f" Memory: {info['memory']} MB") print(f" vCPUs: {info['vcpus']}") print(f" Disk: {info['disk_size']} GB") sys.exit(0) elif args.action == "delete": logger.info("🗑️ Deleting VM and all files...") success = orchestrator.vm_manager.delete_vm() sys.exit(0 if success else 1) if __name__ == "__main__": main()