diff --git a/snow_first_setup/gtk/install-confirm.ui b/snow_first_setup/gtk/install-confirm.ui index 5ff031f..8227730 100644 --- a/snow_first_setup/gtk/install-confirm.ui +++ b/snow_first_setup/gtk/install-confirm.ui @@ -70,6 +70,26 @@ + + + Root Password + Set the root password for system administration + False + + + Root Password + Use a strong password, ideally at least 8 characters with a mix of letters, numbers, and symbols. + + + + + Confirm Root Password + Re-enter the root password to confirm it matches. + + + + + Confirmation diff --git a/snow_first_setup/images.json b/snow_first_setup/images.json index cc08c1d..0b81057 100644 --- a/snow_first_setup/images.json +++ b/snow_first_setup/images.json @@ -1,16 +1,24 @@ { "images": [ - { - "snow": { + { "target": "ghcr.io/frostyard/snow:latest", "description": "standard", "display_name": "Snow Linux" }, - "snowfield": { + { "target": "ghcr.io/frostyard/snowfield:latest", "description": "Surface", "display_name": "Snowfield Linux" + }, + { + "target": "ghcr.io/frostyard/snowdrift:latest", + "description": "server", + "display_name": "Snowdrift Server" + }, + { + "target": "ghcr.io/frostyard/cayo:latest", + "description": "server", + "display_name": "Cayo Server" } - } -] + ] } \ No newline at end of file diff --git a/snow_first_setup/scripts/install-to-disk b/snow_first_setup/scripts/install-to-disk index ada8a88..2f62a36 100755 --- a/snow_first_setup/scripts/install-to-disk +++ b/snow_first_setup/scripts/install-to-disk @@ -4,16 +4,17 @@ set -euo pipefail # Install script using nbc (SNOW bootc installer) # Outputs JSON Lines for streaming progress to the installer GUI # -# Usage: install-to-disk [fde] [passphrase] [tpm2] -# image - container image reference -# filesystem - filesystem type (btrfs, ext4) -# device - block device path -# fde - "true" or "false" for full disk encryption (default: false) -# passphrase - encryption passphrase (required if fde=true) -# tpm2 - "true" or "false" for TPM2 auto-unlock (default: false) +# Usage: install-to-disk [fde] [passphrase] [tpm2] [root_password_file] +# image - container image reference +# filesystem - filesystem type (btrfs, ext4) +# device - block device path +# fde - "true" or "false" for full disk encryption (default: false) +# passphrase - encryption passphrase (required if fde=true) +# tpm2 - "true" or "false" for TPM2 auto-unlock (default: false) +# root_password_file - path to file containing root password, or "/dev/null" (default: /dev/null) if [ -z "${1:-}" ] || [ -z "${2:-}" ] || [ -z "${3:-}" ]; then - echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk [fde] [passphrase] [tpm2]"}' + echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk [fde] [passphrase] [tpm2] [root_password_file]"}' exit 5 fi @@ -33,6 +34,7 @@ DEVICE="$3" FDE="${4:-false}" PASSPHRASE="${5:-}" TPM2="${6:-false}" +ROOT_PASSWORD_FILE="${7:-/dev/null}" # Validate FDE parameter if [ "$FDE" != "true" ] && [ "$FDE" != "false" ]; then @@ -46,6 +48,22 @@ if [ "$TPM2" != "true" ] && [ "$TPM2" != "false" ]; then exit 8 fi +# Validate root password file if specified +if [ "$ROOT_PASSWORD_FILE" != "/dev/null" ]; then + if [ ! -f "$ROOT_PASSWORD_FILE" ]; then + echo '{"type":"error","message":"Root password file does not exist: '"$ROOT_PASSWORD_FILE"'"}' + exit 11 + fi + if [ ! -s "$ROOT_PASSWORD_FILE" ]; then + echo '{"type":"error","message":"Root password file is empty: '"$ROOT_PASSWORD_FILE"'"}' + exit 12 + fi + if ! grep -q '[^[:space:]]' < "$ROOT_PASSWORD_FILE"; then + echo '{"type":"error","message":"Root password file contains only whitespace: '"$ROOT_PASSWORD_FILE"'"}' + exit 13 + fi +fi + # If FDE is enabled, require a passphrase if [ "$FDE" == "true" ] && [ -z "$PASSPHRASE" ]; then echo '{"type":"error","message":"FDE is enabled but no passphrase provided"}' @@ -78,6 +96,11 @@ if [ "$FDE" == "true" ]; then fi fi +# Add root password file if provided and not /dev/null +if [ "$ROOT_PASSWORD_FILE" != "/dev/null" ]; then + NBC_ARGS+=("--root-password-file" "$ROOT_PASSWORD_FILE") +fi + # Run nbc install with JSON output streaming directly to stdout # nbc outputs JSON Lines (one JSON object per line) for real-time progress exec nbc "${NBC_ARGS[@]}" \ No newline at end of file diff --git a/snow_first_setup/views/install_confirm.py b/snow_first_setup/views/install_confirm.py index 20a77ed..40f8470 100644 --- a/snow_first_setup/views/install_confirm.py +++ b/snow_first_setup/views/install_confirm.py @@ -17,6 +17,9 @@ class VanillaInstallConfirm(Adw.Bin): fs_label = Gtk.Template.Child() fde_label = Gtk.Template.Child() image_combo = Gtk.Template.Child() + root_password_group = Gtk.Template.Child() + root_password_entry = Gtk.Template.Child() + root_password_confirm_entry = Gtk.Template.Child() confirm_checkbox = Gtk.Template.Child() cancel_button = Gtk.Template.Child() @@ -28,11 +31,15 @@ def __init__(self, window, **kwargs): self.__image_text = "" self.__image_target = None self.__confirm_checked = False + self.__root_password = "" + self.__root_password_confirm = "" # connect signals to update readiness try: self.confirm_checkbox.connect("toggled", self.__on_input_changed) self.cancel_button.connect("clicked", self.__on_cancel_clicked) + self.root_password_entry.connect("changed", self.__on_input_changed) + self.root_password_confirm_entry.connect("changed", self.__on_input_changed) except Exception: pass @@ -78,6 +85,15 @@ def set_page_active(self): pass self.__confirm_checked = False + # Clear password fields + try: + self.root_password_entry.set_text("") + self.root_password_confirm_entry.set_text("") + self.__root_password = "" + self.__root_password_confirm = "" + except Exception: + pass + # Trigger __on_input_changed to populate __image_target from current combo selection self.__on_input_changed() @@ -115,15 +131,61 @@ def __on_input_changed(self, *args): except Exception: self.__confirm_checked = False + # Update password fields visibility and cache passwords + self.__update_password_visibility() + try: + self.__root_password = self.root_password_entry.get_text() + self.__root_password_confirm = self.root_password_confirm_entry.get_text() + except Exception: + self.__root_password = "" + self.__root_password_confirm = "" + print(f"[DEBUG] Final state: image_target={self.__image_target}, confirm={self.__confirm_checked}") self.__validate() + def __update_password_visibility(self): + """Show password fields only for specific images that require a root password""" + try: + # Define which image targets require a root password (canonical, lowercased IDs) + password_required_targets = ("cayo", "snowdrift") + + needs_root_password = False + target_lower = None + text_lower = None + + if self.__image_target: + target_lower = self.__image_target.lower() + + if self.__image_text: + text_lower = self.__image_text.lower() + + # Prefer the canonical target identifier if available; fall back to display text. + if target_lower is not None: + needs_root_password = target_lower in password_required_targets + elif text_lower is not None: + needs_root_password = text_lower in password_required_targets + self.root_password_group.set_visible(needs_root_password) + print(f"[DEBUG] Root password visibility: {needs_root_password} (target={self.__image_target}, text={self.__image_text})") + except Exception as e: + print(f"[DEBUG] Exception in __update_password_visibility: {e}") + def __validate(self): # enable Next only if image target specified and confirmation checked ok = False try: ok = bool(self.__image_target) and self.__confirm_checked and self.__image_target != _("No images found") - except Exception: + + # If root password is visible, validate that passwords match and are not empty + if ok and self.root_password_group.get_visible(): + passwords_valid = ( + self.__root_password and + self.__root_password_confirm and + self.__root_password == self.__root_password_confirm + ) + ok = ok and passwords_valid + print(f"[DEBUG] Password validation: passwords_valid={passwords_valid}, pw_len={len(self.__root_password)}, confirm_len={len(self.__root_password_confirm)}, match={self.__root_password == self.__root_password_confirm}") + except Exception as e: + print(f"[DEBUG] Exception in __validate: {e}") ok = False self.__window.set_ready(ok) @@ -148,8 +210,16 @@ def finish(self): try: self.__window.install_target_image = image print(f"[DEBUG] Successfully set window.install_target_image = {image}") + + # Store root password if it was collected + if self.root_password_group.get_visible() and self.__root_password: + self.__window.install_root_password = self.__root_password + print(f"[DEBUG] Successfully set window.install_root_password (length={len(self.__root_password)})") + else: + self.__window.install_root_password = None + print(f"[DEBUG] No root password needed or collected") except Exception as e: - print("[exception] Failed to set install_target_image:", e) + print("[exception] Failed to set install parameters:", e) pass return True diff --git a/snow_first_setup/views/install_progress.py b/snow_first_setup/views/install_progress.py index 9370c9c..b863f4a 100644 --- a/snow_first_setup/views/install_progress.py +++ b/snow_first_setup/views/install_progress.py @@ -3,6 +3,8 @@ import json import threading import time +import tempfile +import os from gi.repository import Gtk, Adw, GLib, Gio @@ -27,6 +29,7 @@ def __init__(self, window, **kwargs): self.__current_step = 0 self.__total_steps = 0 self.__has_progress = False # True when we have percentage-based progress + self.__root_password_file = None # Track temp file for cleanup # Debug: verify resource presence and child realization try: @@ -150,7 +153,8 @@ def __run_install(self): fde_enabled = getattr(self.__window, "install_fde_enabled", False) fde_passphrase = getattr(self.__window, "install_fde_passphrase", None) or "" tpm_enabled = getattr(self.__window, "install_tpm_enabled", False) - print("[DEBUG] __run_install params:", device, fs, image, "fde_enabled:", fde_enabled, "tpm_enabled:", tpm_enabled) + root_password = getattr(self.__window, "install_root_password", None) + print("[DEBUG] __run_install params:", device, fs, image, "fde_enabled:", fde_enabled, "tpm_enabled:", tpm_enabled, "root_password:", bool(root_password)) if not device or not fs or not image: GLib.idle_add(self.__mark_finished, False, _("Missing installation parameters.")) @@ -158,14 +162,32 @@ def __run_install(self): GLib.idle_add(self.detail_label.set_text, _("Preparing installation…")) - # Build script arguments: image, filesystem, device, fde, passphrase, tpm2 + # Handle root password: create temp file if password is set, otherwise use /dev/null + root_password_path = "/dev/null" + if root_password: + try: + # Create a temporary file and immediately harden its permissions + fd, root_password_path = tempfile.mkstemp(prefix="snow-root-pw-", text=True) + os.fchmod(fd, 0o600) + self.__root_password_file = root_password_path + # Write password to file and ensure the descriptor is closed + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(root_password) + print(f"[DEBUG] Created root password file: {root_password_path}") + except Exception as e: + print(f"[ERROR] Failed to create root password file: {e}") + GLib.idle_add(self.__mark_finished, False, _("Failed to prepare root password.")) + return + + # Build script arguments: image, filesystem, device, fde, passphrase, tpm2, root_password_file script_args = [ image, fs, device, "true" if fde_enabled else "false", fde_passphrase if fde_enabled else "", - "true" if (fde_enabled and tpm_enabled) else "false" + "true" if (fde_enabled and tpm_enabled) else "false", + root_password_path ] # Use streaming script runner to get real-time JSON updates @@ -176,6 +198,15 @@ def __run_install(self): line_callback=self.__handle_json_line ) + # Clean up root password file if created + if self.__root_password_file and os.path.exists(self.__root_password_file): + try: + os.unlink(self.__root_password_file) + print(f"[DEBUG] Cleaned up root password file: {self.__root_password_file}") + self.__root_password_file = None + except Exception as e: + print(f"[WARNING] Failed to clean up root password file: {e}") + # Handle Snowfield image special case if success and "snowfield" in image: print("[DEBUG] __run_install: Snowfield image selected, importing Surface Linux secure boot key")