import argparse import logging import re from dataclasses import dataclass from clan_lib.api import API from clan_lib.dirs import get_clan_flake_toplevel_or_env from clan_lib.errors import ClanError from clan_lib.flake import Flake from clan_lib.git import commit_file from clan_lib.nix_models.clan import InventoryMachine from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.patch_engine import merge_objects from clan_lib.persist.path_utils import set_value_by_path from clan_lib.templates.handler import machine_template from clan_cli.completions import add_dynamic_completer, complete_tags log = logging.getLogger(__name__) @dataclass class CreateOptions: clan_dir: Flake machine: InventoryMachine template: str = "new-machine" target_host: str | None = None @API.register def create_machine( opts: CreateOptions, commit: bool = True, ) -> None: """Create a new machine in the clan directory. This function will create a new machine based on a template. :param opts: Options for creating the machine, including clan directory, machine details, and template name. :param commit: Whether to commit the changes to the git repository. :param _persist: Temporary workaround for 'morph'. Whether to persist the changes to the inventory store. """ if not opts.clan_dir.is_local: msg = f"Clan {opts.clan_dir} is not a local clan." description = "Import machine only works on local clans" raise ClanError(msg, description=description) clan_dir = opts.clan_dir.path machine_name = opts.machine.get("name") if not machine_name: msg = "Machine name is required" raise ClanError(msg, location="Create Machine") # TODO: Move this into nix code hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(? None: if args.flake: clan_dir = args.flake else: tmp = get_clan_flake_toplevel_or_env() clan_dir = Flake(str(tmp)) if tmp else None if not clan_dir: msg = "No clan found." description = ( "Run this command in a clan directory or specify the --flake option" ) raise ClanError(msg, description=description) machine = InventoryMachine( name=args.machine_name, tags=args.tags, deploy=MachineDeploy(targetHost=args.target_host), ) opts = CreateOptions( clan_dir=clan_dir, machine=machine, template=args.template, ) create_machine(opts) def register_create_parser(parser: argparse.ArgumentParser) -> None: parser.set_defaults(func=create_command) parser.add_argument( "machine_name", type=str, help="The name of the machine to create", ) tag_parser = parser.add_argument( "--tags", nargs="+", default=[], help="Tags to associate with the machine. Can be used to assign multiple machines to services.", ) add_dynamic_completer(tag_parser, complete_tags) parser.add_argument( "--target-host", type=str, help="Address of the machine to install and update, in the format of user@host:1234", ) parser.add_argument( "-t", "--template", type=str, help="""Reference to the template to use for the machine. default="new-machine". In the format '#template_name' Where is a flake reference (e.g. github:org/repo) or a local path (e.g. '.' ). Omitting '#' will use the builtin templates (e.g. just 'new-machine' from clan-core ). """, default="new-machine", )