add flash command and tests #916
@ -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 =
|
||||
|
46
checks/flash/flake-module.nix
Normal file
46
checks/flash/flake-module.nix
Normal 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; };
|
||||
};
|
||||
};
|
||||
}
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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";
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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:
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user