diff --git a/nixosModules/clanCore/outputs.nix b/nixosModules/clanCore/outputs.nix index 54d81f69..d3411599 100644 --- a/nixosModules/clanCore/outputs.nix +++ b/nixosModules/clanCore/outputs.nix @@ -69,6 +69,12 @@ json metadata about the vm ''; }; + iso = lib.mkOption { + type = lib.types.path; + description = '' + A generated iso of the machine for the flash command + ''; + }; }; }; description = '' @@ -84,6 +90,5 @@ inherit (config.clanCore) secretsUploadDirectory; }; system.clan.deployment.file = pkgs.writeText "deployment.json" (builtins.toJSON config.system.clan.deployment.data); - }; } diff --git a/nixosModules/clanCore/zerotier/default.nix b/nixosModules/clanCore/zerotier/default.nix index e3e1f027..768b0b50 100644 --- a/nixosModules/clanCore/zerotier/default.nix +++ b/nixosModules/clanCore/zerotier/default.nix @@ -79,7 +79,6 @@ in type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; }; - default = { }; }; }; config = lib.mkMerge [ diff --git a/nixosModules/flake-module.nix b/nixosModules/flake-module.nix index 96b264b5..4fb480ea 100644 --- a/nixosModules/flake-module.nix +++ b/nixosModules/flake-module.nix @@ -5,6 +5,7 @@ clanCore.imports = [ inputs.sops-nix.nixosModules.sops ./clanCore + ./iso ({ pkgs, lib, ... }: { clanCore.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system}; }) diff --git a/nixosModules/iso/default.nix b/nixosModules/iso/default.nix new file mode 100644 index 00000000..22176693 --- /dev/null +++ b/nixosModules/iso/default.nix @@ -0,0 +1,84 @@ +{ config, extendModules, lib, pkgs, ... }: +let + # Generates a fileSystems entry for bind mounting a given state folder path + # It binds directories from /var/clanstate/{some-path} to /{some-path}. + # As a result, all state paths will be persisted across reboots, because + # the state folder is mounted from the host system. + mkBindMount = path: { + name = path; + value = { + device = "/var/clanstate/${path}"; + options = [ "bind" ]; + }; + }; + + # Flatten the list of state folders into a single list + stateFolders = lib.flatten ( + lib.mapAttrsToList + (_item: attrs: attrs.folders) + config.clanCore.state + ); + + # A module setting up bind mounts for all state folders + stateMounts = { + virtualisation.fileSystems = + lib.listToAttrs + (map mkBindMount stateFolders); + }; + + isoModule = { config, ... }: { + imports = [ + stateMounts + ]; + options.clan.iso.disko = lib.mkOption { + type = lib.types.submodule { + freeformType = (pkgs.formats.json { }).type; + }; + default = { + disk = { + iso = { + type = "disk"; + imageSize = "10G"; # TODO add auto image size in disko + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR + }; + ESP = { + size = "100M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; + }; + config.disko.devices = lib.mkOverride 51 config.clan.iso.disko; + }; + + isoConfig = extendModules { + modules = [ isoModule ]; + }; +in +{ + config = { + # for clan vm create + system.clan.iso = isoConfig.config.system.build.diskoImages; + }; +} diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index afc9adf3..88201103 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from types import ModuleType from typing import Any -from . import backups, config, flakes, history, machines, secrets, vms +from . import backups, config, flakes, flash, history, machines, secrets, vms from .custom_logger import setup_logging from .dirs import get_clan_flake_toplevel from .errors import ClanCmdError, ClanError @@ -102,6 +102,11 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser: parser_history = subparsers.add_parser("history", help="manage history") history.register_parser(parser_history) + parser_flash = subparsers.add_parser( + "flash", help="flash machines to usb sticks or into isos" + ) + flash.register_parser(parser_flash) + if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/flash.py b/pkgs/clan-cli/clan_cli/flash.py new file mode 100644 index 00000000..e99edb76 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/flash.py @@ -0,0 +1,62 @@ +import argparse +import importlib +import logging +from dataclasses import dataclass +from pathlib import Path +from tempfile import TemporaryDirectory + +from .machines.machines import Machine +from .secrets.generate import generate_secrets + +log = logging.getLogger(__name__) + + +def flash_machine(machine: Machine, device: str | None = None) -> None: + secrets_module = importlib.import_module(machine.secrets_module) + secret_store = secrets_module.SecretStore(machine=machine) + + generate_secrets(machine) + + with TemporaryDirectory() as tmpdir_: + tmpdir = Path(tmpdir_) + 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) + + fs_image = machine.build_nix("config.system.clan.iso") + print(fs_image) + + +@dataclass +class FlashOptions: + flake: Path + machine: str + device: str | None + + +def flash_command(args: argparse.Namespace) -> None: + opts = FlashOptions( + flake=args.flake, + machine=args.machine, + device=args.device, + ) + machine = Machine(opts.machine, flake=opts.flake) + flash_machine(machine, device=opts.device) + + +def register_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "machine", + type=str, + help="machine to install", + ) + parser.add_argument( + "--device", + type=str, + help="device to flash the system to", + ) + parser.set_defaults(func=flash_command)