mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 19:11:08 -05:00
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
This commit is contained in:
531
shared/scripts/unraid/iso_builder.py
Normal file
531
shared/scripts/unraid/iso_builder.py
Normal file
@@ -0,0 +1,531 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ubuntu ISO Builder for Autoinstall
|
||||
Follows the Ubuntu autoinstall guide exactly:
|
||||
1. Download Ubuntu ISO
|
||||
2. Extract with 7zip equivalent
|
||||
3. Modify GRUB configuration
|
||||
4. Add server/ directory with autoinstall config
|
||||
5. Rebuild ISO with xorriso equivalent
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Ubuntu ISO URLs with fallbacks
|
||||
UBUNTU_MIRRORS = [
|
||||
"https://releases.ubuntu.com", # Official Ubuntu releases (primary)
|
||||
"http://archive.ubuntu.com/ubuntu-releases", # Official archive
|
||||
"http://mirror.csclub.uwaterloo.ca/ubuntu-releases", # University of Waterloo
|
||||
"http://mirror.math.princeton.edu/pub/ubuntu-releases", # Princeton mirror
|
||||
]
|
||||
UBUNTU_24_04_ISO = "24.04/ubuntu-24.04.3-live-server-amd64.iso"
|
||||
UBUNTU_22_04_ISO = "22.04/ubuntu-22.04.3-live-server-amd64.iso"
|
||||
|
||||
|
||||
def get_latest_ubuntu_server_iso(version: str) -> Optional[str]:
|
||||
"""Dynamically find the latest point release for a given Ubuntu version."""
|
||||
try:
|
||||
import re
|
||||
|
||||
for mirror in UBUNTU_MIRRORS:
|
||||
try:
|
||||
url = f"{mirror}/{version}/"
|
||||
response = urllib.request.urlopen(url, timeout=10)
|
||||
content = response.read().decode("utf-8")
|
||||
|
||||
# Find all server ISO files for this version
|
||||
pattern = rf"ubuntu-{
|
||||
re.escape(version)}\.[0-9]+-live-server-amd64\.iso"
|
||||
matches = re.findall(pattern, content)
|
||||
|
||||
if matches:
|
||||
# Sort by version and return the latest
|
||||
matches.sort(key=lambda x: [int(n) for n in re.findall(r"\d+", x)])
|
||||
latest_iso = matches[-1]
|
||||
return f"{version}/{latest_iso}"
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to check {mirror}/{version}/: {e}")
|
||||
continue
|
||||
|
||||
logger.warning(f"Could not dynamically detect latest ISO for Ubuntu {version}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in dynamic ISO detection: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class UbuntuISOBuilder:
|
||||
"""Builds modified Ubuntu ISO with autoinstall configuration."""
|
||||
|
||||
def __init__(self, vm_name: str, work_dir: Optional[str] = None):
|
||||
self.vm_name = vm_name
|
||||
self.work_dir = (
|
||||
Path(work_dir)
|
||||
if work_dir
|
||||
else Path(tempfile.mkdtemp(prefix="ubuntu-autoinstall-"))
|
||||
)
|
||||
self.source_files_dir = self.work_dir / "source-files"
|
||||
self.boot_dir = self.work_dir / "BOOT"
|
||||
self.server_dir = self.source_files_dir / "server"
|
||||
self.grub_cfg_path = self.source_files_dir / "boot" / "grub" / "grub.cfg"
|
||||
|
||||
# Ensure directories exist
|
||||
self.work_dir.mkdir(exist_ok=True, parents=True)
|
||||
self.source_files_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def check_tools(self) -> bool:
|
||||
"""Check if required tools are available."""
|
||||
|
||||
# Check for 7zip equivalent (p7zip on macOS/Linux)
|
||||
if not shutil.which("7z") and not shutil.which("7za"):
|
||||
logger.error(
|
||||
"7zip not found. Install with: brew install p7zip (macOS) or apt install p7zip-full (Ubuntu)"
|
||||
)
|
||||
return False
|
||||
|
||||
# Check for xorriso equivalent
|
||||
if (
|
||||
not shutil.which("xorriso")
|
||||
and not shutil.which("mkisofs")
|
||||
and not shutil.which("hdiutil")
|
||||
):
|
||||
logger.error(
|
||||
"No ISO creation tool found. Install xorriso, mkisofs, or use macOS hdiutil"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def download_ubuntu_iso(self, version: str = "24.04") -> Path:
|
||||
"""Download Ubuntu ISO if not already present, trying multiple mirrors."""
|
||||
iso_filename = f"ubuntu-{version}-live-server-amd64.iso"
|
||||
iso_path = self.work_dir / iso_filename
|
||||
|
||||
if iso_path.exists():
|
||||
logger.info(f"Ubuntu ISO already exists: {iso_path}")
|
||||
return iso_path
|
||||
|
||||
if version == "24.04":
|
||||
iso_subpath = UBUNTU_24_04_ISO
|
||||
elif version == "22.04":
|
||||
iso_subpath = UBUNTU_22_04_ISO
|
||||
else:
|
||||
raise ValueError(f"Unsupported Ubuntu version: {version}")
|
||||
|
||||
# Try each mirror until one works
|
||||
last_error = None
|
||||
for mirror in UBUNTU_MIRRORS:
|
||||
iso_url = f"{mirror}/{iso_subpath}"
|
||||
logger.info(f"Trying to download Ubuntu {version} ISO from {iso_url}")
|
||||
|
||||
try:
|
||||
# Try downloading from this mirror
|
||||
urllib.request.urlretrieve(iso_url, iso_path)
|
||||
logger.info(
|
||||
f"✅ Ubuntu ISO downloaded successfully from {mirror}: {iso_path}"
|
||||
)
|
||||
return iso_path
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
logger.warning(f"Failed to download from {mirror}: {e}")
|
||||
# Remove partial download if it exists
|
||||
if iso_path.exists():
|
||||
iso_path.unlink()
|
||||
continue
|
||||
|
||||
# If we get here, all mirrors failed
|
||||
logger.error(
|
||||
f"Failed to download Ubuntu ISO from all mirrors. Last error: {last_error}"
|
||||
)
|
||||
raise last_error
|
||||
|
||||
def extract_iso(self, iso_path: Path) -> bool:
|
||||
"""Extract Ubuntu ISO following the guide."""
|
||||
logger.info(f"Extracting ISO: {iso_path}")
|
||||
|
||||
# Use 7z to extract ISO
|
||||
seven_zip_cmd = "7z" if shutil.which("7z") else "7za"
|
||||
|
||||
try:
|
||||
# Extract ISO: 7z -y x ubuntu.iso -osource-files
|
||||
subprocess.run(
|
||||
[
|
||||
seven_zip_cmd,
|
||||
"-y",
|
||||
"x",
|
||||
str(iso_path),
|
||||
f"-o{self.source_files_dir}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
logger.info("ISO extracted successfully")
|
||||
|
||||
# Move [BOOT] directory as per guide: mv '[BOOT]' ../BOOT
|
||||
boot_source = self.source_files_dir / "[BOOT]"
|
||||
if boot_source.exists():
|
||||
shutil.move(str(boot_source), str(self.boot_dir))
|
||||
logger.info(f"Moved [BOOT] directory to {self.boot_dir}")
|
||||
else:
|
||||
logger.warning("[BOOT] directory not found in extracted files")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to extract ISO: {e.stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting ISO: {e}")
|
||||
return False
|
||||
|
||||
def modify_grub_config(self) -> bool:
|
||||
"""Modify GRUB configuration to add autoinstall menu entry."""
|
||||
logger.info("Modifying GRUB configuration...")
|
||||
|
||||
if not self.grub_cfg_path.exists():
|
||||
logger.error(f"GRUB config not found: {self.grub_cfg_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Read existing GRUB config
|
||||
with open(self.grub_cfg_path, "r", encoding="utf-8") as f:
|
||||
grub_content = f.read()
|
||||
|
||||
# Autoinstall menu entry as per guide
|
||||
autoinstall_entry = """menuentry "Autoinstall Ubuntu Server" {
|
||||
set gfxpayload=keep
|
||||
linux /casper/vmlinuz quiet autoinstall ds=nocloud\\;s=/cdrom/server/ ---
|
||||
initrd /casper/initrd
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
# Insert autoinstall entry at the beginning of menu entries
|
||||
# Find the first menuentry and insert before it
|
||||
import re
|
||||
|
||||
first_menu_match = re.search(r'(menuentry\s+["\'])', grub_content)
|
||||
if first_menu_match:
|
||||
insert_pos = first_menu_match.start()
|
||||
modified_content = (
|
||||
grub_content[:insert_pos]
|
||||
+ autoinstall_entry
|
||||
+ grub_content[insert_pos:]
|
||||
)
|
||||
else:
|
||||
# Fallback: append at the end
|
||||
modified_content = grub_content + "\n" + autoinstall_entry
|
||||
|
||||
# Write modified GRUB config
|
||||
with open(self.grub_cfg_path, "w", encoding="utf-8") as f:
|
||||
f.write(modified_content)
|
||||
|
||||
logger.info("GRUB configuration modified successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to modify GRUB config: {e}")
|
||||
return False
|
||||
|
||||
def create_autoinstall_config(self, user_data: str) -> bool:
|
||||
"""Create autoinstall configuration in server/ directory."""
|
||||
logger.info("Creating autoinstall configuration...")
|
||||
|
||||
try:
|
||||
# Create server directory
|
||||
self.server_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Create empty meta-data file (as per guide)
|
||||
meta_data_path = self.server_dir / "meta-data"
|
||||
meta_data_path.touch()
|
||||
logger.info(f"Created empty meta-data: {meta_data_path}")
|
||||
|
||||
# Create user-data file with autoinstall configuration
|
||||
user_data_path = self.server_dir / "user-data"
|
||||
with open(user_data_path, "w", encoding="utf-8") as f:
|
||||
f.write(user_data)
|
||||
logger.info(f"Created user-data: {user_data_path}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create autoinstall config: {e}")
|
||||
return False
|
||||
|
||||
def rebuild_iso(self, output_path: Path) -> bool:
|
||||
"""Rebuild ISO with autoinstall configuration using xorriso."""
|
||||
logger.info(f"Rebuilding ISO: {output_path}")
|
||||
|
||||
try:
|
||||
# Change to source-files directory for xorriso command
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(self.source_files_dir)
|
||||
|
||||
# Remove existing output file
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
# Try different ISO creation methods in order of preference
|
||||
success = False
|
||||
|
||||
# Method 1: xorriso (most complete)
|
||||
if shutil.which("xorriso") and not success:
|
||||
try:
|
||||
logger.info("Trying xorriso method...")
|
||||
cmd = [
|
||||
"xorriso",
|
||||
"-as",
|
||||
"mkisofs",
|
||||
"-r",
|
||||
"-V",
|
||||
f"Ubuntu 24.04 LTS AUTO (EFIBIOS)",
|
||||
"-o",
|
||||
str(output_path),
|
||||
"--grub2-mbr",
|
||||
f"..{os.sep}BOOT{os.sep}1-Boot-NoEmul.img",
|
||||
"-partition_offset",
|
||||
"16",
|
||||
"--mbr-force-bootable",
|
||||
"-append_partition",
|
||||
"2",
|
||||
"28732ac11ff8d211ba4b00a0c93ec93b",
|
||||
f"..{os.sep}BOOT{os.sep}2-Boot-NoEmul.img",
|
||||
"-appended_part_as_gpt",
|
||||
"-iso_mbr_part_type",
|
||||
"a2a0d0ebe5b9334487c068b6b72699c7",
|
||||
"-c",
|
||||
"/boot.catalog",
|
||||
"-b",
|
||||
"/boot/grub/i386-pc/eltorito.img",
|
||||
"-no-emul-boot",
|
||||
"-boot-load-size",
|
||||
"4",
|
||||
"-boot-info-table",
|
||||
"--grub2-boot-info",
|
||||
"-eltorito-alt-boot",
|
||||
"-e",
|
||||
"--interval:appended_partition_2:::",
|
||||
"-no-emul-boot",
|
||||
".",
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
success = True
|
||||
logger.info("✅ ISO created with xorriso")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"xorriso failed: {e.stderr}")
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
# Method 2: mkisofs with joliet-long
|
||||
if shutil.which("mkisofs") and not success:
|
||||
try:
|
||||
logger.info("Trying mkisofs with joliet-long...")
|
||||
cmd = [
|
||||
"mkisofs",
|
||||
"-r",
|
||||
"-V",
|
||||
f"Ubuntu 24.04 LTS AUTO",
|
||||
"-cache-inodes",
|
||||
"-J",
|
||||
"-joliet-long",
|
||||
"-l",
|
||||
"-b",
|
||||
"boot/grub/i386-pc/eltorito.img",
|
||||
"-c",
|
||||
"boot.catalog",
|
||||
"-no-emul-boot",
|
||||
"-boot-load-size",
|
||||
"4",
|
||||
"-boot-info-table",
|
||||
"-o",
|
||||
str(output_path),
|
||||
".",
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
success = True
|
||||
logger.info("✅ ISO created with mkisofs (joliet-long)")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"mkisofs with joliet-long failed: {e.stderr}")
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
# Method 3: mkisofs without Joliet (fallback)
|
||||
if shutil.which("mkisofs") and not success:
|
||||
try:
|
||||
logger.info("Trying mkisofs without Joliet (fallback)...")
|
||||
cmd = [
|
||||
"mkisofs",
|
||||
"-r",
|
||||
"-V",
|
||||
f"Ubuntu 24.04 LTS AUTO",
|
||||
"-cache-inodes",
|
||||
"-l", # No -J (Joliet) to avoid filename conflicts
|
||||
"-b",
|
||||
"boot/grub/i386-pc/eltorito.img",
|
||||
"-c",
|
||||
"boot.catalog",
|
||||
"-no-emul-boot",
|
||||
"-boot-load-size",
|
||||
"4",
|
||||
"-boot-info-table",
|
||||
"-o",
|
||||
str(output_path),
|
||||
".",
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
success = True
|
||||
logger.info("✅ ISO created with mkisofs (no Joliet)")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(
|
||||
f"mkisofs without Joliet failed: {
|
||||
e.stderr}"
|
||||
)
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
# Method 4: macOS hdiutil
|
||||
if shutil.which("hdiutil") and not success:
|
||||
try:
|
||||
logger.info("Trying hdiutil (macOS)...")
|
||||
cmd = [
|
||||
"hdiutil",
|
||||
"makehybrid",
|
||||
"-iso",
|
||||
"-joliet",
|
||||
"-o",
|
||||
str(output_path),
|
||||
".",
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
success = True
|
||||
logger.info("✅ ISO created with hdiutil")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"hdiutil failed: {e.stderr}")
|
||||
if output_path.exists():
|
||||
output_path.unlink()
|
||||
|
||||
if not success:
|
||||
logger.error("All ISO creation methods failed")
|
||||
return False
|
||||
|
||||
# Verify the output file was created
|
||||
if not output_path.exists():
|
||||
logger.error("ISO file was not created despite success message")
|
||||
return False
|
||||
|
||||
logger.info(f"ISO rebuilt successfully: {output_path}")
|
||||
logger.info(
|
||||
f"ISO size: {output_path.stat().st_size / (1024 * 1024):.1f} MB"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rebuilding ISO: {e}")
|
||||
return False
|
||||
finally:
|
||||
# Return to original directory
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def build_autoinstall_iso(
|
||||
self, user_data: str, output_path: Path, ubuntu_version: str = "24.04"
|
||||
) -> bool:
|
||||
"""Complete ISO build process following the Ubuntu autoinstall guide."""
|
||||
logger.info(
|
||||
f"🚀 Starting Ubuntu {ubuntu_version} autoinstall ISO build process"
|
||||
)
|
||||
|
||||
try:
|
||||
# Step 1: Check tools
|
||||
if not self.check_tools():
|
||||
return False
|
||||
|
||||
# Step 2: Download Ubuntu ISO
|
||||
iso_path = self.download_ubuntu_iso(ubuntu_version)
|
||||
|
||||
# Step 3: Extract ISO
|
||||
if not self.extract_iso(iso_path):
|
||||
return False
|
||||
|
||||
# Step 4: Modify GRUB
|
||||
if not self.modify_grub_config():
|
||||
return False
|
||||
|
||||
# Step 5: Create autoinstall config
|
||||
if not self.create_autoinstall_config(user_data):
|
||||
return False
|
||||
|
||||
# Step 6: Rebuild ISO
|
||||
if not self.rebuild_iso(output_path):
|
||||
return False
|
||||
|
||||
logger.info(f"🎉 Successfully created autoinstall ISO: {output_path}")
|
||||
logger.info(f"📁 Work directory: {self.work_dir}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to build autoinstall ISO: {e}")
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up temporary work directory."""
|
||||
if self.work_dir.exists():
|
||||
shutil.rmtree(self.work_dir)
|
||||
logger.info(f"Cleaned up work directory: {self.work_dir}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Test the ISO builder."""
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Sample autoinstall user-data
|
||||
user_data = """#cloud-config
|
||||
autoinstall:
|
||||
version: 1
|
||||
packages:
|
||||
- ubuntu-server
|
||||
identity:
|
||||
realname: 'Test User'
|
||||
username: testuser
|
||||
password: '$6$rounds=4096$saltsalt$[AWS-SECRET-REMOVED]AzpI8g8T14F8VnhXo0sUkZV2NV6/.c77tHgVi34DgbPu.'
|
||||
hostname: test-vm
|
||||
locale: en_US.UTF-8
|
||||
keyboard:
|
||||
layout: us
|
||||
storage:
|
||||
layout:
|
||||
name: direct
|
||||
ssh:
|
||||
install-server: true
|
||||
late-commands:
|
||||
- curtin in-target -- apt-get autoremove -y
|
||||
"""
|
||||
|
||||
builder = UbuntuISOBuilder("test-vm")
|
||||
output_path = Path("/tmp/ubuntu-24.04-autoinstall.iso")
|
||||
|
||||
success = builder.build_autoinstall_iso(user_data, output_path)
|
||||
if success:
|
||||
print(f"✅ ISO created: {output_path}")
|
||||
else:
|
||||
print("❌ ISO creation failed")
|
||||
|
||||
# Optionally clean up
|
||||
# builder.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user