diff --git a/pkgs/clan-cli/clan_cli/flash/automount.py b/pkgs/clan-cli/clan_cli/flash/automount.py index 26c2eed8d..d0dda39d4 100644 --- a/pkgs/clan-cli/clan_cli/flash/automount.py +++ b/pkgs/clan-cli/clan_cli/flash/automount.py @@ -1,27 +1,21 @@ import logging -import os import shutil from collections.abc import Generator from contextlib import contextmanager from pathlib import Path -from clan_cli.cmd import run +from clan_cli.cmd import Log, run from clan_cli.errors import ClanError log = logging.getLogger(__name__) @contextmanager -def pause_automounting( - devices: list[Path], no_udev: bool -) -> Generator[None, None, None]: +def pause_automounting(devices: list[Path]) -> Generator[None, None, None]: """ Pause automounting on the device for the duration of this context manager """ - if no_udev: - yield None - return if shutil.which("udevadm") is None: msg = "udev is required to disable automounting" @@ -29,37 +23,18 @@ def pause_automounting( yield None return - if os.geteuid() != 0: - msg = "root privileges are required to disable automounting" + inhibit_path = Path(__file__).parent / "inhibit.sh" + if not inhibit_path.exists(): + msg = f"{inhibit_path} not found" raise ClanError(msg) - try: - # See /usr/lib/udisks2/udisks2-inhibit - rules_dir = Path("/run/udev/rules.d") - rules_dir.mkdir(exist_ok=True) - rule_files: list[Path] = [] - for device in devices: - devpath: str = str(device) - rule_file: Path = ( - rules_dir / f"90-udisks-inhibit-{devpath.replace('/', '_')}.rules" - ) - with rule_file.open("w") as fd: - print( - 'SUBSYSTEM=="block", ENV{DEVNAME}=="' - + devpath - + '*", ENV{UDISKS_IGNORE}="1"', - file=fd, - ) - fd.flush() - os.fsync(fd.fileno()) - rule_files.append(rule_file) - run(["udevadm", "control", "--reload"]) - run(["udevadm", "trigger", "--settle", "--subsystem-match=block"]) - yield None - except Exception as ex: - log.fatal(ex) - finally: - for rule_file in rule_files: - rule_file.unlink(missing_ok=True) - run(["udevadm", "control", "--reload"], check=False) - run(["udevadm", "trigger", "--settle", "--subsystem-match=block"], check=False) + str_devs = [str(dev) for dev in devices] + cmd = ["sudo", str(inhibit_path), "enable", *str_devs] + result = run(cmd, log=Log.BOTH, check=False) + if result.returncode != 0: + log.error("Failed to inhibit automounting") + yield None + cmd = ["sudo", str(inhibit_path), "disable", *str_devs] + result = run(cmd, log=Log.BOTH, check=False) + if result.returncode != 0: + log.error("Failed to re-enable automounting") diff --git a/pkgs/clan-cli/clan_cli/flash/flash.py b/pkgs/clan-cli/clan_cli/flash/flash.py index 6387be02c..ec40809e0 100644 --- a/pkgs/clan-cli/clan_cli/flash/flash.py +++ b/pkgs/clan-cli/clan_cli/flash/flash.py @@ -11,9 +11,11 @@ from typing import Any from clan_cli.api import API from clan_cli.cmd import Log, run from clan_cli.errors import ClanError +from clan_cli.facts.generate import generate_facts from clan_cli.facts.secret_modules import SecretStoreBase from clan_cli.machines.machines import Machine from clan_cli.nix import nix_shell +from clan_cli.vars.generate import generate_vars_for_machine from .automount import pause_automounting from .list import list_possible_keymaps, list_possible_languages @@ -24,7 +26,6 @@ log = logging.getLogger(__name__) @dataclass class WifiConfig: ssid: str - password: str @dataclass @@ -51,19 +52,21 @@ def flash_machine( dry_run: bool, write_efi_boot_entries: bool, debug: bool, - no_udev: bool = False, extra_args: list[str] | None = None, ) -> None: devices = [Path(disk.device) for disk in disks] - with pause_automounting(devices, no_udev): + with pause_automounting(devices): if extra_args is None: extra_args = [] system_config_nix: dict[str, Any] = {} + generate_vars_for_machine(machine, generator_name=None, regenerate=False) + generate_facts([machine], service=None, regenerate=False) + if system_config.wifi_settings: - wifi_settings = {} + wifi_settings: dict[str, dict[str, str]] = {} for wifi in system_config.wifi_settings: - wifi_settings[wifi.ssid] = {"password": wifi.password} + wifi_settings[wifi.ssid] = {} system_config_nix["clan"] = {"iwd": {"networks": wifi_settings}} if system_config.language: diff --git a/pkgs/clan-cli/clan_cli/flash/flash_cmd.py b/pkgs/clan-cli/clan_cli/flash/flash_cmd.py index 9238b42b2..6b5fc443e 100644 --- a/pkgs/clan-cli/clan_cli/flash/flash_cmd.py +++ b/pkgs/clan-cli/clan_cli/flash/flash_cmd.py @@ -26,7 +26,6 @@ class FlashOptions: mode: str write_efi_boot_entries: bool nix_options: list[str] - no_udev: bool system_config: SystemConfig @@ -73,15 +72,11 @@ def flash_command(args: argparse.Namespace) -> None: wifi_settings=None, ), write_efi_boot_entries=args.write_efi_boot_entries, - no_udev=args.no_udev, nix_options=args.option, ) if args.wifi: - opts.system_config.wifi_settings = [ - WifiConfig(ssid=ssid, password=password) - for ssid, password in args.wifi.items() - ] + opts.system_config.wifi_settings = [WifiConfig(ssid=ssid) for ssid in args.wifi] machine = Machine(opts.machine, flake=opts.flake) if opts.confirm and not opts.dry_run: @@ -102,7 +97,6 @@ def flash_command(args: argparse.Namespace) -> None: dry_run=opts.dry_run, debug=opts.debug, write_efi_boot_entries=opts.write_efi_boot_entries, - no_udev=opts.no_udev, extra_args=opts.nix_options, ) @@ -135,11 +129,9 @@ def register_flash_apply_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--wifi", type=str, - nargs=2, - metavar=("ssid", "password"), - action=AppendDiskAction, - help="wifi network to connect to", - default={}, + action="append", + help="wifi ssid to connect to", + default=[], ) parser.add_argument( "--mode", @@ -160,12 +152,6 @@ def register_flash_apply_parser(parser: argparse.ArgumentParser) -> None: type=str, help="system language", ) - parser.add_argument( - "--no-udev", - help="Disable udev rules to block automounting", - default=False, - action="store_true", - ) parser.add_argument( "--keymap", type=str, diff --git a/pkgs/clan-cli/clan_cli/flash/inhibit.sh b/pkgs/clan-cli/clan_cli/flash/inhibit.sh new file mode 100755 index 000000000..3d2ea13ff --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flash/inhibit.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# A function to log error messages +log_fatal() { + echo "FATAL: $*" >&2 +} + +# A function to log warning messages +log_warning() { + echo "WARNING: $*" >&2 +} + + +# Function to enable inhibition (pause automounting) +enable_inhibition() { + local devices=("$@") + + if ! command -v udevadm &> /dev/null; then + log_fatal "udev is required to disable automounting" + exit 1 + fi + + if [ "$EUID" -ne 0 ]; then + log_fatal "Root privileges are required to disable automounting" + exit 1 + fi + + local rules_dir="/run/udev/rules.d" + mkdir -p "$rules_dir" + + for device in "${devices[@]}"; do + local devpath="$device" + local rule_file="$rules_dir/90-udisks-inhibit-${devpath//\//_}.rules" + echo 'SUBSYSTEM=="block", ENV{DEVNAME}=="'"$devpath"'*", ENV{UDISKS_IGNORE}="1"' > "$rule_file" + sync + done + + udevadm control --reload + udevadm trigger --settle --subsystem-match=block +} + +# Function to disable inhibition (cleanup) +disable_inhibition() { + local devices=("$@") + local rules_dir="/run/udev/rules.d" + + for device in "${devices[@]}"; do + local devpath="$device" + local rule_file="$rules_dir/90-udisks-inhibit-${devpath//\//_}.rules" + rm -f "$rule_file" || log_warning "Could not remove file: $rule_file" + done + + udevadm control --reload || log_warning "Could not reload udev rules" + udevadm trigger --settle --subsystem-match=block || log_warning "Could not trigger udev settle" +} + +if [ "$#" -lt 2 ]; then + echo "Usage: $0 [enable|disable] /dev/sdX ..." + exit 1 +fi + +action=$1 +shift +devices=("$@") + +case "$action" in + enable) + enable_inhibition "${devices[@]}" + ;; + disable) + disable_inhibition "${devices[@]}" + ;; + *) + echo "Invalid action: $action" + echo "Usage: $0 [enable|disable] /dev/sdX ..." + exit 1 + ;; +esac \ No newline at end of file diff --git a/pkgs/clan-cli/clan_cli/inventory/__init__.py b/pkgs/clan-cli/clan_cli/inventory/__init__.py index 841a238f3..08de91daf 100644 --- a/pkgs/clan-cli/clan_cli/inventory/__init__.py +++ b/pkgs/clan-cli/clan_cli/inventory/__init__.py @@ -171,24 +171,25 @@ def merge_template_inventory( ) raise ClanError(msg, description=description) - # Get the service config without knowing instance name - service_conf = next((v for v in instance.values() if "config" in v), None) - - if not service_conf: + # services...config + config = next((v for v in instance.values() if "config" in v), None) + if not config: msg = f"Service {service_name} in template inventory has no config" description = "Invalid inventory configuration" raise ClanError(msg, description=description) - if "machines" in service_conf: + # Disallow "config.machines" key + if "machines" in config: msg = f"Service {service_name} in template inventory has machines" description = "The 'machines' key is not allowed in template inventory" raise ClanError(msg, description=description) - if "roles" not in service_conf: + # Require "config.roles" key + if "roles" not in config: msg = f"Service {service_name} in template inventory has no roles" description = "roles key is required in template inventory" raise ClanError(msg, description=description) - # TODO: We need a MachineReference type in nix before we can implement this properly + # TODO: Implement merging of template inventory msg = "Merge template inventory is not implemented yet" raise NotImplementedError(msg) diff --git a/pkgs/clan-cli/clan_cli/vars/graph.py b/pkgs/clan-cli/clan_cli/vars/graph.py index 14841f681..d716ab372 100644 --- a/pkgs/clan-cli/clan_cli/vars/graph.py +++ b/pkgs/clan-cli/clan_cli/vars/graph.py @@ -3,11 +3,16 @@ from dataclasses import dataclass from functools import cached_property from graphlib import TopologicalSorter +from clan_cli.errors import ClanError from clan_cli.machines.machines import Machine from .check import check_vars +class GeneratorNotFoundError(ClanError): + pass + + @dataclass class Generator: name: str @@ -28,6 +33,11 @@ def missing_dependency_closure( queue = list(closure) while queue: gen_name = queue.pop(0) + + if gen_name not in generators: + msg = f"Requested generator {gen_name} not found" + raise GeneratorNotFoundError(msg) + for dep in generators[gen_name].dependencies: if dep not in closure and not generators[dep].exists: dep_closure.add(dep) diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 32990eb95..674ccae8f 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -25,6 +25,7 @@ clan_cli = [ "templates/**/*", "vms/mimetypes/**/*", "webui/assets/**/*", + "flash/*.sh" ] [tool.pytest.ini_options] diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index aef6dd2ec..8eb07b8f6 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -160,7 +160,6 @@ export const Flash = () => { dry_run: false, write_efi_boot_entries: false, debug: false, - no_udev: true, }); } catch (error) { toast.error(`Error could not flash disk: ${error}`); diff --git a/templates/machineTemplates/machines/flash-installer/configuration.nix b/templates/machineTemplates/machines/flash-installer/configuration.nix index 9b3644f27..904b0bba4 100644 --- a/templates/machineTemplates/machines/flash-installer/configuration.nix +++ b/templates/machineTemplates/machines/flash-installer/configuration.nix @@ -1,7 +1,18 @@ +{ + config, + clan-core, + inputs, + ... +}: { imports = [ - + ./disko.nix + clan-core.nixosModules.installer + clan-core.clanModules.trusted-nix-caches + clan-core.clanModules.disk-id + clan-core.clanModules.iwd ]; - # Flash machine template + nixpkgs.pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux; + system.stateVersion = config.system.nixos.version; } diff --git a/templates/machineTemplates/machines/flash-installer/disko.nix b/templates/machineTemplates/machines/flash-installer/disko.nix new file mode 100644 index 000000000..60e2b5f14 --- /dev/null +++ b/templates/machineTemplates/machines/flash-installer/disko.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +let + suffix = config.clan.core.vars.generators.disk-id.files.diskId.value; +in +{ + boot.loader.grub.efiSupport = lib.mkDefault true; + boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true; + disko.devices = { + disk = { + "main" = { + name = "main-" + suffix; + type = "disk"; + device = lib.mkDefault "/dev/null"; + content = { + type = "gpt"; + partitions = { + "boot" = { + size = "1M"; + type = "EF02"; # for grub MBR + priority = 1; + }; + "ESP" = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + name = "root"; + end = "-0"; + content = { + type = "filesystem"; + format = "f2fs"; + mountpoint = "/"; + extraArgs = [ + "-O" + "extra_attr,inode_checksum,sb_checksum,compression" + ]; + # Recommendations for flash: https://wiki.archlinux.org/title/F2FS#Recommended_mount_options + mountOptions = [ + "compress_algorithm=zstd:6,compress_chksum,atgc,gc_merge,lazytime,nodiscard" + ]; + }; + }; + }; + }; + }; + }; + }; +}