From 4b841c7885b9974e2c9e4878982514f93b1a5938 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:28:00 -0500 Subject: [PATCH 1/7] feat: support cayo/snowdrift and root password Signed-off-by: Brian Ketelsen --- snow_first_setup/gtk/install-confirm.ui | 18 ++++++ snow_first_setup/images.json | 34 +++++++---- snow_first_setup/scripts/install-to-disk | 35 +++++++++--- snow_first_setup/views/install_confirm.py | 65 +++++++++++++++++++++- snow_first_setup/views/install_progress.py | 38 ++++++++++++- 5 files changed, 165 insertions(+), 25 deletions(-) diff --git a/snow_first_setup/gtk/install-confirm.ui b/snow_first_setup/gtk/install-confirm.ui index 5ff031f..72d0b97 100644 --- a/snow_first_setup/gtk/install-confirm.ui +++ b/snow_first_setup/gtk/install-confirm.ui @@ -70,6 +70,24 @@ + + + Root Password + Set the root password for system administration + False + + + Root Password + + + + + Confirm Root Password + + + + + Confirmation diff --git a/snow_first_setup/images.json b/snow_first_setup/images.json index cc08c1d..1e7e57d 100644 --- a/snow_first_setup/images.json +++ b/snow_first_setup/images.json @@ -1,16 +1,26 @@ { "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" + { + "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" + }, + "snowdrift": { + "target": "ghcr.io/frostyard/snowdrift:latest", + "description": "server", + "display_name": "Snowdrift Server" + }, + "cayo": { + "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..47b9394 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,18 @@ 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 +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 +92,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..ed2358c 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,52 @@ 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 cayo or snowdrift images""" + try: + # Check if the target image is cayo or snowdrift + needs_root_password = False + if self.__image_target: + # Check both the target string and the display text + target_lower = self.__image_target.lower() + text_lower = self.__image_text.lower() + needs_root_password = 'cayo' in target_lower or 'snowdrift' in target_lower or 'cayo' in text_lower or 'snowdrift' in text_lower + + 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}") + pass + 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 +201,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..dde8d71 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,33 @@ 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 with restrictive permissions + fd, root_password_path = tempfile.mkstemp(prefix="snow-root-pw-", text=True) + self.__root_password_file = root_password_path + # Write password to file + os.write(fd, root_password.encode('utf-8')) + os.close(fd) + # Ensure only root can read it + os.chmod(root_password_path, 0o600) + 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 +199,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") From 4ccbf1a4ca1afd31aa7b932d21eb6a66ac92c618 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:39:09 -0500 Subject: [PATCH 2/7] Update snow_first_setup/scripts/install-to-disk Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/scripts/install-to-disk | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snow_first_setup/scripts/install-to-disk b/snow_first_setup/scripts/install-to-disk index 47b9394..2f62a36 100755 --- a/snow_first_setup/scripts/install-to-disk +++ b/snow_first_setup/scripts/install-to-disk @@ -58,6 +58,10 @@ if [ "$ROOT_PASSWORD_FILE" != "/dev/null" ]; 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 From 4fbaaf465417b240746ea6c71b1eb104e7d02a38 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:42:18 -0500 Subject: [PATCH 3/7] Update snow_first_setup/gtk/install-confirm.ui Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/gtk/install-confirm.ui | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snow_first_setup/gtk/install-confirm.ui b/snow_first_setup/gtk/install-confirm.ui index 72d0b97..8227730 100644 --- a/snow_first_setup/gtk/install-confirm.ui +++ b/snow_first_setup/gtk/install-confirm.ui @@ -78,11 +78,13 @@ 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. From e9267522c32ec39bf8e92e35fa587c5cb24b7e05 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:43:23 -0500 Subject: [PATCH 4/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/views/install_confirm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/snow_first_setup/views/install_confirm.py b/snow_first_setup/views/install_confirm.py index ed2358c..80af136 100644 --- a/snow_first_setup/views/install_confirm.py +++ b/snow_first_setup/views/install_confirm.py @@ -158,7 +158,6 @@ def __update_password_visibility(self): 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}") - pass def __validate(self): # enable Next only if image target specified and confirmation checked From ae784120e316f42f1a0ed7bdd60ddd969176e6d3 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:44:16 -0500 Subject: [PATCH 5/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/views/install_confirm.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/snow_first_setup/views/install_confirm.py b/snow_first_setup/views/install_confirm.py index 80af136..40f8470 100644 --- a/snow_first_setup/views/install_confirm.py +++ b/snow_first_setup/views/install_confirm.py @@ -144,16 +144,26 @@ def __on_input_changed(self, *args): self.__validate() def __update_password_visibility(self): - """Show password fields only for cayo or snowdrift images""" + """Show password fields only for specific images that require a root password""" try: - # Check if the target image is cayo or snowdrift + # 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: - # Check both the target string and the display text target_lower = self.__image_target.lower() + + if self.__image_text: text_lower = self.__image_text.lower() - needs_root_password = 'cayo' in target_lower or 'snowdrift' in target_lower or 'cayo' in text_lower or 'snowdrift' in 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: From 97b60eef70c33b0d8c5af46e3f44a2ea56d637d3 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:44:43 -0500 Subject: [PATCH 6/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/views/install_progress.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/snow_first_setup/views/install_progress.py b/snow_first_setup/views/install_progress.py index dde8d71..b863f4a 100644 --- a/snow_first_setup/views/install_progress.py +++ b/snow_first_setup/views/install_progress.py @@ -166,14 +166,13 @@ def __run_install(self): root_password_path = "/dev/null" if root_password: try: - # Create a temporary file with restrictive permissions + # 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 - os.write(fd, root_password.encode('utf-8')) - os.close(fd) - # Ensure only root can read it - os.chmod(root_password_path, 0o600) + # 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}") From e8292cccca1c95d6d9572c64f58b71aca8d34350 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Tue, 30 Dec 2025 20:47:57 -0500 Subject: [PATCH 7/7] Update snow_first_setup/images.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- snow_first_setup/images.json | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/snow_first_setup/images.json b/snow_first_setup/images.json index 1e7e57d..0b81057 100644 --- a/snow_first_setup/images.json +++ b/snow_first_setup/images.json @@ -1,26 +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" - }, - "snowdrift": { - "target": "ghcr.io/frostyard/snowdrift:latest", - "description": "server", - "display_name": "Snowdrift Server" - }, - "cayo": { - "target": "ghcr.io/frostyard/cayo:latest", - "description": "server", - "display_name": "Cayo Server" - } + "target": "ghcr.io/frostyard/snow:latest", + "description": "standard", + "display_name": "Snow Linux" + }, + { + "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