clan flash: Remove root requirement for flash, add a flash-template
This commit is contained in:
@@ -1,27 +1,21 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from clan_cli.cmd import run
|
from clan_cli.cmd import Log, run
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def pause_automounting(
|
def pause_automounting(devices: list[Path]) -> Generator[None, None, None]:
|
||||||
devices: list[Path], no_udev: bool
|
|
||||||
) -> Generator[None, None, None]:
|
|
||||||
"""
|
"""
|
||||||
Pause automounting on the device for the duration of this context
|
Pause automounting on the device for the duration of this context
|
||||||
manager
|
manager
|
||||||
"""
|
"""
|
||||||
if no_udev:
|
|
||||||
yield None
|
|
||||||
return
|
|
||||||
|
|
||||||
if shutil.which("udevadm") is None:
|
if shutil.which("udevadm") is None:
|
||||||
msg = "udev is required to disable automounting"
|
msg = "udev is required to disable automounting"
|
||||||
@@ -29,37 +23,18 @@ def pause_automounting(
|
|||||||
yield None
|
yield None
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
inhibit_path = Path(__file__).parent / "inhibit.sh"
|
||||||
msg = "root privileges are required to disable automounting"
|
if not inhibit_path.exists():
|
||||||
|
msg = f"{inhibit_path} not found"
|
||||||
raise ClanError(msg)
|
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
|
str_devs = [str(dev) for dev in devices]
|
||||||
except Exception as ex:
|
cmd = ["sudo", str(inhibit_path), "enable", *str_devs]
|
||||||
log.fatal(ex)
|
result = run(cmd, log=Log.BOTH, check=False)
|
||||||
finally:
|
if result.returncode != 0:
|
||||||
for rule_file in rule_files:
|
log.error("Failed to inhibit automounting")
|
||||||
rule_file.unlink(missing_ok=True)
|
yield None
|
||||||
run(["udevadm", "control", "--reload"], check=False)
|
cmd = ["sudo", str(inhibit_path), "disable", *str_devs]
|
||||||
run(["udevadm", "trigger", "--settle", "--subsystem-match=block"], check=False)
|
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.api import API
|
||||||
from clan_cli.cmd import Log, run
|
from clan_cli.cmd import Log, run
|
||||||
from clan_cli.errors import ClanError
|
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.facts.secret_modules import SecretStoreBase
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.nix import nix_shell
|
from clan_cli.nix import nix_shell
|
||||||
|
from clan_cli.vars.generate import generate_vars_for_machine
|
||||||
|
|
||||||
from .automount import pause_automounting
|
from .automount import pause_automounting
|
||||||
from .list import list_possible_keymaps, list_possible_languages
|
from .list import list_possible_keymaps, list_possible_languages
|
||||||
@@ -24,7 +26,6 @@ log = logging.getLogger(__name__)
|
|||||||
@dataclass
|
@dataclass
|
||||||
class WifiConfig:
|
class WifiConfig:
|
||||||
ssid: str
|
ssid: str
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -51,19 +52,21 @@ def flash_machine(
|
|||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
write_efi_boot_entries: bool,
|
write_efi_boot_entries: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
no_udev: bool = False,
|
|
||||||
extra_args: list[str] | None = None,
|
extra_args: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
devices = [Path(disk.device) for disk in disks]
|
devices = [Path(disk.device) for disk in disks]
|
||||||
with pause_automounting(devices, no_udev):
|
with pause_automounting(devices):
|
||||||
if extra_args is None:
|
if extra_args is None:
|
||||||
extra_args = []
|
extra_args = []
|
||||||
system_config_nix: dict[str, Any] = {}
|
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:
|
if system_config.wifi_settings:
|
||||||
wifi_settings = {}
|
wifi_settings: dict[str, dict[str, str]] = {}
|
||||||
for wifi in system_config.wifi_settings:
|
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}}
|
system_config_nix["clan"] = {"iwd": {"networks": wifi_settings}}
|
||||||
|
|
||||||
if system_config.language:
|
if system_config.language:
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class FlashOptions:
|
|||||||
mode: str
|
mode: str
|
||||||
write_efi_boot_entries: bool
|
write_efi_boot_entries: bool
|
||||||
nix_options: list[str]
|
nix_options: list[str]
|
||||||
no_udev: bool
|
|
||||||
system_config: SystemConfig
|
system_config: SystemConfig
|
||||||
|
|
||||||
|
|
||||||
@@ -73,15 +72,11 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
wifi_settings=None,
|
wifi_settings=None,
|
||||||
),
|
),
|
||||||
write_efi_boot_entries=args.write_efi_boot_entries,
|
write_efi_boot_entries=args.write_efi_boot_entries,
|
||||||
no_udev=args.no_udev,
|
|
||||||
nix_options=args.option,
|
nix_options=args.option,
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.wifi:
|
if args.wifi:
|
||||||
opts.system_config.wifi_settings = [
|
opts.system_config.wifi_settings = [WifiConfig(ssid=ssid) for ssid in args.wifi]
|
||||||
WifiConfig(ssid=ssid, password=password)
|
|
||||||
for ssid, password in args.wifi.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
machine = Machine(opts.machine, flake=opts.flake)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
if opts.confirm and not opts.dry_run:
|
if opts.confirm and not opts.dry_run:
|
||||||
@@ -102,7 +97,6 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
dry_run=opts.dry_run,
|
dry_run=opts.dry_run,
|
||||||
debug=opts.debug,
|
debug=opts.debug,
|
||||||
write_efi_boot_entries=opts.write_efi_boot_entries,
|
write_efi_boot_entries=opts.write_efi_boot_entries,
|
||||||
no_udev=opts.no_udev,
|
|
||||||
extra_args=opts.nix_options,
|
extra_args=opts.nix_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -135,11 +129,9 @@ def register_flash_apply_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--wifi",
|
"--wifi",
|
||||||
type=str,
|
type=str,
|
||||||
nargs=2,
|
action="append",
|
||||||
metavar=("ssid", "password"),
|
help="wifi ssid to connect to",
|
||||||
action=AppendDiskAction,
|
default=[],
|
||||||
help="wifi network to connect to",
|
|
||||||
default={},
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mode",
|
"--mode",
|
||||||
@@ -160,12 +152,6 @@ def register_flash_apply_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=str,
|
type=str,
|
||||||
help="system language",
|
help="system language",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--no-udev",
|
|
||||||
help="Disable udev rules to block automounting",
|
|
||||||
default=False,
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--keymap",
|
"--keymap",
|
||||||
type=str,
|
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)
|
raise ClanError(msg, description=description)
|
||||||
|
|
||||||
# Get the service config without knowing instance name
|
# services.<service_name>.<instance_name>.config
|
||||||
service_conf = next((v for v in instance.values() if "config" in v), None)
|
config = next((v for v in instance.values() if "config" in v), None)
|
||||||
|
if not config:
|
||||||
if not service_conf:
|
|
||||||
msg = f"Service {service_name} in template inventory has no config"
|
msg = f"Service {service_name} in template inventory has no config"
|
||||||
description = "Invalid inventory configuration"
|
description = "Invalid inventory configuration"
|
||||||
raise ClanError(msg, description=description)
|
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"
|
msg = f"Service {service_name} in template inventory has machines"
|
||||||
description = "The 'machines' key is not allowed in template inventory"
|
description = "The 'machines' key is not allowed in template inventory"
|
||||||
raise ClanError(msg, description=description)
|
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"
|
msg = f"Service {service_name} in template inventory has no roles"
|
||||||
description = "roles key is required in template inventory"
|
description = "roles key is required in template inventory"
|
||||||
raise ClanError(msg, description=description)
|
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"
|
msg = "Merge template inventory is not implemented yet"
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ from dataclasses import dataclass
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from graphlib import TopologicalSorter
|
from graphlib import TopologicalSorter
|
||||||
|
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
|
|
||||||
from .check import check_vars
|
from .check import check_vars
|
||||||
|
|
||||||
|
|
||||||
|
class GeneratorNotFoundError(ClanError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Generator:
|
class Generator:
|
||||||
name: str
|
name: str
|
||||||
@@ -28,6 +33,11 @@ def missing_dependency_closure(
|
|||||||
queue = list(closure)
|
queue = list(closure)
|
||||||
while queue:
|
while queue:
|
||||||
gen_name = queue.pop(0)
|
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:
|
for dep in generators[gen_name].dependencies:
|
||||||
if dep not in closure and not generators[dep].exists:
|
if dep not in closure and not generators[dep].exists:
|
||||||
dep_closure.add(dep)
|
dep_closure.add(dep)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ clan_cli = [
|
|||||||
"templates/**/*",
|
"templates/**/*",
|
||||||
"vms/mimetypes/**/*",
|
"vms/mimetypes/**/*",
|
||||||
"webui/assets/**/*",
|
"webui/assets/**/*",
|
||||||
|
"flash/*.sh"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ export const Flash = () => {
|
|||||||
dry_run: false,
|
dry_run: false,
|
||||||
write_efi_boot_entries: false,
|
write_efi_boot_entries: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
no_udev: true,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(`Error could not flash disk: ${error}`);
|
toast.error(`Error could not flash disk: ${error}`);
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
clan-core,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
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