Compare commits

...

36 Commits

Author SHA1 Message Date
ada17eefd7 Switch to systemd-boot for UEFI while keeping GRUB2 for BIOS/MBR 2026-02-10 13:29:02 +01:00
2e8e99f481 Add --force to UEFI grub2-install to bypass Fedora Secure Boot warning 2026-02-10 13:26:49 +01:00
53227e0f8e Fix UEFI grub2-install: add missing modules package and fix DNF stderr logging 2026-02-09 19:05:27 +01:00
2762f2f767 Robust log capture on SIGINT/SIGTERM and cleaner Discord reporting 2026-02-09 18:39:17 +01:00
19254e128c Enhance Discord error reporting: code blocks and crash robustness 2026-02-09 18:00:20 +01:00
5abba6ce88 Improve Discord logging for errors and further fix UEFI mount robustness 2026-02-09 17:41:12 +01:00
0ec7de3937 Fix UEFI GRUB installation: ensure ESP is mounted to /boot/efi and improve error logging 2026-02-09 16:53:30 +01:00
cbc1b05eca Simplify run_vm_bios.sh to unconditionally load clean-state snapshot 2026-02-09 15:24:46 +01:00
e8afc31d00 Enable snapshot support in run_vm_bios.sh with automatic detection 2026-02-09 15:14:09 +01:00
59219130bd Fix run_vm_bios.sh: add missing backslashes and improve CDROM boot compatibility 2026-02-09 15:08:23 +01:00
e8a2f7aaa9 Redo backend installer logic to support BIOS/MBR boot on older devices and switch to GRUB2 2026-02-09 15:02:14 +01:00
b37fc8d060 Update run_vm_uefi.sh 2026-02-09 14:22:45 +01:00
44af152887 Revert "Improve offline repository discovery and dnf configuration for ISO installs"
This reverts commit 297f2cd3c2.
2026-02-05 19:43:12 +01:00
5238fd29d9 Revert "Fix dnf failure by adding /run mount, removing --cacheonly, and ensuring host-config usage"
This reverts commit 951a1b7fdc.
2026-02-05 19:43:12 +01:00
74fedf9001 Revert "Further improvements to dnf robustness and repo discovery for offline installs"
This reverts commit 5df00a5814.
2026-02-05 19:43:12 +01:00
5df00a5814 Further improvements to dnf robustness and repo discovery for offline installs 2026-02-05 19:39:54 +01:00
951a1b7fdc Fix dnf failure by adding /run mount, removing --cacheonly, and ensuring host-config usage 2026-02-05 19:32:56 +01:00
297f2cd3c2 Improve offline repository discovery and dnf configuration for ISO installs 2026-02-05 19:28:41 +01:00
ce2c242454 revert 4278a3198d
revert Add minimal-linux-installer skill with proven installation patterns
2026-02-05 18:32:01 +01:00
4278a3198d Add minimal-linux-installer skill with proven installation patterns 2026-02-05 18:30:55 +01:00
943cfee8bb IT ACTUALLY INSTALLS 2026-02-05 18:16:08 +01:00
9cfd6ad2e0 Use robust usermod -p method for setting passwords 2026-02-05 17:54:14 +01:00
23b5f017d4 Fix password setting using chpasswd -R and improve screen wake lock 2026-02-05 17:39:01 +01:00
ee83411333 Fix password setting by using passwd --stdin and sync 2026-02-05 17:25:00 +01:00
7613bdf8d5 Fix password setting and inhibit system sleep during install 2026-02-05 17:03:36 +01:00
400235067d Fix bootctl unrecognized option and improve repo discovery 2026-02-05 16:46:59 +01:00
f3e7122d02 Fix dnf5 deferred transaction and add shadow-utils for user creation 2026-02-05 16:27:57 +01:00
afe18c1eee Fix dnf error by removing localpkg_only and improve repo search 2026-02-05 15:40:42 +01:00
76c638de8e Improve offline installation by searching for repo in multiple paths 2026-02-05 15:37:18 +01:00
1238dcdabd Remove accidental log file 2026-02-05 15:17:53 +01:00
f7fc354f3f Fix UEFI boot by mounting efivarfs and forcing systemd-boot install 2026-02-05 15:17:48 +01:00
3a226108ec Fix: ensure disk is unmounted before partitioning and use sudo for reboot 2026-02-05 14:55:18 +01:00
45b0fa5a84 . 2026-02-05 14:54:15 +01:00
6b7c7debff Increase VM memory allocation to 8GB 2026-02-05 14:49:59 +01:00
9fcbf6f204 Add automatic dependency installation to run.sh 2026-02-05 14:34:35 +01:00
e5f46324e4 Fix UEFI snapshot support and ensure sudo privileges 2026-02-05 14:23:44 +01:00
9 changed files with 426 additions and 223 deletions

View File

