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 @@
+
+
+
+
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")