mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 05:31:08 -05:00
- Implemented extensive test cases for the Parks API, covering endpoints for listing, retrieving, creating, updating, and deleting parks. - Added tests for filtering, searching, and ordering parks in the API. - Created tests for error handling in the API, including malformed JSON and unsupported methods. - Developed model tests for Park, ParkArea, Company, and ParkReview models, ensuring validation and constraints are enforced. - Introduced utility mixins for API and model testing to streamline assertions and enhance test readability. - Included integration tests to validate complete workflows involving park creation, retrieval, updating, and deletion.
452 lines
18 KiB
Python
452 lines
18 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 sys
|
|
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."""
|
|
required_tools = []
|
|
|
|
# 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
|
|
result = 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",
|
|
"."
|
|
]
|
|
result = 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),
|
|
"."
|
|
]
|
|
result = 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),
|
|
"."
|
|
]
|
|
result = 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), "."
|
|
]
|
|
result = 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()
|