#!/usr/bin/env python3 """ Unraid VM Manager for ThrillWiki - Template-Based Main Orchestrator Uses pre-built template VMs for fast deployment instead of autoinstall. """ import os import sys import logging from pathlib import Path # Import our modular components from template_manager import TemplateVMManager from vm_manager_template import UnraidTemplateVMManager class ConfigLoader: """Dynamic configuration loader that reads environment variables when needed.""" def __init__(self): # Try to load ***REMOVED***.unraid if it exists to ensure we have the # latest config self._load_env_file() def _load_env_file(self): """Load ***REMOVED***.unraid file if it exists.""" # Find the project directory (two levels up from this script) script_dir = Path(__file__).parent project_dir = script_dir.parent.parent env_file = project_dir / "***REMOVED***.unraid" if env_file.exists(): try: with open(env_file, "r") as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) # Remove quotes if present value = value.strip("\"'") # Only set if not already in environment (env vars # take precedence) if key not in os.environ: os.environ[key] = value logging.info(f"📝 Loaded configuration from {env_file}") except Exception as e: logging.warning(f"⚠️ Could not load ***REMOVED***.unraid: {e}") @property def UNRAID_HOST(self): return os.environ.get("UNRAID_HOST", "localhost") @property def UNRAID_USER(self): return os.environ.get("UNRAID_USER", "root") @property def VM_NAME(self): return os.environ.get("VM_NAME", "thrillwiki-vm") @property def VM_MEMORY(self): return int(os.environ.get("VM_MEMORY", 4096)) @property def VM_VCPUS(self): return int(os.environ.get("VM_VCPUS", 2)) @property def VM_DISK_SIZE(self): return int(os.environ.get("VM_DISK_SIZE", 50)) @property def SSH_PUBLIC_KEY(self): return os.environ.get("SSH_PUBLIC_KEY", "") @property def VM_IP(self): return os.environ.get("VM_IP", "dhcp") @property def VM_GATEWAY(self): return os.environ.get("VM_GATEWAY", "192.168.20.1") @property def VM_NETMASK(self): return os.environ.get("VM_NETMASK", "255.255.255.0") @property def VM_NETWORK(self): return os.environ.get("VM_NETWORK", "192.168.20.0/24") @property def REPO_URL(self): return os.environ.get("REPO_URL", "") @property def GITHUB_USERNAME(self): return os.environ.get("GITHUB_USERNAME", "") @property def GITHUB_TOKEN(self): return os.environ.get("GITHUB_TOKEN", "") # Create a global configuration instance config = ConfigLoader() # Setup logging with reduced buffering os.makedirs("logs", exist_ok=True) # Configure console handler with line buffering console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter( logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") ) # Force flush after each log message console_handler.flush = lambda: sys.stdout.flush() # Configure file handler file_handler = logging.FileHandler("logs/unraid-vm.log") file_handler.setLevel(logging.INFO) file_handler.setFormatter( logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") ) # Set up basic config with both handlers logging.basicConfig( level=logging.INFO, handlers=[file_handler, console_handler], ) # Ensure stdout is line buffered for real-time output sys.stdout.reconfigure(line_buffering=True) logger = logging.getLogger(__name__) class ThrillWikiTemplateVMOrchestrator: """Main orchestrator for template-based ThrillWiki VM deployment.""" def __init__(self): # Log current configuration for debugging logger.info( f"🔧 Using configuration: UNRAID_HOST={ config.UNRAID_HOST}, UNRAID_USER={ config.UNRAID_USER}, VM_NAME={ config.VM_NAME}" ) self.template_manager = TemplateVMManager( config.UNRAID_HOST, config.UNRAID_USER ) self.vm_manager = UnraidTemplateVMManager( config.VM_NAME, config.UNRAID_HOST, config.UNRAID_USER ) def check_template_ready(self) -> bool: """Check if template VM is ready for use.""" logger.info("🔍 Checking template VM availability...") if not self.template_manager.check_template_exists(): logger.error("❌ Template VM disk not found!") logger.error( "Please ensure 'thrillwiki-template-ubuntu' VM exists and is properly configured" ) logger.error( "Template should be located at: /mnt/user/domains/thrillwiki-template-ubuntu/vdisk1.qcow2" ) return False # Check template status if not self.template_manager.update_template(): logger.warning("⚠️ Template VM may be running - this could cause issues") logger.warning( "Ensure the template VM is stopped before creating new instances" ) info = self.template_manager.get_template_info() if info: logger.info(f"📋 Template Info:") logger.info(f" Virtual Size: {info['virtual_size']}") logger.info(f" File Size: {info['file_size']}") logger.info(f" Last Modified: {info['last_modified']}") return True def deploy_vm_from_template(self) -> bool: """Complete template-based VM deployment process.""" try: logger.info("🚀 Starting ThrillWiki template-based 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: Check template availability logger.info("🔍 Verifying template VM...") if not self.check_template_ready(): logger.error("❌ Template VM not ready") return False # Step 3: Create VM from template logger.info("⚙️ Creating VM from template...") success = self.vm_manager.create_vm_from_template( vm_memory=config.VM_MEMORY, vm_vcpus=config.VM_VCPUS, vm_disk_size=config.VM_DISK_SIZE, vm_ip=config.VM_IP, ) if not success: logger.error("❌ Failed to create VM from template") return False # Step 4: 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("🎉 Template-based VM deployment completed successfully!") logger.info("") logger.info("📋 Next Steps:") logger.info("1. VM is now booting from template disk") logger.info("2. Boot time should be much faster (2-5 minutes)") logger.info("3. Use 'python main_template.py ip' to get VM IP when ready") logger.info("4. SSH to VM and run deployment commands") logger.info("") return True except Exception as e: logger.error(f"❌ Template VM deployment failed: {e}") return False def deploy_and_configure_thrillwiki(self) -> bool: """Deploy VM from template and configure ThrillWiki.""" try: logger.info("🚀 Starting complete ThrillWiki deployment from template...") # Step 1: Deploy VM from template if not self.deploy_vm_from_template(): return False # Step 2: Wait for VM to be accessible and configure ThrillWiki if config.REPO_URL: logger.info("🔧 Configuring ThrillWiki on VM...") success = self.vm_manager.customize_vm_for_thrillwiki( config.REPO_URL, config.GITHUB_TOKEN ) if success: vm_ip = self.vm_manager.get_vm_ip() logger.info("🎉 Complete ThrillWiki deployment successful!") logger.info(f"🌐 ThrillWiki is available at: http://{vm_ip}:8000") else: logger.warning( "⚠️ VM deployed but ThrillWiki configuration may have failed" ) logger.info( "You can manually configure ThrillWiki by SSH'ing to the VM" ) else: logger.info( "📝 No repository URL provided - VM deployed but ThrillWiki not configured" ) logger.info( "Set REPO_URL environment variable to auto-configure ThrillWiki" ) return True except Exception as e: logger.error(f"❌ Complete deployment failed: {e}") return False def get_vm_info(self) -> dict: """Get VM information.""" return { "name": config.VM_NAME, "status": self.vm_manager.vm_status(), "ip": self.vm_manager.get_vm_ip(), "memory": config.VM_MEMORY, "vcpus": config.VM_VCPUS, "disk_size": config.VM_DISK_SIZE, "deployment_type": "template-based", } def main(): """Main entry point.""" import argparse parser = argparse.ArgumentParser( description="ThrillWiki Template-Based VM Manager - Fast VM deployment using templates", epilog=""" Examples: python main_template.py setup # Deploy VM from template only python main_template.py deploy # Deploy VM and configure ThrillWiki python main_template.py start # Start existing VM python main_template.py ip # Get VM IP address python main_template.py status # Get VM status python main_template.py delete # Remove VM completely python main_template.py template # Manage template VM """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "action", choices=[ "setup", "deploy", "create", "start", "stop", "status", "ip", "delete", "info", "template", ], help="Action to perform", ) parser.add_argument( "template_action", nargs="?", choices=["info", "check", "update", "list"], help="Template management action (used with 'template' action)", ) args = parser.parse_args() # Create orchestrator orchestrator = ThrillWikiTemplateVMOrchestrator() if args.action == "setup": logger.info("🚀 Setting up VM from template...") success = orchestrator.deploy_vm_from_template() sys.exit(0 if success else 1) elif args.action == "deploy": logger.info("🚀 Complete ThrillWiki deployment from template...") success = orchestrator.deploy_and_configure_thrillwiki() sys.exit(0 if success else 1) elif args.action == "create": logger.info("⚙️ Creating VM from template...") success = orchestrator.vm_manager.create_vm_from_template( config.VM_MEMORY, config.VM_VCPUS, config.VM_DISK_SIZE, config.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"ThrillWiki: http://{ip}:8000") 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") print(f" Type: {info['deployment_type']}") 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) elif args.action == "template": template_action = args.template_action or "info" if template_action == "info": logger.info("📋 Template VM Information") info = orchestrator.template_manager.get_template_info() if info: print(f"Template Path: {info['template_path']}") print(f"Virtual Size: {info['virtual_size']}") print(f"File Size: {info['file_size']}") print(f"Last Modified: {info['last_modified']}") else: print("❌ Failed to get template information") sys.exit(1) elif template_action == "check": if orchestrator.template_manager.check_template_exists(): logger.info("✅ Template VM disk exists and is ready to use") sys.exit(0) else: logger.error("❌ Template VM disk not found") sys.exit(1) elif template_action == "update": success = orchestrator.template_manager.update_template() sys.exit(0 if success else 1) elif template_action == "list": logger.info("📋 Template-based VM Instances") instances = orchestrator.template_manager.list_template_instances() if instances: for instance in instances: status_emoji = ( "🟢" if instance["status"] == "running" else "🔴" if instance["status"] == "shut off" else "🟡" ) print( f"{status_emoji} { instance['name']} ({ instance['status']})" ) else: print("No template instances found") sys.exit(0) if __name__ == "__main__": main()