Files
thrillwiki_django_no_react/shared/scripts/unraid/iso_builder.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

532 lines
19 KiB
Python

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