Files
thrillwiki_django_no_react/shared/scripts/unraid/main_template.py
pacnpal d504d41de2 feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
2025-08-23 18:40:07 -04:00

457 lines
15 KiB
Python

#!/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()