Merge pull request 'add flash command and tests' (#916) from Mic92-main into main

This commit is contained in:
clan-bot
2024-03-07 16:29:11 +00:00
11 changed files with 194 additions and 48 deletions

View File

@@ -3,6 +3,7 @@
./impure/flake-module.nix ./impure/flake-module.nix
./backups/flake-module.nix ./backups/flake-module.nix
./installation/flake-module.nix ./installation/flake-module.nix
./flash/flake-module.nix
]; ];
perSystem = { pkgs, lib, self', ... }: { perSystem = { pkgs, lib, self', ... }: {
checks = checks =

View File

@@ -0,0 +1,46 @@
{ self, ... }:
{
perSystem = { nodes, pkgs, lib, ... }:
let
dependencies = [
self
pkgs.stdenv.drvPath
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.toplevel
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.clan.deployment.file
self.inputs.nixpkgs.legacyPackages.${pkgs.hostPlatform.system}.disko
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
flash =
(import ../lib/test-base.nix)
{
name = "flash";
nodes.target = {
virtualisation.emptyDiskImages = [ 4096 ];
virtualisation.memorySize = 3000;
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
environment.etc."install-closure".source = "${closureInfo}/store-paths";
nix.settings = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = lib.mkForce 3;
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
experimental-features = [
"nix-command"
"flakes"
];
};
};
testScript = ''
start_all()
machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine")
'';
}
{ inherit pkgs self; };
};
};
}

View File

@@ -21,12 +21,6 @@ in
(modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests (modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
(modulesPath + "/profiles/qemu-guest.nix") (modulesPath + "/profiles/qemu-guest.nix")
]; ];
fileSystems."/nix/store" = lib.mkForce {
device = "nix-store";
fsType = "9p";
neededForBoot = true;
options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
};
clan.diskLayouts.singleDiskExt4.device = "/dev/vdb"; clan.diskLayouts.singleDiskExt4.device = "/dev/vdb";
environment.etc."install-successful".text = "ok"; environment.etc."install-successful".text = "ok";
@@ -92,22 +86,22 @@ in
testScript = '' testScript = ''
def create_test_machine(oldmachine=None, args={}): # taken from <nixpkgs/nixos/tests/installer.nix> def create_test_machine(oldmachine=None, args={}): # taken from <nixpkgs/nixos/tests/installer.nix>
startCommand = "${pkgs.qemu_test}/bin/qemu-kvm"
startCommand += " -cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store"
startCommand += f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report'
startCommand += ' -device virtio-blk-pci,drive=drive1'
machine = create_machine({ machine = create_machine({
"qemuFlags": "startCommand": startCommand,
'-cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store,'
f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report'
f' -device virtio-blk-pci,drive=drive1',
} | args) } | args)
driver.machines.append(machine) driver.machines.append(machine)
return machine return machine
start_all() start_all()
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519") client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname") client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
client.succeed("clan --debug --flake ${../..} machines install test_install_machine root@target >&2") client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2")
try: try:
target.shutdown() target.shutdown()
except BrokenPipeError: except BrokenPipeError:

View File

@@ -258,7 +258,7 @@ class Driver:
self.machines = [] self.machines = []
for container in containers: for container in containers:
name_match = re.match(r".*-nixos-system-(.+)-\d.+", container.name) name_match = re.match(r".*-nixos-system-(.+)-(.+)", container.name)
if not name_match: if not name_match:
raise ValueError(f"Unable to extract hostname from {container.name}") raise ValueError(f"Unable to extract hostname from {container.name}")
name = name_match.group(1) name = name_match.group(1)

6
flake.lock generated
View File

