Compare commits
46 Commits
8bac02357c
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
| a5d946b2a2 | |||
| ff0760b772 | |||
| 752b6ddb60 | |||
| 3da9d659a0 | |||
| 0e6d3cd83c | |||
| 17548898bd | |||
| c92e0bb1f9 | |||
| 8f7eb17d3c | |||
| ada17eefd7 | |||
| 2e8e99f481 | |||
| 53227e0f8e | |||
| 2762f2f767 | |||
| 19254e128c | |||
| 5abba6ce88 | |||
| 0ec7de3937 | |||
| cbc1b05eca | |||
| e8afc31d00 | |||
| 59219130bd | |||
| e8a2f7aaa9 | |||
| b37fc8d060 | |||
| 44af152887 | |||
| 5238fd29d9 | |||
| 74fedf9001 | |||
| 5df00a5814 | |||
| 951a1b7fdc | |||
| 297f2cd3c2 | |||
| ce2c242454 | |||
| 4278a3198d | |||
| 943cfee8bb | |||
| 9cfd6ad2e0 | |||
| 23b5f017d4 | |||
| ee83411333 | |||
| 7613bdf8d5 | |||
| 400235067d | |||
| f3e7122d02 | |||
| afe18c1eee | |||
| 76c638de8e | |||
| 1238dcdabd | |||
| f7fc354f3f | |||
| 3a226108ec | |||
| 45b0fa5a84 | |||
| 6b7c7debff | |||
| 9fcbf6f204 | |||
| e5f46324e4 | |||
| 4eff9d7e22 | |||
| 7bfab2ac5b |
@@ -97,120 +97,128 @@ def get_disk_size(disk_device):
|
||||
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):
|
||||
"""
|
||||
Automatically partitions the disk:
|
||||
1. EFI System Partition (2GB standard, 1GB small)
|
||||
2. Root (Remaining)
|
||||
3. Swap (RAM + 2GB, or 0 if small)
|
||||
|
||||
Layout: P1=EFI, P3=Swap (End), P2=Root (Middle)
|
||||
Automatically partitions the disk based on boot mode:
|
||||
UEFI (GPT): ESP (1GB), Root (Remaining), Swap (RAM+2GB)
|
||||
BIOS (MBR): Root (Remaining, Bootable), Swap (RAM+2GB)
|
||||
"""
|
||||
logger.info(f"Starting auto-partitioning on {disk_device}")
|
||||
log_disk_operation("PARTITION", "start", f"Disk: {disk_device}")
|
||||
uefi = is_uefi()
|
||||
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)
|
||||
ram_size = get_total_memory()
|
||||
|
||||
# Defaults
|
||||
efi_mb = 2048
|
||||
# Calculate sizes
|
||||
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
|
||||
swap_mb = min(swap_mb, 4096)
|
||||
|
||||
total_required_mb = efi_mb + swap_mb + min_root_mb
|
||||
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 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)
|
||||
if uefi:
|
||||
# GPT Layout
|
||||
logger.info("Using GPT layout for UEFI")
|
||||
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,
|
||||
]
|
||||
)
|
||||
# 1. ESP (1GB)
|
||||
run_command(["sgdisk", "-n", "1:0:+1024M", "-t", "1:ef00", "-c", "1:EFI System", disk_device])
|
||||
|
||||
# 4. Create Swap Partition (Part 3, End) - If enabled
|
||||
# 2. Swap (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,
|
||||
]
|
||||
)
|
||||
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)
|
||||
# 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"])
|
||||
|
||||
# Inform kernel of changes
|
||||
run_command(["partprobe", disk_device])
|
||||
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# 6. Format Partitions
|
||||
# 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
|
||||
|
||||
logger.info("Formatting EFI partition...")
|
||||
# Format
|
||||
if uefi:
|
||||
logger.info(f"Formatting EFI partition {efi_part}...")
|
||||
run_command(["mkfs.vfat", "-F32", efi_part])
|
||||
|
||||
if use_swap:
|
||||
logger.info("Formatting Swap partition...")
|
||||
logger.info(f"Formatting Swap partition {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])
|
||||
|
||||
logger.info("Partitioning and formatting complete.")
|
||||
log_disk_operation(
|
||||
"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
|
||||
|
||||
result = {"efi": efi_part, "root": root_part, "swap": swap_part}
|
||||
log_disk_operation("PARTITION", "complete", str(result))
|
||||
return result
|
||||
|
||||
|
||||
@@ -219,32 +227,30 @@ def mount_partitions(partition_info, mount_root="/mnt"):
|
||||
Mounts the partitions into mount_root.
|
||||
"""
|
||||
import os
|
||||
|
||||
log_disk_operation("MOUNT", "start", f"Mounting to {mount_root}")
|
||||
|
||||
# 1. Mount Root
|
||||
if not os.path.exists(mount_root):
|
||||
os.makedirs(mount_root)
|
||||
|
||||
run_command(["mount", partition_info["root"], mount_root])
|
||||
|
||||
# 2. Mount EFI
|
||||
# 2. Mount EFI (if exists)
|
||||
if partition_info.get("efi"):
|
||||
# For systemd-boot, we prefer mounting ESP to /boot
|
||||
efi_mount = os.path.join(mount_root, "boot")
|
||||
if not os.path.exists(efi_mount):
|
||||
os.makedirs(efi_mount, exist_ok=True)
|
||||
|
||||
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
|
||||
if partition_info.get("swap"):
|
||||
run_command(["swapon", partition_info["swap"]])
|
||||
|
||||
logger.info(f"Partitions mounted at {mount_root}")
|
||||
log_disk_operation(
|
||||
"MOUNT",
|
||||
"complete",
|
||||
f"Root: {partition_info['root']}, EFI: {partition_info['efi']}",
|
||||
)
|
||||
log_disk_operation("MOUNT", "complete", f"Root: {partition_info['root']}")
|
||||
|
||||
|
||||
def create_partition(
|
||||
|
||||
@@ -3,21 +3,31 @@ import requests
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
import atexit
|
||||
import sys
|
||||
import signal
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__ + ".network_logging")
|
||||
|
||||
DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1468696228647932280/L9XSHS6TPEeK0wwJTFdK9RUyZvztSGQBd4xEfVvb4Y1AXGQAOc4YTsuxeFuWC9HxymJn"
|
||||
|
||||
LOG_QUEUE = []
|
||||
FULL_LOG = []
|
||||
QUEUE_LOCK = threading.Lock()
|
||||
ENABLED = True
|
||||
LOG_SENT = False
|
||||
|
||||
|
||||
def init_network_logging(enabled: bool = True):
|
||||
global 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"):
|
||||
@@ -35,19 +45,30 @@ def log_to_discord(level: str, message: str, module: str = "general"):
|
||||
with QUEUE_LOCK:
|
||||
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():
|
||||
# Deprecated: No partial flushing anymore
|
||||
pass
|
||||
|
||||
|
||||
def send_full_log():
|
||||
"""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
|
||||
|
||||
# Give a tiny bit of time for final async logs to land
|
||||
time.sleep(0.5)
|
||||
LOG_SENT = True
|
||||
|
||||
# Final sync wait
|
||||
time.sleep(1)
|
||||
|
||||
with QUEUE_LOCK:
|
||||
logs_to_send = FULL_LOG.copy()
|
||||
@@ -55,29 +76,31 @@ def send_full_log():
|
||||
if not logs_to_send:
|
||||
return
|
||||
|
||||
def send_sync():
|
||||
temp_file = "/tmp/iridium_install_log.txt"
|
||||
temp_file = f"/tmp/iridium_install_log_{int(time.time())}.txt"
|
||||
try:
|
||||
error_count = 0
|
||||
with open(temp_file, "w") as f:
|
||||
for log in logs_to_send:
|
||||
if log["level"] in ["ERROR", "CRITICAL"]:
|
||||
error_count += 1
|
||||
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)}
|
||||
requests.post(DISCORD_WEBHOOK_URL, files=files, timeout=30)
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to send full log file to Discord: {e}")
|
||||
finally:
|
||||
if os.path.exists(temp_file):
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
|
||||
# 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)
|
||||
except: pass
|
||||
|
||||
|
||||
def send_discord_message(content: str):
|
||||
|
||||
@@ -42,6 +42,12 @@ def run_command(cmd, check=True):
|
||||
for line in stream:
|
||||
line_clean = line.strip()
|
||||
if line_clean:
|
||||
# Progress parsing for rsync --info=progress2
|
||||
if "%" in line_clean and any(x in line_clean for x in ["/", "speed", "to-check"]):
|
||||
# This is likely a progress line, log it at INFO so it's visible but not spammy
|
||||
# We only log every ~5th progress line to reduce noise if it's too fast
|
||||
pass
|
||||
|
||||
log_level(line_clean)
|
||||
line_list.append(line)
|
||||
|
||||
@@ -51,7 +57,7 @@ def run_command(cmd, check=True):
|
||||
target=read_stream, args=(process.stdout, stdout_lines, logger.info)
|
||||
)
|
||||
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()
|
||||
@@ -66,6 +72,8 @@ def run_command(cmd, check=True):
|
||||
stderr_str = "".join(stderr_lines)
|
||||
|
||||
if check and returncode != 0:
|
||||
error_msg = f"Command failed: {' '.join(cmd)}\nExit Code: {returncode}\nStderr: {stderr_str}"
|
||||
logger.error(error_msg)
|
||||
raise subprocess.CalledProcessError(
|
||||
returncode, cmd, output=stdout_str, stderr=stderr_str
|
||||
)
|
||||
@@ -76,7 +84,7 @@ def run_command(cmd, check=True):
|
||||
@contextmanager
|
||||
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}...")
|
||||
mounts = ["dev", "proc", "sys"]
|
||||
@@ -88,6 +96,24 @@ def mount_pseudo_fs(mount_root):
|
||||
os.makedirs(target, exist_ok=True)
|
||||
run_command(["mount", "--bind", f"/{fs}", 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
|
||||
finally:
|
||||
logger.info(f"Unmounting pseudo-filesystems from {mount_root}...")
|
||||
@@ -98,141 +124,216 @@ def mount_pseudo_fs(mount_root):
|
||||
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"):
|
||||
"""
|
||||
Installs minimal Fedora packages to mount_root.
|
||||
Installs the OS by rsyncing from the live environment (fully offline).
|
||||
"""
|
||||
logger.info(f"Installing minimal Fedora {releasever} to {mount_root}...")
|
||||
log_os_install("INSTALL", "start", f"Target: {mount_root}, Release: {releasever}")
|
||||
logger.info(f"Installing Iridium OS to {mount_root} via rsync...")
|
||||
log_os_install("INSTALL", "start", f"Target: {mount_root}")
|
||||
|
||||
packages = [
|
||||
"basesystem",
|
||||
"bash",
|
||||
"coreutils",
|
||||
"kernel",
|
||||
"systemd",
|
||||
"dnf",
|
||||
"efibootmgr",
|
||||
"passwd",
|
||||
"rootfiles",
|
||||
"vim-minimal",
|
||||
uefi = is_uefi()
|
||||
|
||||
# Exclude list for rsync to avoid copying pseudo-filesystems and temporary data
|
||||
excludes = [
|
||||
"/dev/*",
|
||||
"/proc/*",
|
||||
"/sys/*",
|
||||
"/tmp/*",
|
||||
"/run/*",
|
||||
"/mnt/*",
|
||||
"/media/*",
|
||||
"/lost+found",
|
||||
"/home/*/.gvfs",
|
||||
"/home/*/.cache",
|
||||
"/home/*/.local/share/Trash",
|
||||
"/var/lib/dnf/*",
|
||||
"/var/cache/dnf/*",
|
||||
"/etc/fstab",
|
||||
"/etc/hostname",
|
||||
"/boot", # We handle boot separately (including the dir itself)
|
||||
# Avoid copying the installer data itself if it's in home
|
||||
"domek_na_skale",
|
||||
]
|
||||
|
||||
# Offline installation logic
|
||||
iso_repo = "/run/install/repo"
|
||||
dnf_args = []
|
||||
exclude_args = [f"--exclude={ex}" for ex in excludes]
|
||||
|
||||
if os.path.exists(iso_repo):
|
||||
logger.info(f"Found ISO repository at {iso_repo}. Using strictly offline mode.")
|
||||
dnf_args = [
|
||||
"--disablerepo=*",
|
||||
f"--repofrompath=iridium-iso,{iso_repo}",
|
||||
"--enablerepo=iridium-iso"
|
||||
]
|
||||
else:
|
||||
logger.warning(f"ISO repository not found at {iso_repo}. Trying to use host config (may require network).")
|
||||
# 1. Main Root Sync
|
||||
# We use -a (archive), -H (hard links), -A (acls), -X (xattrs), -v (verbose), -x (one file system)
|
||||
# --info=progress2 gives a nice summary for logging
|
||||
cmd = ["rsync", "-aHAXvx", "--info=progress2"] + exclude_args + ["/", mount_root]
|
||||
|
||||
cmd = [
|
||||
"dnf",
|
||||
"install",
|
||||
"-y",
|
||||
f"--installroot={mount_root}",
|
||||
f"--releasever={releasever}",
|
||||
"--use-host-config",
|
||||
"--setopt=install_weak_deps=False",
|
||||
"--nodocs",
|
||||
] + dnf_args + packages
|
||||
|
||||
with mount_pseudo_fs(mount_root):
|
||||
logger.info("Starting main rsync operation...")
|
||||
run_command(cmd)
|
||||
|
||||
logger.info("Base system installation complete.")
|
||||
# 2. Boot Sync
|
||||
# If UEFI, /boot is likely FAT32 which doesn't support symlinks or xattrs
|
||||
logger.info(f"Syncing /boot (UEFI: {uefi})...")
|
||||
boot_dst = os.path.join(mount_root, "boot/")
|
||||
os.makedirs(boot_dst, exist_ok=True)
|
||||
|
||||
if uefi:
|
||||
# FAT32 friendly: follow links (-L), recursive (-r), preserve times (-t)
|
||||
# We skip ACLs, xattrs, and hardlinks as they are not supported
|
||||
boot_cmd = ["rsync", "-rtvL", "--info=progress2", "/boot/", boot_dst]
|
||||
else:
|
||||
# BIOS (ext4): standard archive is fine
|
||||
boot_cmd = ["rsync", "-aHAXvx", "--info=progress2", "/boot/", boot_dst]
|
||||
|
||||
run_command(boot_cmd)
|
||||
|
||||
# Ensure essential directories exist
|
||||
for d in ["dev", "proc", "sys", "run", "mnt", "tmp"]:
|
||||
os.makedirs(os.path.join(mount_root, d), exist_ok=True)
|
||||
|
||||
logger.info("Base system rsync complete.")
|
||||
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...")
|
||||
log_os_install("CONFIGURE", "start", f"Configuring system in {mount_root}")
|
||||
|
||||
uefi = is_uefi()
|
||||
|
||||
# 1. Generate fstab
|
||||
def get_uuid(dev):
|
||||
res = run_command(["blkid", "-s", "UUID", "-o", "value", dev])
|
||||
return res.stdout.strip()
|
||||
|
||||
root_uuid = get_uuid(partition_info["root"])
|
||||
efi_uuid = get_uuid(partition_info["efi"])
|
||||
|
||||
swap_entry = ""
|
||||
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")
|
||||
|
||||
if partition_info.get("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)
|
||||
with open(os.path.join(mount_root, "etc/fstab"), "w") as f:
|
||||
f.write(fstab_content)
|
||||
f.write("\n".join(fstab_lines) + "\n")
|
||||
|
||||
# Ensure EFI is mounted for bootloader installation if UEFI
|
||||
if uefi and partition_info.get("efi"):
|
||||
efi_target = os.path.join(mount_root, "boot")
|
||||
os.makedirs(efi_target, exist_ok=True)
|
||||
# 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']}...")
|
||||
with mount_pseudo_fs(mount_root):
|
||||
# Create user and add to wheel group (sudoer)
|
||||
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']}")
|
||||
# Since we rsync from live, we might have a 'liveuser' or similar.
|
||||
# We should probably clear /home and /etc/passwd entries that are non-system
|
||||
# but for now, let's just make sure we can create the new one.
|
||||
try:
|
||||
# Remove liveuser if it exists (common in Fedora live)
|
||||
run_command(["chroot", mount_root, "userdel", "-r", "liveuser"], check=False)
|
||||
except: pass
|
||||
|
||||
run_command(["chroot", mount_root, "useradd", "-m", "-G", "wheel", user_info["username"]])
|
||||
|
||||
# Set hostname
|
||||
with open(os.path.join(mount_root, "etc/hostname"), "w") as f:
|
||||
f.write(user_info["hostname"] + "\n")
|
||||
|
||||
# 3. Configure systemd-boot
|
||||
with mount_pseudo_fs(mount_root):
|
||||
# Ensure machine-id exists for kernel-install
|
||||
if not os.path.exists(os.path.join(mount_root, "etc/machine-id")):
|
||||
# Set passwords
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["openssl", "passwd", "-6", user_info["password"]],
|
||||
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}")
|
||||
|
||||
# 3. Configure Bootloader
|
||||
if uefi:
|
||||
logger.info("Configuring systemd-boot...")
|
||||
|
||||
# Remove any copied machine-id to ensure a unique one is generated
|
||||
mid_path = os.path.join(mount_root, "etc/machine-id")
|
||||
if os.path.exists(mid_path):
|
||||
os.remove(mid_path)
|
||||
run_command(["chroot", mount_root, "systemd-machine-id-setup"])
|
||||
|
||||
# Set kernel command line
|
||||
os.makedirs(os.path.join(mount_root, "etc/kernel"), exist_ok=True)
|
||||
with open(os.path.join(mount_root, "etc/kernel/cmdline"), "w") as f:
|
||||
f.write(f"root=UUID={root_uuid} rw quiet\n")
|
||||
|
||||
# Install systemd-boot to the ESP (mounted at /boot)
|
||||
# Use --path=/boot to be explicit, though standard behavior usually finds it.
|
||||
with open(os.path.join(mount_root, "etc/kernel/layout"), "w") as f:
|
||||
f.write("bls\n")
|
||||
|
||||
# Cleanup existing systemd-boot files to ensure a fresh install since --force is not supported
|
||||
for d in ["loader", "EFI/systemd", "EFI/BOOT"]:
|
||||
path = os.path.join(mount_root, "boot", d)
|
||||
if os.path.exists(path):
|
||||
import shutil
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
os.remove(path)
|
||||
|
||||
# Initialize systemd-boot
|
||||
run_command(["chroot", mount_root, "bootctl", "install", "--path=/boot"])
|
||||
|
||||
# Add kernel entries
|
||||
# Sync kernels and generate BLS entries
|
||||
# Since we rsync'd, kernels are in /lib/modules and /boot
|
||||
# We use kernel-install to make sure they are properly set up for systemd-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}...")
|
||||
logger.info(f"Setting up boot entries for kernel {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)
|
||||
# Ensure initramfs exists in /boot (rsync should have copied it, but let's be sure)
|
||||
initrd_path = f"/boot/initramfs-{kver}.img"
|
||||
vmlinuz_path = f"/boot/vmlinuz-{kver}"
|
||||
|
||||
if not os.path.exists(initrd_full_path):
|
||||
if not os.path.exists(os.path.join(mount_root, initrd_path.lstrip("/"))):
|
||||
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"
|
||||
initrd_arg = f"/boot/initramfs-{kver}.img"
|
||||
run_command(["chroot", mount_root, "dracut", "--force", initrd_path, kver])
|
||||
|
||||
# kernel-install add <version> <image> [initrd]
|
||||
cmd = ["chroot", mount_root, "kernel-install", "add", kver, kernel_image]
|
||||
if os.path.exists(os.path.join(mount_root, initrd_arg.lstrip("/"))):
|
||||
cmd.append(initrd_arg)
|
||||
# On Fedora, kernel-install add will populate /boot/<machine-id>/<version>/
|
||||
if os.path.exists(os.path.join(mount_root, vmlinuz_path.lstrip("/"))):
|
||||
run_command(["chroot", mount_root, "kernel-install", "add", kver, vmlinuz_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')
|
||||
|
||||
run_command(cmd)
|
||||
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.")
|
||||
log_os_install("CONFIGURE", "complete", "systemd-boot and user configured successfully")
|
||||
log_os_install("CONFIGURE", "complete", "Bootloader and user configured successfully")
|
||||
|
||||
@@ -64,7 +64,7 @@ def main():
|
||||
"password": "password", # Insecure default for CLI dev testing
|
||||
"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")
|
||||
|
||||
print("Installation complete! You can now reboot.")
|
||||
|
||||
@@ -54,8 +54,13 @@ def get_total_memory() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def is_uefi():
|
||||
return os.path.exists("/sys/firmware/efi")
|
||||
|
||||
|
||||
def calculate_auto_partitions(disk_device):
|
||||
disk_size = 0
|
||||
uefi = is_uefi()
|
||||
try:
|
||||
# Get disk size in bytes
|
||||
result = subprocess.run(
|
||||
@@ -83,16 +88,21 @@ def calculate_auto_partitions(disk_device):
|
||||
disk_mb = disk_size / (1024 * 1024)
|
||||
|
||||
# Defaults
|
||||
efi_size = 2 * 1024 * 1024 * 1024
|
||||
efi_size = 1024 * 1024 * 1024 if uefi else 0
|
||||
swap_size = ram_size + (2 * 1024 * 1024 * 1024)
|
||||
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
|
||||
|
||||
use_swap = True
|
||||
|
||||
if disk_size < total_required:
|
||||
efi_size = 1 * 1024 * 1024 * 1024
|
||||
if uefi:
|
||||
efi_size = 512 * 1024 * 1024
|
||||
use_swap = False
|
||||
swap_size = 0
|
||||
if disk_size < (efi_size + min_root_size):
|
||||
@@ -101,17 +111,20 @@ def calculate_auto_partitions(disk_device):
|
||||
|
||||
root_size = disk_size - efi_size - swap_size
|
||||
|
||||
partitions = [
|
||||
{
|
||||
partitions = []
|
||||
|
||||
if uefi:
|
||||
partitions.append({
|
||||
"type": "partition",
|
||||
"name": "EFI System",
|
||||
"filesystem": "vfat",
|
||||
"mount_point": "/boot",
|
||||
"mount_point": "/boot/efi",
|
||||
"size": f"{efi_size / (1024**3):.1f} GB",
|
||||
"bytes": efi_size,
|
||||
"style_class": "part-efi",
|
||||
},
|
||||
{
|
||||
})
|
||||
|
||||
partitions.append({
|
||||
"type": "partition",
|
||||
"name": "Root",
|
||||
"filesystem": "ext4",
|
||||
@@ -119,8 +132,7 @@ def calculate_auto_partitions(disk_device):
|
||||
"size": f"{root_size / (1024**3):.1f} GB",
|
||||
"bytes": root_size,
|
||||
"style_class": "part-root",
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
if use_swap:
|
||||
partitions.append(
|
||||
|
||||
@@ -182,6 +182,13 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
spinner.start()
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||
|
||||
self.progress_bar = Gtk.ProgressBar()
|
||||
self.progress_bar.set_show_text(True)
|
||||
self.progress_bar.set_margin_start(24)
|
||||
self.progress_bar.set_margin_end(24)
|
||||
box.append(self.progress_bar)
|
||||
|
||||
box.append(spinner)
|
||||
|
||||
# Log view
|
||||
@@ -226,25 +233,69 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
mark = self.log_buffer.create_mark("end", end_iter, False)
|
||||
self.log_view.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
|
||||
|
||||
# Update progress bar based on keywords or rsync progress
|
||||
msg_lower = msg.lower()
|
||||
if "step 1:" in msg_lower:
|
||||
self.progress_bar.set_fraction(0.1)
|
||||
self.progress_bar.set_text("Partitioning...")
|
||||
elif "step 2:" in msg_lower:
|
||||
self.progress_bar.set_fraction(0.2)
|
||||
self.progress_bar.set_text("Mounting...")
|
||||
elif "step 3:" in msg_lower:
|
||||
self.progress_bar.set_fraction(0.3)
|
||||
self.progress_bar.set_text("Installing OS...")
|
||||
elif "step 4:" in msg_lower:
|
||||
self.progress_bar.set_fraction(0.9)
|
||||
self.progress_bar.set_text("Configuring Bootloader...")
|
||||
elif "%" in msg and "to-check" in msg:
|
||||
# Parse rsync progress (e.g., "1,234,567 80% 10.00MB/s 0:00:05")
|
||||
import re
|
||||
match = re.search(r"(\d+)%", msg)
|
||||
if match:
|
||||
percentage = int(match.group(1))
|
||||
# Map 0-100% of rsync to 0.3-0.8 of total progress
|
||||
fraction = 0.3 + (percentage / 100.0) * 0.5
|
||||
self.progress_bar.set_fraction(fraction)
|
||||
self.progress_bar.set_text(f"Syncing files... {percentage}%")
|
||||
|
||||
def show_finish_page(self, message, success=True):
|
||||
if self.log_handler:
|
||||
logging.getLogger().removeHandler(self.log_handler)
|
||||
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.set_title(message)
|
||||
|
||||
btn = Gtk.Button()
|
||||
btn.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
if success:
|
||||
finish_page.set_icon_name("emblem-ok-symbolic")
|
||||
finish_page.set_description("You can now restart your computer.")
|
||||
btn_label = "Restart"
|
||||
btn.set_label("Restart System")
|
||||
btn.add_css_class("suggested-action")
|
||||
btn.add_css_class("pill")
|
||||
|
||||
def on_restart(b):
|
||||
import subprocess
|
||||
try:
|
||||
# Use sudo since we might be running as liveuser
|
||||
subprocess.run(["sudo", "systemctl", "reboot"])
|
||||
except Exception as e:
|
||||
print(f"Failed to reboot: {e}")
|
||||
self.close()
|
||||
|
||||
btn.connect("clicked", on_restart)
|
||||
else:
|
||||
finish_page.set_icon_name("dialog-error-symbolic")
|
||||
finish_page.set_description("An error occurred during installation.")
|
||||
btn_label = "Close"
|
||||
|
||||
btn = Gtk.Button(label=btn_label)
|
||||
btn.set_halign(Gtk.Align.CENTER)
|
||||
btn.add_css_class("suggested-action")
|
||||
btn.set_label("Close Installer")
|
||||
btn.connect("clicked", lambda b: self.close())
|
||||
|
||||
finish_page.set_child(btn)
|
||||
@@ -309,7 +360,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
f"Session: {self.session_id} - Configuring bootloader and user",
|
||||
"installer",
|
||||
)
|
||||
configure_system(mount_root, parts, user_info)
|
||||
configure_system(mount_root, parts, user_info, disk_device=disk)
|
||||
nlog(
|
||||
"INSTALL_PROGRESS",
|
||||
f"Session: {self.session_id} - Configuration complete",
|
||||
@@ -452,6 +503,14 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
# Show success in UI even in mock
|
||||
self.show_finish_page("Mock Installation Complete!")
|
||||
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...")
|
||||
thread = threading.Thread(
|
||||
target=self.run_installation,
|
||||
|
||||
21
run.sh
21
run.sh
@@ -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
|
||||
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=.
|
||||
|
||||
python3 -m iridium_installer.main "$@"
|
||||
|
||||
28
run_vm_bios.sh
Executable file
28
run_vm_bios.sh
Executable 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
|
||||
44
run_vm_uefi.sh
Executable file
44
run_vm_uefi.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/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}"
|
||||
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
|
||||
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
|
||||
OVMF_VARS_LOCAL="/tmp/iridium_vm_vars.qcow2"
|
||||
|
||||
# Ensure OVMF vars exist in qcow2 format for snapshot support
|
||||
if [ ! -f "$OVMF_VARS_LOCAL" ]; then
|
||||
if [ -f "$OVMF_VARS_TEMPLATE" ]; then
|
||||
echo "Creating UEFI vars file from template..."
|
||||
qemu-img convert -f raw -O qcow2 "$OVMF_VARS_TEMPLATE" "$OVMF_VARS_LOCAL"
|
||||
else
|
||||
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
|
||||
|
||||
echo "Starting Iridium VM with UEFI support..."
|
||||
echo "ISO: $ISO_PATH"
|
||||
echo "Disk: $DISK_PATH"
|
||||
|
||||
# QEMU Command with UEFI (OVMF) enabled
|
||||
# We use format=qcow2 for pflash1 to allow snapshots (savevm)
|
||||
qemu-system-x86_64 \
|
||||
-enable-kvm \
|
||||
-m 8G \
|
||||
-smp 2 \
|
||||
-cpu host \
|
||||
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
|
||||
-drive if=pflash,format=qcow2,file="$OVMF_VARS_LOCAL" \
|
||||
-drive file="$DISK_PATH",format=qcow2,if=virtio \
|
||||
-cdrom "$ISO_PATH" \
|
||||
-boot once=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 (UEFI)" \
|
||||
-loadvm clean-state
|
||||
19
take_snapshot.sh
Executable file
19
take_snapshot.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOCKET="$HOME/.local/share/iridium-installer-vm/monitor.sock"
|
||||
SNAPSHOT_NAME="${1:-clean-state}"
|
||||
|
||||
if [ ! -S "$SOCKET" ]; then
|
||||
echo "Error: QEMU monitor socket not found at $SOCKET"
|
||||
echo "Is the VM running?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Taking snapshot: $SNAPSHOT_NAME"
|
||||
echo "savevm $SNAPSHOT_NAME" | nc -U "$SOCKET"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Snapshot command sent successfully."
|
||||
else
|
||||
echo "Failed to send snapshot command. Ensure 'nc' (netcat) with -U support is installed."
|
||||
fi
|
||||
Reference in New Issue
Block a user