Add machine validator with suggestion logic to: - `clan machines update` - `clan machines delete` - `clan machines update-hardware-config`
169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
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)
|