Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions snow_first_setup/gtk/install-confirm.ui
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@
</object>
</child>

<child>
<object class="AdwPreferencesGroup" id="root_password_group">
<property name="title" translatable="yes">Root Password</property>
<property name="description" translatable="yes">Set the root password for system administration</property>
<property name="visible">False</property>
<child>
<object class="AdwPasswordEntryRow" id="root_password_entry">
<property name="title" translatable="yes">Root Password</property>
<property name="subtitle" translatable="yes">Use a strong password, ideally at least 8 characters with a mix of letters, numbers, and symbols.</property>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="root_password_confirm_entry">
<property name="title" translatable="yes">Confirm Root Password</property>
<property name="subtitle" translatable="yes">Re-enter the root password to confirm it matches.</property>
</object>
</child>
</object>
</child>

<child>
<object class="AdwPreferencesGroup">
<property name="title" translatable="yes">Confirmation</property>
Expand Down
18 changes: 13 additions & 5 deletions snow_first_setup/images.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
]
}
39 changes: 31 additions & 8 deletions snow_first_setup/scripts/install-to-disk
Original file line number Diff line number Diff line change
Expand Up @@ -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 <image> <filesystem> <device> [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 <image> <filesystem> <device> [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 <image> <filesystem> <device> [fde] [passphrase] [tpm2]"}'
echo '{"type":"error","message":"Missing arguments. Usage: install-to-disk <image> <filesystem> <device> [fde] [passphrase] [tpm2] [root_password_file]"}'
exit 5
fi

Expand All @@ -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
Expand All @@ -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"}'
Expand Down Expand Up @@ -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[@]}"
74 changes: 72 additions & 2 deletions snow_first_setup/views/install_confirm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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

Expand Down Expand Up @@ -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:
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Copilot uses AI. Check for mistakes.
pass

# Trigger __on_input_changed to populate __image_target from current combo selection
self.__on_input_changed()

Expand Down Expand Up @@ -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 = ""
Comment on lines +139 to +141
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception handler swallows all exceptions and prints a debug message, then continues with empty password strings. This could mask serious errors like GTK widget access failures. Consider logging the exception type and stack trace, or re-raising critical exceptions that indicate widget initialization problems.

Copilot uses AI. Check for mistakes.

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
Comment on lines +180 to +185
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password validation only checks that passwords are non-empty and match, but doesn't enforce any password strength requirements. For a root password, consider adding minimum length requirements or warning users about weak passwords, especially since this is for server images where security is critical.

Copilot uses AI. Check for mistakes.
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)

Expand All @@ -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
Comment on lines +215 to +216
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The root password is stored directly in the window object without any cleanup mechanism. Once set, the password remains in memory as plain text until the application exits. Consider implementing a cleanup method that clears this sensitive data after the installation completes, or use a more secure approach like clearing it immediately after passing to the install script.

Copilot uses AI. Check for mistakes.
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

Expand Down
37 changes: 34 additions & 3 deletions snow_first_setup/views/install_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import json
import threading
import time
import tempfile
import os

from gi.repository import Gtk, Adw, GLib, Gio

Expand All @@ -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:
Expand Down Expand Up @@ -150,22 +153,41 @@ 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."))
return

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
Expand All @@ -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}")
Comment on lines +201 to +208
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password file cleanup happens only in the success path after run_stream_script returns. If an exception occurs during run_stream_script or if the installation is interrupted, the temporary password file will not be cleaned up. Consider wrapping the script execution in a try-finally block to guarantee cleanup regardless of the outcome.

Copilot uses AI. Check for mistakes.

# Handle Snowfield image special case
if success and "snowfield" in image:
print("[DEBUG] __run_install: Snowfield image selected, importing Surface Linux secure boot key")
Expand Down
Loading