diff --git a/pkgs/clan-cli/clan_cli/cli.py b/pkgs/clan-cli/clan_cli/cli.py index 0f352d036..7b7001c47 100644 --- a/pkgs/clan-cli/clan_cli/cli.py +++ b/pkgs/clan-cli/clan_cli/cli.py @@ -198,7 +198,7 @@ This subcommand provides an interface to templates provided by clan. Examples: $ clan templates list - List all the machines managed by Clan. + List all available templates Usage differs based on the template type @@ -227,6 +227,16 @@ Disk templates Real world example $ clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368" + +--- + +Machine templates + + $ clan templates apply machine [TEMPLATE] [MACHINE_NAME] + Will create a new machine [MACHINE_NAME] from the specified [TEMPLATE] + + Real world example + $ clan templates apply machine flash-installer my-installer """ ), formatter_class=argparse.RawTextHelpFormatter, diff --git a/pkgs/clan-cli/clan_cli/completions.py b/pkgs/clan-cli/clan_cli/completions.py index 2d64f1ab7..de653f699 100644 --- a/pkgs/clan-cli/clan_cli/completions.py +++ b/pkgs/clan-cli/clan_cli/completions.py @@ -303,6 +303,27 @@ def complete_templates_clan( return [] +def complete_templates_machine( + _prefix: str, + parsed_args: argparse.Namespace, + **_kwargs: Any, +) -> Iterable[str]: + """Provides completion functionality for machine templates""" + flake = ( + clan_dir_result + if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None))) + is not None + else "." + ) + + list_all_templates = list_templates(Flake(flake)) + machine_template_list = list_all_templates.builtins.get("machine") + if machine_template_list: + machine_templates = list(machine_template_list) + return dict.fromkeys(machine_templates, "machine") + return [] + + def complete_vars_for_machine( _prefix: str, parsed_args: argparse.Namespace, diff --git a/pkgs/clan-cli/clan_cli/templates/apply.py b/pkgs/clan-cli/clan_cli/templates/apply.py index d5113853c..cfe4da9d7 100644 --- a/pkgs/clan-cli/clan_cli/templates/apply.py +++ b/pkgs/clan-cli/clan_cli/templates/apply.py @@ -1,6 +1,7 @@ import argparse from .apply_disk import register_apply_disk_template_parser +from .apply_machine import register_apply_machine_template_parser def register_apply_parser(parser: argparse.ArgumentParser) -> None: @@ -11,5 +12,7 @@ def register_apply_parser(parser: argparse.ArgumentParser) -> None: required=True, ) disk_parser = subparser.add_parser("disk", help="Apply a disk template") + machine_parser = subparser.add_parser("machine", help="Apply a machine template") register_apply_disk_template_parser(disk_parser) + register_apply_machine_template_parser(machine_parser) diff --git a/pkgs/clan-cli/clan_cli/templates/apply_machine.py b/pkgs/clan-cli/clan_cli/templates/apply_machine.py new file mode 100644 index 000000000..31e7af501 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/templates/apply_machine.py @@ -0,0 +1,42 @@ +import argparse +import logging + +from clan_lib.nix_models.clan import InventoryMachine +from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy + +from clan_cli.machines.create import CreateOptions, create_machine + +log = logging.getLogger(__name__) + + +def apply_command(args: argparse.Namespace) -> None: + """Apply a machine template - actually an alias for machines create --template.""" + # Create machine using the create_machine API directly + machine = InventoryMachine( + name=args.machine, + tags=[], + deploy=MachineDeploy(targetHost=None), + ) + + opts = CreateOptions( + clan_dir=args.flake, + machine=machine, + template=args.template, + ) + + create_machine(opts) + + +def register_apply_machine_template_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "template", + type=str, + help="The name of the machine template to apply", + ) + parser.add_argument( + "machine", + type=str, + help="The name of the machine to create from the template", + ) + + parser.set_defaults(func=apply_command) diff --git a/pkgs/clan-cli/clan_cli/templates/apply_test.py b/pkgs/clan-cli/clan_cli/templates/apply_test.py new file mode 100644 index 000000000..86c5aa9af --- /dev/null +++ b/pkgs/clan-cli/clan_cli/templates/apply_test.py @@ -0,0 +1,79 @@ +import json + +import pytest +from clan_lib.errors import ClanError +from clan_lib.flake import Flake +from clan_lib.machines.machines import Machine +from clan_lib.templates.disk import set_machine_disk_schema + +from clan_cli.tests.fixtures_flakes import FlakeForTest +from clan_cli.tests.helpers import cli + + +@pytest.mark.with_core +def test_templates_apply_machine_and_disk( + test_flake_with_core: FlakeForTest, +) -> None: + """Test both machine template creation and disk template application.""" + flake_path = str(test_flake_with_core.path) + + cli.run( + [ + "templates", + "apply", + "machine", + "new-machine", + "test-apply-machine", + "--flake", + flake_path, + ] + ) + + # Verify machine was created + machine_dir = test_flake_with_core.path / "machines" / "test-apply-machine" + assert machine_dir.exists(), "Machine directory should be created" + assert (machine_dir / "configuration.nix").exists(), ( + "Configuration file should exist" + ) + + facter_content = { + "disks": [ + { + "name": "test-disk", + "path": "/dev/sda", + "size": 107374182400, + "type": "disk", + } + ] + } + + facter_path = machine_dir / "facter.json" + facter_path.write_text(json.dumps(facter_content, indent=2)) + + machine = Machine(name="test-apply-machine", flake=Flake(flake_path)) + set_machine_disk_schema( + machine, + "single-disk", + {"mainDisk": "/dev/sda"}, + force=False, + check_hw=False, # Skip hardware validation for test + ) + + # Verify disk template was applied by checking that disko.nix exists or was updated + disko_file = machine_dir / "disko.nix" + assert disko_file.exists(), "Disko configuration should be created" + + # Verify error handling - try to create duplicate machine + # Since apply machine now uses machines create, it raises ClanError for duplicates + with pytest.raises(ClanError, match="already exists"): + cli.run( + [ + "templates", + "apply", + "machine", + "new-machine", + "test-apply-machine", # Same name as existing + "--flake", + flake_path, + ] + )