@@ -78,11 +78,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1708847675, "lastModified": 1709764733,
"narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=", "narHash": "sha256-GptBnEUy8IcRrnd8X5WBJPDXG7M4bjj8OG4Wjg8dCDs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2a34566b67bef34c551f204063faeecc444ae9da", "rev": "edf9f14255a7ac20f8da7b70609e980a964fca7a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -48,8 +48,13 @@
cat /var/shared/qrcode.utf8 cat /var/shared/qrcode.utf8
fi fi
''; '';
boot.loader.grub.efiInstallAsRemovable = true;
boot.loader.grub.efiSupport = true; boot.loader.systemd-boot.enable = true;
# Grub doesn't find devices for both BIOS and UEFI?
#boot.loader.grub.efiInstallAsRemovable = true;
#boot.loader.grub.efiSupport = true;
disko.devices = { disko.devices = {
disk = { disk = {
stick = { stick = {
@@ -59,10 +64,10 @@
content = { content = {
type = "gpt"; type = "gpt";
partitions = { partitions = {
boot = { #boot = {
size = "1M"; # size = "1M";
type = "EF02"; # for grub MBR # type = "EF02"; # for grub MBR
}; #};
ESP = { ESP = {
size = "100M"; size = "100M";
type = "EF00"; type = "EF00";

View File

@@ -1,51 +1,110 @@
import argparse import argparse
import importlib import importlib
import logging import logging
import os
import shlex
import shutil
from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any
from .cmd import Log, run
from .errors import ClanError
from .machines.machines import Machine from .machines.machines import Machine
from .secrets.generate import generate_secrets from .nix import nix_shell
from .secrets.modules import SecretStoreBase
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def flash_machine(machine: Machine, device: str | None = None) -> None: def flash_machine(
machine: Machine, disks: dict[str, str], dry_run: bool, debug: bool
) -> None:
secrets_module = importlib.import_module(machine.secrets_module) secrets_module = importlib.import_module(machine.secrets_module)
secret_store = secrets_module.SecretStore(machine=machine) secret_store: SecretStoreBase = secrets_module.SecretStore(machine=machine)
generate_secrets(machine)
with TemporaryDirectory() as tmpdir_: with TemporaryDirectory() as tmpdir_:
tmpdir = Path(tmpdir_) tmpdir = Path(tmpdir_)
upload_dir_ = machine.secrets_upload_directory upload_dir = machine.secrets_upload_directory
if upload_dir_.startswith("/"): if upload_dir.startswith("/"):
upload_dir_ = upload_dir_[1:] local_dir = tmpdir / upload_dir[1:]
upload_dir = tmpdir / upload_dir_ else:
upload_dir.mkdir(parents=True) local_dir = tmpdir / upload_dir
secret_store.upload(upload_dir)
fs_image = machine.build_nix("config.system.clan.iso") local_dir.mkdir(parents=True)
print(fs_image) secret_store.upload(local_dir)
disko_install = []
if os.geteuid() != 0:
if shutil.which("sudo") is None:
raise ClanError(
"sudo is required to run disko-install as a non-root user"
)
disko_install.append("sudo")
disko_install.append("disko-install")
if dry_run:
disko_install.append("--dry-run")
if debug:
disko_install.append("--debug")
for name, device in disks.items():
disko_install.extend(["--disk", name, device])
disko_install.extend(["--extra-files", str(local_dir), upload_dir])
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
cmd = nix_shell(
["nixpkgs#disko"],
disko_install,
)
print("$", " ".join(map(shlex.quote, cmd)))
run(cmd, log=Log.BOTH, error_msg=f"Failed to flash {machine}")
@dataclass @dataclass
class FlashOptions: class FlashOptions:
flake: Path flake: Path
machine: str machine: str
device: str | None disks: dict[str, str]
dry_run: bool
confirm: bool
debug: bool
class AppendDiskAction(argparse.Action):
def __init__(self, option_strings: str, dest: str, **kwargs: Any) -> None:
super().__init__(option_strings, dest, **kwargs)
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[str] | None,
option_string: str | None = None,
) -> None:
disks = getattr(namespace, self.dest)
assert isinstance(values, list), "values must be a list"
disks[values[0]] = values[1]
def flash_command(args: argparse.Namespace) -> None: def flash_command(args: argparse.Namespace) -> None:
opts = FlashOptions( opts = FlashOptions(
flake=args.flake, flake=args.flake,
machine=args.machine, machine=args.machine,
device=args.device, disks=args.disk,
dry_run=args.dry_run,
confirm=not args.yes,
debug=args.debug,
) )
machine = Machine(opts.machine, flake=opts.flake) machine = Machine(opts.machine, flake=opts.flake)
flash_machine(machine, device=opts.device) if opts.confirm and not opts.dry_run:
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
ask = input(f"Install {machine.name} to {disk_str}? [y/N] ")
if ask != "y":
return
flash_machine(machine, disks=opts.disks, dry_run=opts.dry_run, debug=opts.debug)
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
@@ -55,8 +114,30 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="machine to install", help="machine to install",
) )
parser.add_argument( parser.add_argument(
"--device", "--disk",
type=str, type=str,
help="device to flash the system to", nargs=2,
metavar=("name", "value"),
action=AppendDiskAction,
help="device to flash to",
default={},
)
parser.add_argument(
"--yes",
action="store_true",
help="do not ask for confirmation",
default=False,
)
parser.add_argument(
"--dry-run",
help="Only build the system, don't flash it",
default=False,
action="store_true",
)
parser.add_argument(
"--debug",
help="Print debug information",
default=False,
action="store_true",
) )
parser.set_defaults(func=flash_command) parser.set_defaults(func=flash_command)

View File

@@ -63,6 +63,7 @@ class InstallOptions:
machine: str machine: str
target_host: str target_host: str
kexec: str | None kexec: str | None
confirm: bool
def install_command(args: argparse.Namespace) -> None: def install_command(args: argparse.Namespace) -> None:
@@ -71,10 +72,16 @@ def install_command(args: argparse.Namespace) -> None:
machine=args.machine, machine=args.machine,
target_host=args.target_host, target_host=args.target_host,
kexec=args.kexec, kexec=args.kexec,
confirm=not args.yes,
) )
machine = Machine(opts.machine, flake=opts.flake) machine = Machine(opts.machine, flake=opts.flake)
machine.target_host_address = opts.target_host machine.target_host_address = opts.target_host
if opts.confirm:
ask = input(f"Install {machine.name} to {opts.target_host}? [y/N] ")
if ask != "y":
return
install_nixos(machine, kexec=opts.kexec) install_nixos(machine, kexec=opts.kexec)
@@ -84,6 +91,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
type=str, type=str,
help="use another kexec tarball to bootstrap NixOS", help="use another kexec tarball to bootstrap NixOS",
) )
parser.add_argument(
"--yes",
action="store_true",
help="do not ask for confirmation",
default=False,
)
parser.add_argument( parser.add_argument(
"machine", "machine",
type=str, type=str,

View File

@@ -174,12 +174,13 @@ def run_vm(
if vm.graphics and not vm.waypipe: if vm.graphics and not vm.waypipe:
packages.append("nixpkgs#virt-viewer") packages.append("nixpkgs#virt-viewer")
remote_viewer_mimetypes = module_root() / "vms" / "mimetypes" remote_viewer_mimetypes = module_root() / "vms" / "mimetypes"
env[ env["XDG_DATA_DIRS"] = (
"XDG_DATA_DIRS" f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}"
] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" )
with start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "), start_virtiofsd( with (
virtiofsd_socket start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "),
start_virtiofsd(virtiofsd_socket),
): ):
run( run(
nix_shell(packages, qemu_cmd.args), nix_shell(packages, qemu_cmd.args),

View File

@@ -12,7 +12,12 @@ let
nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux; nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux;
}; };
installer = lib.nixosSystem { modules = [ installerModule ]; }; installer = lib.nixosSystem {
modules = [
installerModule
{ disko.memSize = 4096; } # FIXME: otherwise the image builder goes OOM
];
};
clan = self.lib.buildClan { clan = self.lib.buildClan {
clanName = "clan-core"; clanName = "clan-core";