@@ -97,120 +97,128 @@ def get_disk_size(disk_device):
return 0 return 0
def ensure_unmounted(disk_device):
"""
Finds all mounted partitions belonging to disk_device and unmounts them.
"""
logger.info(f"Ensuring all partitions on {disk_device} are unmounted...")
try:
# Get all partitions for this disk from lsblk
res = run_command(["lsblk", "-nlo", "NAME,MOUNTPOINT", disk_device])
for line in res.stdout.splitlines():
parts = line.split()
if len(parts) >= 2:
# NAME is usually the leaf name (e.g. vda1), we need full path
# But lsblk -lp gives full paths
pass
# Simpler approach: findmnt or just look at /proc/mounts
# Let's use lsblk -lp to get full paths
res = run_command(["lsblk", "-lpno", "NAME,MOUNTPOINT", disk_device])
for line in res.stdout.splitlines():
# line looks like "/dev/vda1 /mnt/boot" or "/dev/vda1"
parts = line.split()
if len(parts) >= 2:
dev_path = parts[0]
mount_point = parts[1]
if mount_point and mount_point != "None":
logger.info(f"Unmounting {dev_path} from {mount_point}...")
run_command(["umount", "-l", dev_path])
except Exception as e:
logger.warning(f"Error during pre-partition unmount: {e}")
def is_uefi():
"""Checks if the system is booted in UEFI mode."""
return os.path.exists("/sys/firmware/efi")
def auto_partition_disk(disk_device): def auto_partition_disk(disk_device):
""" """
Automatically partitions the disk: Automatically partitions the disk based on boot mode:
1. EFI System Partition (2GB standard, 1GB small) UEFI (GPT): ESP (1GB), Root (Remaining), Swap (RAM+2GB)
2. Root (Remaining) BIOS (MBR): Root (Remaining, Bootable), Swap (RAM+2GB)
3. Swap (RAM + 2GB, or 0 if small)
Layout: P1=EFI, P3=Swap (End), P2=Root (Middle)
""" """
logger.info(f"Starting auto-partitioning on {disk_device}") uefi = is_uefi()
log_disk_operation("PARTITION", "start", f"Disk: {disk_device}") logger.info(f"Starting auto-partitioning on {disk_device} (UEFI: {uefi})")
log_disk_operation("PARTITION", "start", f"Disk: {disk_device}, UEFI: {uefi}")
ensure_unmounted(disk_device)
# Calculate sizes
disk_size = get_disk_size(disk_device) disk_size = get_disk_size(disk_device)
ram_size = get_total_memory() ram_size = get_total_memory()
# Defaults # Calculate sizes
efi_mb = 2048
swap_mb = int((ram_size / (1024 * 1024)) + 2048) swap_mb = int((ram_size / (1024 * 1024)) + 2048)
min_root_mb = 10240 # 10GB # Cap swap at 16GB for sanity on large RAM systems unless disk is huge
if disk_size < 100 * 1024**3: # < 100GB
total_required_mb = efi_mb + swap_mb + min_root_mb swap_mb = min(swap_mb, 4096)
disk_mb = disk_size / (1024 * 1024) disk_mb = disk_size / (1024 * 1024)
use_swap = disk_mb > (10240 + swap_mb) # Only use swap if we have at least 10GB for root
use_swap = True if uefi:
# GPT Layout
logger.info("Using GPT layout for UEFI")
run_command(["sgdisk", "-Z", disk_device])
run_command(["sgdisk", "-o", disk_device])
# 1. ESP (1GB)
run_command(["sgdisk", "-n", "1:0:+1024M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
# 2. Swap (if enabled)
if use_swap:
run_command(["sgdisk", "-n", f"3:-{swap_mb}M:0", "-t", "3:8200", "-c", "3:Swap", disk_device])
# 3. Root
run_command(["sgdisk", "-n", "2:0:0", "-t", "2:8300", "-c", "2:Root", disk_device])
else:
# MBR Layout for BIOS
logger.info("Using MBR layout for BIOS")
# Wipe disk using dd to be sure MBR is cleared
run_command(["dd", "if=/dev/zero", f"of={disk_device}", "bs=512", "count=100"])
# Use parted for MBR
run_command(["parted", "-s", disk_device, "mklabel", "msdos"])
if use_swap:
# Root first, then swap at end
root_end = int(disk_mb - swap_mb)
run_command(["parted", "-s", disk_device, "mkpart", "primary", "ext4", "1MiB", f"{root_end}MiB"])
run_command(["parted", "-s", disk_device, "mkpart", "primary", "linux-swap", f"{root_end}MiB", "100%"])
run_command(["parted", "-s", disk_device, "set", "1", "boot", "on"])
else:
run_command(["parted", "-s", disk_device, "mkpart", "primary", "ext4", "1MiB", "100%"])
run_command(["parted", "-s", disk_device, "set", "1", "boot", "on"])
if disk_mb < total_required_mb:
logger.warning("Disk too small for standard layout. Adjusting...")
efi_mb = 1024
use_swap = False
# Check minimal viability
if disk_mb < (efi_mb + min_root_mb):
raise Exception("Disk too small for installation (Need ~11GB)")
# 1. Zap the disk (destroy all data)
run_command(["sgdisk", "-Z", disk_device])
# 2. Create new GPT table
run_command(["sgdisk", "-o", disk_device])
# 3. Create EFI Partition (Part 1, Start)
run_command(
[
"sgdisk",
"-n",
f"1:0:+{efi_mb}M",
"-t",
"1:ef00",
"-c",
"1:EFI System",
disk_device,
]
)
# 4. Create Swap Partition (Part 3, End) - If enabled
if use_swap:
# sgdisk negative start is from end of disk
# We use partition 3 for Swap
run_command(
[
"sgdisk",
"-n",
f"3:-{swap_mb}M:0",
"-t",
"3:8200",
"-c",
"3:Swap",
disk_device,
]
)
# 5. Create Root Partition (Part 2, Fill Gap)
# This fills the space between P1 and P3 (or end if no swap)
run_command(["sgdisk", "-n", "2:0:0", "-t", "2:8300", "-c", "2:Root", disk_device])
# Inform kernel of changes
run_command(["partprobe", disk_device]) run_command(["partprobe", disk_device])
import time import time
time.sleep(2)
time.sleep(1) # Identify partitions
if uefi:
efi_part = get_partition_device(disk_device, 1)
root_part = get_partition_device(disk_device, 2)
swap_part = get_partition_device(disk_device, 3) if use_swap else None
else:
efi_part = None # BIOS doesn't use ESP
root_part = get_partition_device(disk_device, 1)
swap_part = get_partition_device(disk_device, 2) if use_swap else None
# 6. Format Partitions # Format
efi_part = get_partition_device(disk_device, 1) if uefi:
root_part = get_partition_device(disk_device, 2) logger.info(f"Formatting EFI partition {efi_part}...")
swap_part = get_partition_device(disk_device, 3) if use_swap else None run_command(["mkfs.vfat", "-F32", efi_part])
logger.info("Formatting EFI partition...")
run_command(["mkfs.vfat", "-F32", efi_part])
if use_swap: if use_swap:
logger.info("Formatting Swap partition...") logger.info(f"Formatting Swap partition {swap_part}...")
run_command(["mkswap", swap_part]) run_command(["mkswap", swap_part])
logger.info("Formatting Root partition...") logger.info(f"Formatting Root partition {root_part}...")
run_command(["mkfs.ext4", "-F", root_part]) run_command(["mkfs.ext4", "-F", root_part])
logger.info("Partitioning and formatting complete.") result = {"efi": efi_part, "root": root_part, "swap": swap_part}
log_disk_operation( log_disk_operation("PARTITION", "complete", str(result))
"PARTITION",
"complete",
f"EFI: {efi_part}, Root: {root_part}, Swap: {swap_part or 'none'}",
)
result = {"efi": efi_part, "root": root_part}
if use_swap:
result["swap"] = swap_part
else:
# If no swap, we should probably return None or handle it in fstab generation
# backend/os_install.py expects "swap" key for UUID.
# We should probably pass a dummy or None, and update os_install to handle it.
result["swap"] = None
return result return result
@@ -219,32 +227,30 @@ def mount_partitions(partition_info, mount_root="/mnt"):
Mounts the partitions into mount_root. Mounts the partitions into mount_root.
""" """
import os import os
log_disk_operation("MOUNT", "start", f"Mounting to {mount_root}") log_disk_operation("MOUNT", "start", f"Mounting to {mount_root}")
# 1. Mount Root # 1. Mount Root
if not os.path.exists(mount_root): if not os.path.exists(mount_root):
os.makedirs(mount_root) os.makedirs(mount_root)
run_command(["mount", partition_info["root"], mount_root]) run_command(["mount", partition_info["root"], mount_root])
# 2. Mount EFI # 2. Mount EFI (if exists)
efi_mount = os.path.join(mount_root, "boot") if partition_info.get("efi"):
if not os.path.exists(efi_mount): # For systemd-boot, we prefer mounting ESP to /boot
efi_mount = os.path.join(mount_root, "boot")
os.makedirs(efi_mount, exist_ok=True) os.makedirs(efi_mount, exist_ok=True)
run_command(["mount", partition_info["efi"], efi_mount])
run_command(["mount", partition_info["efi"], efi_mount]) else:
# For BIOS, /boot is just a directory on root or we could make a separate /boot
# Current logic keeps it on root.
pass
# 3. Enable Swap # 3. Enable Swap
if partition_info.get("swap"): if partition_info.get("swap"):
run_command(["swapon", partition_info["swap"]]) run_command(["swapon", partition_info["swap"]])
logger.info(f"Partitions mounted at {mount_root}") logger.info(f"Partitions mounted at {mount_root}")
log_disk_operation( log_disk_operation("MOUNT", "complete", f"Root: {partition_info['root']}")
"MOUNT",
"complete",
f"Root: {partition_info['root']}, EFI: {partition_info['efi']}",
)
def create_partition( def create_partition(

View File

@@ -3,21 +3,31 @@ import requests
import threading import threading
import time import time
import os import os
import atexit
import sys
import signal
from datetime import datetime from datetime import datetime
logger = logging.getLogger(__name__ + ".network_logging") logger = logging.getLogger(__name__ + ".network_logging")
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1468696228647932280/L9XSHS6TPEeK0wwJTFdK9RUyZvztSGQBd4xEfVvb4Y1AXGQAOc4YTsuxeFuWC9HxymJn" DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1468696228647932280/L9XSHS6TPEeK0wwJTFdK9RUyZvztSGQBd4xEfVvb4Y1AXGQAOc4YTsuxeFuWC9HxymJn"
LOG_QUEUE = []
FULL_LOG = [] FULL_LOG = []
QUEUE_LOCK = threading.Lock() QUEUE_LOCK = threading.Lock()
ENABLED = True ENABLED = True
LOG_SENT = False
def init_network_logging(enabled: bool = True): def init_network_logging(enabled: bool = True):
global ENABLED global ENABLED
ENABLED = enabled ENABLED = enabled
if ENABLED:
atexit.register(send_full_log)
# Handle signals to ensure atexit runs
def signal_handler(sig, frame):
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def log_to_discord(level: str, message: str, module: str = "general"): def log_to_discord(level: str, message: str, module: str = "general"):
@@ -34,20 +44,31 @@ def log_to_discord(level: str, message: str, module: str = "general"):
with QUEUE_LOCK: with QUEUE_LOCK:
FULL_LOG.append(log_entry) FULL_LOG.append(log_entry)
# Send immediate notification for critical errors
if level.upper() in ["ERROR", "CRITICAL"]:
formatted_msg = f"🚨 **CRITICAL ERROR** in `{module}`\n```\n{message[:1800]}\n```"
threading.Thread(
target=send_discord_message,
args=(formatted_msg,),
daemon=False
).start()
def flush_logs(): def flush_logs():
# Deprecated: No partial flushing anymore
pass pass
def send_full_log(): def send_full_log():
"""Sends the entire session log as a text file attachment at the end.""" """Sends the entire session log as a text file attachment at the end."""
if not ENABLED: global LOG_SENT
if not ENABLED or LOG_SENT:
return return
LOG_SENT = True
# Give a tiny bit of time for final async logs to land # Final sync wait
time.sleep(0.5) time.sleep(1)
with QUEUE_LOCK: with QUEUE_LOCK:
logs_to_send = FULL_LOG.copy() logs_to_send = FULL_LOG.copy()
@@ -55,29 +76,31 @@ def send_full_log():
if not logs_to_send: if not logs_to_send:
return return
def send_sync(): temp_file = f"/tmp/iridium_install_log_{int(time.time())}.txt"
temp_file = "/tmp/iridium_install_log.txt" try:
try: error_count = 0
with open(temp_file, "w") as f: with open(temp_file, "w") as f:
for log in logs_to_send: for log in logs_to_send:
ts = log["timestamp"][:19].replace("T", " ") if log["level"] in ["ERROR", "CRITICAL"]:
line = f"[{ts}] [{log['level']}] [{log['module']}] {log['message']}\n" error_count += 1
f.write(line) ts = log["timestamp"][:19].replace("T", " ")
line = f"[{ts}] [{log['level']}] [{log['module']}] {log['message']}\n"
f.write(line)
with open(temp_file, "rb") as f:
files = {"file": ("iridium_install_log.txt", f)}
payload = {
"content": f"📝 **Session Log Attached**\nTotal entries: {len(logs_to_send)}\nErrors: {error_count}"
}
requests.post(DISCORD_WEBHOOK_URL, data=payload, files=files, timeout=30)
with open(temp_file, "rb") as f: except Exception as e:
files = {"file": ("iridium_install_log.txt", f)} print(f"Failed to send full log file to Discord: {e}")
requests.post(DISCORD_WEBHOOK_URL, files=files, timeout=30) finally:
if os.path.exists(temp_file):
except Exception as e: try:
print(f"Failed to send full log file to Discord: {e}")
finally:
if os.path.exists(temp_file):
os.remove(temp_file) os.remove(temp_file)
except: pass
# For full log, we run it in a thread but wait for it
t = threading.Thread(target=send_sync)
t.start()
t.join(timeout=30)
def send_discord_message(content: str): def send_discord_message(content: str):

View File

@@ -50,8 +50,9 @@ def run_command(cmd, check=True):
t1 = threading.Thread( t1 = threading.Thread(
target=read_stream, args=(process.stdout, stdout_lines, logger.info) target=read_stream, args=(process.stdout, stdout_lines, logger.info)
) )
# Log stderr as INFO to avoid Discord notification spam, but still capture it
t2 = threading.Thread( t2 = threading.Thread(
target=read_stream, args=(process.stderr, stderr_lines, logger.error) target=read_stream, args=(process.stderr, stderr_lines, logger.info)
) )
t1.start() t1.start()
@@ -66,6 +67,9 @@ def run_command(cmd, check=True):
stderr_str = "".join(stderr_lines) stderr_str = "".join(stderr_lines)
if check and returncode != 0: if check and returncode != 0:
error_msg = f"Command failed: {' '.join(cmd)}\nExit Code: {returncode}\nStderr: {stderr_str}"
# Log this specific failure as ERROR so it DOES go to Discord
logger.error(error_msg)
raise subprocess.CalledProcessError( raise subprocess.CalledProcessError(
returncode, cmd, output=stdout_str, stderr=stderr_str returncode, cmd, output=stdout_str, stderr=stderr_str
) )
@@ -76,7 +80,7 @@ def run_command(cmd, check=True):
@contextmanager @contextmanager
def mount_pseudo_fs(mount_root): def mount_pseudo_fs(mount_root):
""" """
Context manager to bind mount /dev, /proc, and /sys into mount_root. Context manager to bind mount /dev, /proc, /sys, and efivarfs into mount_root.
""" """
logger.info(f"Mounting pseudo-filesystems to {mount_root}...") logger.info(f"Mounting pseudo-filesystems to {mount_root}...")
mounts = ["dev", "proc", "sys"] mounts = ["dev", "proc", "sys"]
@@ -88,6 +92,24 @@ def mount_pseudo_fs(mount_root):
os.makedirs(target, exist_ok=True) os.makedirs(target, exist_ok=True)
run_command(["mount", "--bind", f"/{fs}", target]) run_command(["mount", "--bind", f"/{fs}", target])
mounted_paths.append(target) mounted_paths.append(target)
# Mount efivarfs if it exists on the host
efivars_path = "/sys/firmware/efi/efivars"
if os.path.exists(efivars_path):
target = os.path.join(mount_root, "sys/firmware/efi/efivars")
os.makedirs(target, exist_ok=True)
try:
# Try bind mount first
run_command(["mount", "--bind", efivars_path, target])
mounted_paths.append(target)
except Exception:
try:
# Fallback to direct mount
run_command(["mount", "-t", "efivarfs", "efivarfs", target])
mounted_paths.append(target)
except Exception as e:
logger.warning(f"Failed to mount efivarfs: {e}")
yield yield
finally: finally:
logger.info(f"Unmounting pseudo-filesystems from {mount_root}...") logger.info(f"Unmounting pseudo-filesystems from {mount_root}...")
@@ -98,6 +120,10 @@ def mount_pseudo_fs(mount_root):
logger.warning(f"Failed to unmount {path}: {e}") logger.warning(f"Failed to unmount {path}: {e}")
def is_uefi():
return os.path.exists("/sys/firmware/efi")
def install_minimal_os(mount_root, releasever="43"): def install_minimal_os(mount_root, releasever="43"):
""" """
Installs minimal Fedora packages to mount_root. Installs minimal Fedora packages to mount_root.
@@ -105,6 +131,8 @@ def install_minimal_os(mount_root, releasever="43"):
logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...") logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...")
log_os_install("INSTALL", "start", f"Target: {mount_root}, Release: {releasever}") log_os_install("INSTALL", "start", f"Target: {mount_root}, Release: {releasever}")
uefi = is_uefi()
packages = [ packages = [
"basesystem", "basesystem",
"bash", "bash",
@@ -112,25 +140,62 @@ def install_minimal_os(mount_root, releasever="43"):
"kernel", "kernel",
"systemd", "systemd",
"dnf", "dnf",
"efibootmgr", "shadow-utils",
"util-linux",
"passwd", "passwd",
"rootfiles", "rootfiles",
"vim-minimal", "vim-minimal",
] ]
if uefi:
packages += ["systemd-boot-unsigned", "efibootmgr"]
else:
packages += ["grub2-pc", "grub2-tools", "grubby"]
# Offline installation logic # Offline installation logic
iso_repo = "/run/install/repo" possible_repos = [
dnf_args = [] "/run/install/repo",
"/run/install/source",
"/mnt/install/repo",
"/run/initramfs/live",
"/run/initramfs/isoscan",
]
if os.path.exists(iso_repo): iso_repo = None
for path in possible_repos:
if os.path.exists(os.path.join(path, "repodata")):
iso_repo = path
break
elif os.path.exists(os.path.join(path, "Packages")):
iso_repo = path
break
# Try searching in /run/media if not found
if not iso_repo and os.path.exists("/run/media"):
try:
for user in os.listdir("/run/media"):
user_path = os.path.join("/run/media", user)
if os.path.isdir(user_path):
for label in os.listdir(user_path):
label_path = os.path.join(user_path, label)
if os.path.exists(os.path.join(label_path, "repodata")):
iso_repo = label_path
break
if iso_repo: break
except Exception: pass
dnf_args = []
if iso_repo:
logger.info(f"Found ISO repository at {iso_repo}. Using strictly offline mode.") logger.info(f"Found ISO repository at {iso_repo}. Using strictly offline mode.")
dnf_args = [ dnf_args = [
"--disablerepo=*", "--disablerepo=*",
f"--repofrompath=iridium-iso,{iso_repo}", f"--repofrompath=iridium-iso,{iso_repo}",
"--enablerepo=iridium-iso" "--enablerepo=iridium-iso",
"--cacheonly",
] ]
else: else:
logger.warning(f"ISO repository not found at {iso_repo}. Trying to use host config (may require network).") logger.warning("ISO repository not found. DNF might try to use network.")
dnf_args = []
cmd = [ cmd = [
"dnf", "dnf",
@@ -138,10 +203,14 @@ def install_minimal_os(mount_root, releasever="43"):
"-y", "-y",
f"--installroot={mount_root}", f"--installroot={mount_root}",
f"--releasever={releasever}", f"--releasever={releasever}",
"--use-host-config",
"--setopt=install_weak_deps=False", "--setopt=install_weak_deps=False",
"--nodocs", "--nodocs",
] + dnf_args + packages ]
if not iso_repo:
cmd.append("--use-host-config")
cmd += dnf_args + packages
with mount_pseudo_fs(mount_root): with mount_pseudo_fs(mount_root):
run_command(cmd) run_command(cmd)
@@ -150,89 +219,114 @@ def install_minimal_os(mount_root, releasever="43"):
log_os_install("INSTALL", "complete", f"Installed to {mount_root}") log_os_install("INSTALL", "complete", f"Installed to {mount_root}")
def configure_system(mount_root, partition_info, user_info=None): def configure_system(mount_root, partition_info, user_info=None, disk_device=None):
""" """
Basic configuration: fstab, systemd-boot, and user creation. Basic configuration: fstab, bootloader, and user creation.
""" """
logger.info("Configuring system...") logger.info("Configuring system...")
log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}") log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}")
uefi = is_uefi()
# 1. Generate fstab # 1. Generate fstab
def get_uuid(dev): def get_uuid(dev):
res = run_command(["blkid", "-s", "UUID", "-o", "value", dev]) res = run_command(["blkid", "-s", "UUID", "-o", "value", dev])
return res.stdout.strip() return res.stdout.strip()
root_uuid = get_uuid(partition_info["root"]) root_uuid = get_uuid(partition_info["root"])
efi_uuid = get_uuid(partition_info["efi"])
fstab_lines = [f"UUID={root_uuid} / ext4 defaults 1 1"]
if uefi and partition_info.get("efi"):
efi_uuid = get_uuid(partition_info["efi"])
# For systemd-boot, we mount ESP to /boot
fstab_lines.append(f"UUID={efi_uuid} /boot vfat defaults 0 2")
swap_entry = ""
if partition_info.get("swap"): if partition_info.get("swap"):
swap_uuid = get_uuid(partition_info["swap"]) swap_uuid = get_uuid(partition_info["swap"])
swap_entry = f"UUID={swap_uuid} none swap defaults 0 0\n" fstab_lines.append(f"UUID={swap_uuid} none swap defaults 0 0")
fstab_content = f"""
UUID={root_uuid} / ext4 defaults 1 1
UUID={efi_uuid} /boot vfat defaults 0 2
{swap_entry}
"""
os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True) os.makedirs(os.path.join(mount_root, "etc"), exist_ok=True)
with open(os.path.join(mount_root, "etc/fstab"), "w") as f: with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
f.write(fstab_content) f.write("\n".join(fstab_lines) + "\n")
# 2. Configure User # Ensure EFI is mounted for bootloader installation if UEFI
if user_info: if uefi and partition_info.get("efi"):
logger.info(f"Creating user {user_info['username']}...") efi_target = os.path.join(mount_root, "boot")
with mount_pseudo_fs(mount_root): os.makedirs(efi_target, exist_ok=True)
# Create user and add to wheel group (sudoer) # Check if already mounted
res = subprocess.run(["mount"], capture_output=True, text=True)
if efi_target not in res.stdout:
run_command(["mount", partition_info["efi"], efi_target])
with mount_pseudo_fs(mount_root):
# 2. Configure User
if user_info:
logger.info(f"Creating user {user_info['username']}...")
run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]]) run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]])
# Set password
p = subprocess.Popen(["chroot", mount_root, "chpasswd"], stdin=subprocess.PIPE, text=True)
p.communicate(input=f"{user_info['username']}:{user_info['password']}")
# Set hostname # Set hostname
with open(os.path.join(mount_root, "etc/hostname"), "w") as f: with open(os.path.join(mount_root, "etc/hostname"), "w") as f:
f.write(user_info["hostname"] + "\n") f.write(user_info["hostname"] + "\n")
# 3. Configure systemd-boot # Set passwords
with mount_pseudo_fs(mount_root): try:
# Ensure machine-id exists for kernel-install res = subprocess.run(
if not os.path.exists(os.path.join(mount_root, "etc/machine-id")): ["openssl", "passwd", "-6", user_info["password"]],
run_command(["chroot", mount_root, "systemd-machine-id-setup"]) capture_output=True,
text=True,
check=True
)
hashed_pass = res.stdout.strip()
run_command(["chroot", mount_root, "usermod", "-p", hashed_pass, user_info["username"]])
run_command(["chroot", mount_root, "usermod", "-p", hashed_pass, "root"])
except Exception as e:
logger.error(f"Failed to set passwords: {e}")
# Set kernel command line # 3. Configure Bootloader
os.makedirs(os.path.join(mount_root, "etc/kernel"), exist_ok=True) if uefi:
with open(os.path.join(mount_root, "etc/kernel/cmdline"), "w") as f: logger.info("Configuring systemd-boot...")
f.write(f"root=UUID={root_uuid} rw quiet\n") if not os.path.exists(os.path.join(mount_root, "etc/machine-id")):
run_command(["chroot", mount_root, "systemd-machine-id-setup"])
# Install systemd-boot to the ESP (mounted at /boot) os.makedirs(os.path.join(mount_root, "etc/kernel"), exist_ok=True)
# Use --path=/boot to be explicit, though standard behavior usually finds it. with open(os.path.join(mount_root, "etc/kernel/cmdline"), "w") as f:
run_command(["chroot", mount_root, "bootctl", "install", "--path=/boot"]) f.write(f"root=UUID={root_uuid} rw quiet\n")
with open(os.path.join(mount_root, "etc/kernel/layout"), "w") as f:
f.write("bls\n")
# Add kernel entries run_command(["chroot", mount_root, "bootctl", "install", "--path=/boot"])
modules_dir = os.path.join(mount_root, "lib/modules")
if os.path.exists(modules_dir):
kvers = [d for d in os.listdir(modules_dir) if os.path.isdir(os.path.join(modules_dir, d))]
for kver in kvers:
logger.info(f"Adding kernel entry for {kver}...")
# Ensure initramfs exists. If not, generate it.
initrd_path_rel = f"boot/initramfs-{kver}.img"
initrd_full_path = os.path.join(mount_root, initrd_path_rel)
if not os.path.exists(initrd_full_path):
logger.info(f"Generating initramfs for {kver}...")
run_command(["chroot", mount_root, "dracut", "--force", f"/boot/initramfs-{kver}.img", kver])
kernel_image = f"/lib/modules/{kver}/vmlinuz" # Add kernel entries
initrd_arg = f"/boot/initramfs-{kver}.img" modules_dir = os.path.join(mount_root, "lib/modules")
if os.path.exists(modules_dir):
# kernel-install add <version> <image> [initrd] kvers = [d for d in os.listdir(modules_dir) if os.path.isdir(os.path.join(modules_dir, d))]
cmd = ["chroot", mount_root, "kernel-install", "add", kver, kernel_image] for kver in kvers:
if os.path.exists(os.path.join(mount_root, initrd_arg.lstrip("/"))): logger.info(f"Adding kernel entry for {kver}...")
cmd.append(initrd_arg) initrd_path = f"/boot/initramfs-{kver}.img"
if not os.path.exists(os.path.join(mount_root, initrd_path.lstrip("/"))):
run_command(cmd) run_command(["chroot", mount_root, "dracut", "--force", initrd_path, kver])
kernel_image = f"/lib/modules/{kver}/vmlinuz"
run_command(["chroot", mount_root, "kernel-install", "add", kver, kernel_image, initrd_path])
else:
logger.info("Configuring GRUB2 (BIOS)...")
# Ensure /etc/default/grub exists
grub_default = os.path.join(mount_root, "etc/default/grub")
if not os.path.exists(grub_default):
with open(grub_default, "w") as f:
f.write('GRUB_TIMEOUT=5\nGRUB_DISTRIBUTOR="$(sed \'s, release .*$,,g\' /etc/system-release)"\nGRUB_DEFAULT=saved\nGRUB_DISABLE_SUBMENU=true\nGRUB_TERMINAL_OUTPUT="console"\nGRUB_CMDLINE_LINUX="rhgb quiet"\nGRUB_DISABLE_RECOVERY="true"\nGRUB_ENABLE_BLSCFG=true\n')
if not disk_device:
disk_device = partition_info["root"].rstrip("0123456789")
if disk_device.endswith("p"): disk_device = disk_device[:-1]
logger.info(f"Installing GRUB to {disk_device} (BIOS)")
run_command(["chroot", mount_root, "grub2-install", "--target=i386-pc", disk_device])
run_command(["chroot", mount_root, "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"])
run_command(["sync"])
logger.info("System configuration complete.") logger.info("System configuration complete.")
log_os_install("CONFIGURE", "complete", "systemd-boot and user configured successfully") log_os_install("CONFIGURE", "complete", "Bootloader and user configured successfully")

