From a5f181a43070afe1d917718761b086c334b652f1 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 28 Sep 2023 18:27:06 +0200 Subject: [PATCH] clan-cli: init vm command --- nixosModules/clanCore/vm.nix | 25 +++++- pkgs/clan-cli/clan_cli/__init__.py | 5 +- pkgs/clan-cli/clan_cli/vms/__init__.py | 21 +++++ pkgs/clan-cli/clan_cli/vms/create.py | 101 +++++++++++++++++++++++++ pkgs/clan-cli/clan_cli/vms/inspect.py | 38 ++++++++++ pkgs/clan-cli/default.nix | 3 + 6 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/vms/__init__.py create mode 100644 pkgs/clan-cli/clan_cli/vms/create.py create mode 100644 pkgs/clan-cli/clan_cli/vms/inspect.py diff --git a/nixosModules/clanCore/vm.nix b/nixosModules/clanCore/vm.nix index 26a874f22..d9d0a9516 100644 --- a/nixosModules/clanCore/vm.nix +++ b/nixosModules/clanCore/vm.nix @@ -1,4 +1,11 @@ -{ lib, config, options, ... }: +{ lib, config, pkgs, options, extendModules, modulesPath, ... }: +let + vmConfig = extendModules { + modules = [ + (modulesPath + "/virtualisation/qemu-vm.nix") + ]; + }; +in { options = { clan.virtualisation = { @@ -33,9 +40,19 @@ }; config = { - system.clan.vm.config = { - inherit (config.clan.virtualisation) cores graphics; - memory_size = config.clan.virtualisation.memorySize; + system.clan.vm = { + # for clan vm inspect + config = { + inherit (config.clan.virtualisation) cores graphics; + memory_size = config.clan.virtualisation.memorySize; + }; + # for clan vm create + create = pkgs.writeText "vm.json" (builtins.toJSON { + initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}"; + toplevel = vmConfig.config.system.build.toplevel; + regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; }); + inherit (config.clan.virtualisation) memorySize cores graphics; + }); }; virtualisation = lib.optionalAttrs (options.virtualisation ? cores) { diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index c4f59f5f9..bfbe083e8 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -3,7 +3,7 @@ import sys from types import ModuleType from typing import Optional -from . import config, create, machines, secrets, webui +from . import config, create, machines, secrets, vms, webui from .errors import ClanError from .ssh import cli as ssh_cli @@ -47,6 +47,9 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: parser_webui = subparsers.add_parser("webui", help="start webui") webui.register_parser(parser_webui) + parser_vms = subparsers.add_parser("vms", help="manage virtual machines") + vms.register_parser(parser_vms) + if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/vms/__init__.py b/pkgs/clan-cli/clan_cli/vms/__init__.py new file mode 100644 index 000000000..6fb5db731 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/vms/__init__.py @@ -0,0 +1,21 @@ +import argparse + +from .create import register_create_parser +from .inspect import register_inspect_parser + + +def register_parser(parser: argparse.ArgumentParser) -> None: + subparser = parser.add_subparsers( + title="command", + description="command to execute", + help="the command to execute", + required=True, + ) + + inspect_parser = subparser.add_parser( + "inspect", help="inspect the vm configuration" + ) + register_inspect_parser(inspect_parser) + + create_parser = subparser.add_parser("create", help="create a VM from a machine") + register_create_parser(create_parser) diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py new file mode 100644 index 000000000..93ffa6b58 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -0,0 +1,101 @@ +import argparse +import json +import subprocess +import tempfile +from pathlib import Path + +from ..dirs import get_clan_flake_toplevel +from ..nix import nix_build, nix_shell + + +def get_vm_create_info(machine: str) -> dict: + clan_dir = get_clan_flake_toplevel().as_posix() + + # config = nix_config() + # system = config["system"] + + vm_json = subprocess.run( + nix_build( + [ + # f'{clan_dir}#clanInternals.machines."{system}"."{machine}".config.clan.virtualisation.createJSON' # TODO use this + f'{clan_dir}#nixosConfigurations."{machine}".config.system.clan.vm.create' + ] + ), + stdout=subprocess.PIPE, + check=True, + text=True, + ).stdout.strip() + with open(vm_json) as f: + return json.load(f) + + +def create(args: argparse.Namespace) -> None: + print(f"Creating VM for {args.machine}") + machine = args.machine + vm_config = get_vm_create_info(machine) + with tempfile.TemporaryDirectory() as tmpdir_: + xchg_dir = Path(tmpdir_) / "xchg" + xchg_dir.mkdir() + disk_img = f"{tmpdir_}/disk.img" + subprocess.run( + nix_shell( + ["qemu"], + [ + "qemu-img", + "create", + "-f", + "raw", + disk_img, + "1024M", + ], + ), + stdout=subprocess.PIPE, + check=True, + text=True, + ) + subprocess.run( + [ + "mkfs.ext4", + "-L", + "nixos", + disk_img, + ], + stdout=subprocess.PIPE, + check=True, + text=True, + ) + + subprocess.run( + nix_shell( + ["qemu"], + [ + # fmt: off + "qemu-kvm", + "-name", machine, + "-m", f'{vm_config["memorySize"]}M', + "-smp", str(vm_config["cores"]), + "-device", "virtio-rng-pci", + "-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0", + "-virtfs", "local,path=/nix/store,security_model=none,mount_tag=nix-store", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=shared", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg", + "-drive", f'cache=writeback,file={disk_img},format=raw,id=drive1,if=none,index=1,werror=report', + "-device", "virtio-blk-pci,bootindex=1,drive=drive1,serial=root", + "-device", "virtio-keyboard", + "-usb", + "-device", "usb-tablet,bus=usb-bus.0", + "-kernel", f'{vm_config["toplevel"]}/kernel', + "-initrd", vm_config["initrd"], + "-append", f'{(Path(vm_config["toplevel"]) / "kernel-params").read_text()} init={vm_config["toplevel"]}/init regInfo={vm_config["regInfo"]}/registration console=ttyS0,115200n8 console=tty0', + # fmt: on + ], + ), + stdout=subprocess.PIPE, + check=True, + text=True, + ) + + +def register_create_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("machine", type=str) + parser.set_defaults(func=create) diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py new file mode 100644 index 000000000..67e5fedc8 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -0,0 +1,38 @@ +import argparse +import json +import subprocess + +from ..dirs import get_clan_flake_toplevel +from ..nix import nix_eval + + +def get_vm_inspect_info(machine: str) -> dict: + clan_dir = get_clan_flake_toplevel().as_posix() + + # config = nix_config() + # system = config["system"] + + return json.loads( + subprocess.run( + nix_eval( + [ + # f'{clan_dir}#clanInternals.machines."{system}"."{machine}".config.clan.virtualisation' # TODO use this + f'{clan_dir}#nixosConfigurations."{machine}".config.system.clan.vm.config' + ] + ), + stdout=subprocess.PIPE, + check=True, + text=True, + ).stdout + ) + + +def inspect(args: argparse.Namespace) -> None: + print(f"Creating VM for {args.machine}") + machine = args.machine + print(get_vm_inspect_info(machine)) + + +def register_inspect_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument("machine", type=str) + parser.set_defaults(func=inspect) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index a6dccf1d0..89fea0545 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -27,6 +27,8 @@ , nixpkgs , makeDesktopItem , copyDesktopItems +, qemu +, gnupg }: let @@ -59,6 +61,7 @@ let rsync sops git + qemu ]; runtimeDependenciesAsSet = builtins.listToAttrs (builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies);