Merge pull request 'clan flash: Remove root requirement for flash, add a flash-template' (#2165) from Qubasa/clan-core:Qubasa-main into main

This commit is contained in:
clan-bot
2024-09-24 11:48:57 +00:00
10 changed files with 191 additions and 73 deletions

View File

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

View File

@@ -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:

View File

@@ -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,

View 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

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ clan_cli = [
"templates/**/*",
"vms/mimetypes/**/*",
"webui/assets/**/*",
"flash/*.sh"
]
[tool.pytest.ini_options]

View File

@@ -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}`);

View File

@@ -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;
}

View File

@@ -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"
];
};
};
};
};
};
};
};
}