Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -10,7 +10,6 @@ Follows the Ubuntu autoinstall guide exactly:
"""
import os
import sys
import logging
import subprocess
import tempfile
@@ -26,7 +25,7 @@ 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
"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"
@@ -36,28 +35,30 @@ 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')
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'
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)])
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
@@ -65,61 +66,74 @@ def get_latest_ubuntu_server_iso(version: str) -> Optional[str]:
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.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)")
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")
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}")
logger.info(
f"✅ Ubuntu ISO downloaded successfully from {mirror}: {iso_path}"
)
return iso_path
except Exception as e:
last_error = e
@@ -128,27 +142,37 @@ class UbuntuISOBuilder:
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}")
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)
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():
@@ -156,249 +180,304 @@ class UbuntuISOBuilder:
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:
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" {
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:]
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:
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:
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",
"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",
"-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:::",
"-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)
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),
"."
"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)
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),
"."
"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)
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}")
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), "."
"hdiutil",
"makehybrid",
"-iso",
"-joliet",
"-o",
str(output_path),
".",
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
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")
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:
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")
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
# 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():
@@ -409,8 +488,9 @@ class UbuntuISOBuilder:
def main():
"""Test the ISO builder."""
import logging
logging.basicConfig(level=logging.INFO)
# Sample autoinstall user-data
user_data = """#cloud-config
autoinstall:
@@ -433,16 +513,16 @@ autoinstall:
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()