clan flash: Remove root requirement for flash, add a flash-template
This commit is contained in:
@@ -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"])
|
||||
|
||||
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
|
||||
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)
|
||||
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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
78
pkgs/clan-cli/clan_cli/flash/inhibit.sh
Executable file
78
pkgs/clan-cli/clan_cli/flash/inhibit.sh
Executable file
@@ -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
|
||||
@@ -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.<service_name>.<instance_name>.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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -25,6 +25,7 @@ clan_cli = [
|
||||
"templates/**/*",
|
||||
"vms/mimetypes/**/*",
|
||||
"webui/assets/**/*",
|
||||
"flash/*.sh"
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user