Merge pull request 'add flash command and tests' (#916) from Mic92-main into main
All checks were successful
checks / check-links (push) Successful in 21s
checks / checks (push) Successful in 33s
checks / checks-impure (push) Successful in 1m56s

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

View File

@ -3,6 +3,7 @@
./impure/flake-module.nix
./backups/flake-module.nix
./installation/flake-module.nix
./flash/flake-module.nix
];
perSystem = { pkgs, lib, self', ... }: {
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 + "/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";
environment.etc."install-successful".text = "ok";
@ -92,22 +86,22 @@ in
testScript = ''
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({
"qemuFlags":
'-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',
"startCommand": startCommand,
} | args)
driver.machines.append(machine)
return machine
start_all()
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.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:
target.shutdown()
except BrokenPipeError:

View File

@ -258,7 +258,7 @@ class Driver:
self.machines = []
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:
raise ValueError(f"Unable to extract hostname from {container.name}")
name = name_match.group(1)

View File

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

View File

@ -48,8 +48,13 @@
cat /var/shared/qrcode.utf8
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 = {
disk = {
stick = {
@ -59,10 +64,10 @@
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02"; # for grub MBR
};
#boot = {
# size = "1M";
# type = "EF02"; # for grub MBR
#};
ESP = {
size = "100M";
type = "EF00";

View File

@ -1,51 +1,110 @@
import argparse
import importlib
import logging
import os
import shlex
import shutil
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from .cmd import Log, run
from .errors import ClanError
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__)
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)
secret_store = secrets_module.SecretStore(machine=machine)
generate_secrets(machine)
secret_store: SecretStoreBase = secrets_module.SecretStore(machine=machine)
with TemporaryDirectory() as tmpdir_:
tmpdir = Path(tmpdir_)
upload_dir_ = machine.secrets_upload_directory
upload_dir = machine.secrets_upload_directory
if upload_dir_.startswith("/"):
upload_dir_ = upload_dir_[1:]
upload_dir = tmpdir / upload_dir_
upload_dir.mkdir(parents=True)
secret_store.upload(upload_dir)
if upload_dir.startswith("/"):
local_dir = tmpdir / upload_dir[1:]
else:
local_dir = tmpdir / upload_dir
fs_image = machine.build_nix("config.system.clan.iso")
print(fs_image)
local_dir.mkdir(parents=True)
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
class FlashOptions:
flake: Path
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:
opts = FlashOptions(
flake=args.flake,
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)
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:
@ -55,8 +114,30 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="machine to install",
)
parser.add_argument(
"--device",
"--disk",
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)

View File

@ -63,6 +63,7 @@ class InstallOptions:
machine: str
target_host: str
kexec: str | None
confirm: bool
def install_command(args: argparse.Namespace) -> None:
@ -71,10 +72,16 @@ def install_command(args: argparse.Namespace) -> None:
machine=args.machine,
target_host=args.target_host,
kexec=args.kexec,
confirm=not args.yes,
)
machine = Machine(opts.machine, flake=opts.flake)
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)
@ -84,6 +91,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
type=str,
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(
"machine",
type=str,

View File

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

View File

@ -1,6 +1,6 @@
# mypy: ignore-errors
""" QEMU Monitor Protocol Python class """
"""QEMU Monitor Protocol Python class"""
# Copyright (C) 2009, 2010 Red Hat Inc.
#
# Authors:

View File

@ -12,7 +12,12 @@ let
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 {
clanName = "clan-core";