View File

@@ -64,7 +64,7 @@ def main():
"password": "password", # Insecure default for CLI dev testing "password": "password", # Insecure default for CLI dev testing
"hostname": "iridium-cli" "hostname": "iridium-cli"
} }
configure_system(mount_root, parts, user_info) configure_system(mount_root, parts, user_info, disk_device=target)
logger.info("INSTALLER_BOOTLOADER_COMPLETE: Bootloader configured") logger.info("INSTALLER_BOOTLOADER_COMPLETE: Bootloader configured")
print("Installation complete! You can now reboot.") print("Installation complete! You can now reboot.")

View File

@@ -54,8 +54,13 @@ def get_total_memory() -> int:
return 0 return 0
def is_uefi():
return os.path.exists("/sys/firmware/efi")
def calculate_auto_partitions(disk_device): def calculate_auto_partitions(disk_device):
disk_size = 0 disk_size = 0
uefi = is_uefi()
try: try:
# Get disk size in bytes # Get disk size in bytes
result = subprocess.run( result = subprocess.run(
@@ -83,16 +88,21 @@ def calculate_auto_partitions(disk_device):
disk_mb = disk_size / (1024 * 1024) disk_mb = disk_size / (1024 * 1024)
# Defaults # Defaults
efi_size = 2 * 1024 * 1024 * 1024 efi_size = 1024 * 1024 * 1024 if uefi else 0
swap_size = ram_size + (2 * 1024 * 1024 * 1024) swap_size = ram_size + (2 * 1024 * 1024 * 1024)
min_root_size = 10 * 1024 * 1024 * 1024 # 10GB min_root_size = 10 * 1024 * 1024 * 1024 # 10GB
# Cap swap for auto-calc if disk is small
if disk_size < 100 * 1024**3:
swap_size = min(swap_size, 4 * 1024**3)
total_required = efi_size + swap_size + min_root_size total_required = efi_size + swap_size + min_root_size
use_swap = True use_swap = True
if disk_size < total_required: if disk_size < total_required:
efi_size = 1 * 1024 * 1024 * 1024 if uefi:
efi_size = 512 * 1024 * 1024
use_swap = False use_swap = False
swap_size = 0 swap_size = 0
if disk_size < (efi_size + min_root_size): if disk_size < (efi_size + min_root_size):
@@ -101,26 +111,28 @@ def calculate_auto_partitions(disk_device):
root_size = disk_size - efi_size - swap_size root_size = disk_size - efi_size - swap_size
partitions = [ partitions = []
{
if uefi:
partitions.append({
"type": "partition", "type": "partition",
"name": "EFI System", "name": "EFI System",
"filesystem": "vfat", "filesystem": "vfat",
"mount_point": "/boot", "mount_point": "/boot/efi",
"size": f"{efi_size / (1024**3):.1f} GB", "size": f"{efi_size / (1024**3):.1f} GB",
"bytes": efi_size, "bytes": efi_size,
"style_class": "part-efi", "style_class": "part-efi",
}, })
{
"type": "partition", partitions.append({
"name": "Root", "type": "partition",
"filesystem": "ext4", "name": "Root",
"mount_point": "/", "filesystem": "ext4",
"size": f"{root_size / (1024**3):.1f} GB", "mount_point": "/",
"bytes": root_size, "size": f"{root_size / (1024**3):.1f} GB",
"style_class": "part-root", "bytes": root_size,
}, "style_class": "part-root",
] })
if use_swap: if use_swap:
partitions.append( partitions.append(

View File

@@ -230,6 +230,12 @@ class InstallerWindow(Adw.ApplicationWindow):
if self.log_handler: if self.log_handler:
logging.getLogger().removeHandler(self.log_handler) logging.getLogger().removeHandler(self.log_handler)
self.log_handler = None self.log_handler = None
# Release wake lock if held
if hasattr(self, "inhibit_cookie") and self.inhibit_cookie:
app = self.get_application()
app.uninhibit(self.inhibit_cookie)
self.inhibit_cookie = None
finish_page = Adw.StatusPage() finish_page = Adw.StatusPage()
finish_page.set_title(message) finish_page.set_title(message)
@@ -247,7 +253,8 @@ class InstallerWindow(Adw.ApplicationWindow):
def on_restart(b): def on_restart(b):
import subprocess import subprocess
try: try:
subprocess.run(["systemctl", "reboot"]) # Use sudo since we might be running as liveuser
subprocess.run(["sudo", "systemctl", "reboot"])
except Exception as e: except Exception as e:
print(f"Failed to reboot: {e}") print(f"Failed to reboot: {e}")
self.close() self.close()
@@ -321,7 +328,7 @@ class InstallerWindow(Adw.ApplicationWindow):
f"Session: {self.session_id} - Configuring bootloader and user", f"Session: {self.session_id} - Configuring bootloader and user",
"installer", "installer",
) )
configure_system(mount_root, parts, user_info) configure_system(mount_root, parts, user_info, disk_device=disk)
nlog( nlog(
"INSTALL_PROGRESS", "INSTALL_PROGRESS",
f"Session: {self.session_id} - Configuration complete", f"Session: {self.session_id} - Configuration complete",
@@ -464,6 +471,14 @@ class InstallerWindow(Adw.ApplicationWindow):
# Show success in UI even in mock # Show success in UI even in mock
self.show_finish_page("Mock Installation Complete!") self.show_finish_page("Mock Installation Complete!")
else: else:
# Inhibit logout/suspend/idle
app = self.get_application()
self.inhibit_cookie = app.inhibit(
self,
Gtk.ApplicationInhibitFlags.LOGOUT | Gtk.ApplicationInhibitFlags.SUSPEND | Gtk.ApplicationInhibitFlags.IDLE,
"Installing Operating System"
)
self.show_progress_page("Installing Iridium OS...") self.show_progress_page("Installing Iridium OS...")
thread = threading.Thread( thread = threading.Thread(
target=self.run_installation, target=self.run_installation,

21
run.sh
View File

@@ -27,6 +27,27 @@ if command -v gtk-update-icon-cache &> /dev/null; then
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
fi fi
# Check and install dependencies (Fedora/DNF)
if command -v dnf &> /dev/null; then
DEPENDENCIES="python3-gobject gtk4 libadwaita python3-requests gdisk dosfstools e2fsprogs parted grub2-tools openssl"
MISSING_DEPS=""
for dep in $DEPENDENCIES; do
if ! rpm -q $dep &> /dev/null; then
MISSING_DEPS="$MISSING_DEPS $dep"
fi
done
if [ -n "$MISSING_DEPS" ]; then
echo "Missing dependencies found:$MISSING_DEPS"
echo "Installing..."
sudo dnf install -y $MISSING_DEPS
fi
else
echo "Warning: 'dnf' not found. Automatic dependency installation skipped."
echo "Please ensure you have: python3-gobject gtk4 libadwaita python3-requests gdisk dosfstools e2fsprogs"
fi
export GSETTINGS_SCHEMA_DIR=. export GSETTINGS_SCHEMA_DIR=.
python3 -m iridium_installer.main "$@" python3 -m iridium_installer.main "$@"

28
run_vm_bios.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Configuration
ISO_PATH="${1:-/home/n0va/.local/share/iridium-installer-vm/Fedora.iso}"
DISK_PATH="${2:-/home/n0va/.local/share/iridium-installer-vm/test-disk.qcow2}"
echo "Starting Iridium VM with BIOS (Legacy) support..."
echo "ISO: $ISO_PATH"
echo "Disk: $DISK_PATH"
# QEMU Command with BIOS
qemu-system-x86_64 \
-enable-kvm \
-m 8G \
-smp 2 \
-cpu host \
-drive file="$DISK_PATH",format=qcow2,if=virtio \
-device virtio-scsi-pci,id=scsi0 \
-drive file="$ISO_PATH",format=raw,if=none,id=cdrom,readonly=on \
-device scsi-cd,bus=scsi0.0,drive=cdrom \
-boot order=d \
-netdev user,id=net0 \
-device virtio-net-pci,netdev=net0 \
-vga virtio \
-display gtk,gl=on \
-monitor unix:$HOME/.local/share/iridium-installer-vm/monitor.sock,server,nowait \
-name "Iridium Installer Test VM (BIOS)" \
-loadvm clean-state

View File

@@ -1,18 +1,21 @@
#!/bin/bash #!/bin/bash
# Configuration # Configuration
ISO_PATH="${1:-/home/n0va/Downloads/Fedora-Workstation-Live-43-1.6.x86_64.iso}" ISO_PATH="${1:-/home/n0va/.local/share/iridium-installer-vm/Fedora.iso}"
DISK_PATH="${2:-/home/n0va/.local/share/iridium-installer-vm/test-disk.qcow2}" DISK_PATH="${2:-/home/n0va/.local/share/iridium-installer-vm/test-disk.qcow2}"
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd" OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
OVMF_VARS_LOCAL="/tmp/iridium_vm_vars.fd" OVMF_VARS_LOCAL="/tmp/iridium_vm_vars.qcow2"
# Ensure OVMF vars exist # Ensure OVMF vars exist in qcow2 format for snapshot support
if [ ! -f "$OVMF_VARS_LOCAL" ]; then if [ ! -f "$OVMF_VARS_LOCAL" ]; then
if [ -f "$OVMF_VARS_TEMPLATE" ]; then if [ -f "$OVMF_VARS_TEMPLATE" ]; then
cp "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_LOCAL" echo "Creating UEFI vars file from template..."
qemu-img convert -f raw -O qcow2 "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_LOCAL"
else else
echo "Warning: OVMF VARS template not found at $OVMF_VARS_TEMPLATE" echo "Warning: OVMF VARS template not found at $OVMF_VARS_TEMPLATE"
# Fallback to creating an empty qcow2 if template is missing (not ideal but avoids crash)
qemu-img create -f qcow2 "$OVMF_VARS_LOCAL" 528K
fi fi
fi fi
@@ -21,13 +24,14 @@ echo "ISO: $ISO_PATH"
echo "Disk: $DISK_PATH" echo "Disk: $DISK_PATH"
# QEMU Command with UEFI (OVMF) enabled # QEMU Command with UEFI (OVMF) enabled
# We use format=qcow2 for pflash1 to allow snapshots (savevm)
qemu-system-x86_64 \ qemu-system-x86_64 \
-enable-kvm \ -enable-kvm \
-m 4G \ -m 8G \
-smp 2 \ -smp 2 \
-cpu host \ -cpu host \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \ -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$OVMF_VARS_LOCAL" \ -drive if=pflash,format=qcow2,file="$OVMF_VARS_LOCAL" \
-drive file="$DISK_PATH",format=qcow2,if=virtio \ -drive file="$DISK_PATH",format=qcow2,if=virtio \
-cdrom "$ISO_PATH" \ -cdrom "$ISO_PATH" \
-boot once=d \ -boot once=d \
@@ -37,4 +41,4 @@ qemu-system-x86_64 \
-display gtk,gl=on \ -display gtk,gl=on \
-monitor unix:$HOME/.local/share/iridium-installer-vm/monitor.sock,server,nowait \ -monitor unix:$HOME/.local/share/iridium-installer-vm/monitor.sock,server,nowait \
-name "Iridium Installer Test VM (UEFI)" \ -name "Iridium Installer Test VM (UEFI)" \
# -loadvm clean-state -loadvm clean-state