import argparse import logging import sys from clan_lib.async_run import AsyncContext, AsyncOpts, AsyncRuntime from clan_lib.errors import ClanError from clan_lib.machines.machines import Machine from clan_lib.machines.suggestions import validate_machine_names from clan_lib.machines.update import deploy_machine from clan_lib.nix import nix_config from clan_lib.ssh.remote import Remote from clan_cli.completions import ( add_dynamic_completer, complete_machines, complete_tags, ) from clan_cli.machines.list import list_full_machines, query_machines_by_tags log = logging.getLogger(__name__) def update_command(args: argparse.Namespace) -> None: try: if args.flake is None: msg = "Could not find clan flake toplevel directory" raise ClanError(msg) machines: list[Machine] = [] if args.tags: tag_filtered_machines = query_machines_by_tags(args.flake, args.tags) if args.machines: selected_machines = [ name for name in args.machines if name in tag_filtered_machines ] else: selected_machines = list(tag_filtered_machines.keys()) else: selected_machines = ( args.machines if args.machines else list(list_full_machines(args.flake).keys()) ) if args.tags and not selected_machines: msg = f"No machines found with tags: {', '.join(args.tags)}" raise ClanError(msg) if args.machines: validate_machine_names(args.machines, args.flake) for machine_name in selected_machines: machine = Machine(name=machine_name, flake=args.flake) machines.append(machine) if args.target_host is not None and len(machines) > 1: msg = "Target Host can only be set for one machines" raise ClanError(msg) def filter_machine(m: Machine) -> bool: if m.deployment.get("requireExplicitUpdate", False): return False try: # check if the machine has a target host set m.target_host # noqa: B018 except ClanError: return False return True machines_to_update = machines implicit_all: bool = len(args.machines) == 0 and not args.tags if implicit_all: machines_to_update = list(filter(filter_machine, machines)) # machines that are in the list but not included in the update list ignored_machines = {m.name for m in machines if m not in machines_to_update} if not machines_to_update and ignored_machines: print( "WARNING: No machines to update.\n" "The following defined machines were ignored because they\n" "- Require explicit update (see 'requireExplicitUpdate')\n", "- Might not have the `clan.core.networking.targetHost` nixos option set:\n", file=sys.stderr, ) for m in ignored_machines: print(m, file=sys.stderr) if machines_to_update: # Prepopulate the cache config = nix_config() system = config["system"] machine_names = [machine.name for machine in machines_to_update] args.flake.precache( [ f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.clan.core.vars.generators.*.validationHash", f"clanInternals.machines.{system}.{{{','.join(machine_names)}}}.config.system.clan.deployment.file", ] ) host_key_check = args.host_key_check with AsyncRuntime() as runtime: for machine in machines: if args.target_host: target_host = Remote.from_ssh_uri( machine_name=machine.name, address=args.target_host, ).override(host_key_check=host_key_check) else: target_host = machine.target_host().override( host_key_check=host_key_check ) runtime.async_run( AsyncOpts( tid=machine.name, async_ctx=AsyncContext(prefix=machine.name), ), deploy_machine, machine=machine, target_host=target_host, build_host=machine.build_host(), ) runtime.join_all() runtime.check_all() except KeyboardInterrupt: log.warning("Interrupted by user") sys.exit(1) def register_update_parser(parser: argparse.ArgumentParser) -> None: machines_parser = parser.add_argument( "machines", type=str, nargs="*", default=[], metavar="MACHINE", help="Machine to update. If no machines are specified, all machines that don't require explicit updates will be updated.", ) add_dynamic_completer(machines_parser, complete_machines) tag_parser = parser.add_argument( "--tags", nargs="+", default=[], help="Tags that machines should be queried for. Multiple tags will intersect.", ) add_dynamic_completer(tag_parser, complete_tags) parser.add_argument( "--host-key-check", choices=["strict", "ask", "tofu", "none"], default="ask", help="Host key (.ssh/known_hosts) check mode.", ) parser.add_argument( "--target-host", type=str, help="Address of the machine to update, in the format of user@host:1234.", ) parser.add_argument( "--build-host", type=str, help="Address of the machine to build the flake, in the format of user@host:1234.", ) parser.set_defaults(func=update_command)