2023-08-10 10:30:52 +00:00
|
|
|
import argparse
|
|
|
|
import json
|
2024-02-03 06:52:30 +00:00
|
|
|
import logging
|
2023-09-13 18:33:51 +00:00
|
|
|
import os
|
2024-02-03 06:52:30 +00:00
|
|
|
import shlex
|
2024-02-06 13:45:21 +00:00
|
|
|
import sys
|
2023-08-10 10:30:52 +00:00
|
|
|
|
2024-06-02 08:00:19 +00:00
|
|
|
from ..cmd import run
|
2024-05-30 17:51:53 +00:00
|
|
|
from ..completions import add_dynamic_completer, complete_machines
|
2023-11-03 11:51:17 +00:00
|
|
|
from ..errors import ClanError
|
2024-03-23 04:05:31 +00:00
|
|
|
from ..facts.generate import generate_facts
|
|
|
|
from ..facts.upload import upload_secrets
|
2023-10-04 13:32:04 +00:00
|
|
|
from ..machines.machines import Machine
|
2024-04-15 19:06:03 +00:00
|
|
|
from ..nix import nix_command, nix_metadata
|
|
|
|
from ..ssh import HostKeyCheck
|
|
|
|
from .inventory import get_all_machines, get_selected_machines
|
|
|
|
from .machine_group import MachineGroup
|
2023-08-10 10:30:52 +00:00
|
|
|
|
2024-02-03 06:52:30 +00:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
def is_path_input(node: dict[str, dict[str, str]]) -> bool:
|
|
|
|
locked = node.get("locked")
|
|
|
|
if not locked:
|
|
|
|
return False
|
|
|
|
return locked["type"] == "path" or locked.get("url", "").startswith("file://")
|
|
|
|
|
|
|
|
|
|
|
|
def upload_sources(
|
|
|
|
flake_url: str, remote_url: str, always_upload_source: bool = False
|
|
|
|
) -> str:
|
|
|
|
if not always_upload_source:
|
|
|
|
flake_data = nix_metadata(flake_url)
|
|
|
|
url = flake_data["resolvedUrl"]
|
|
|
|
has_path_inputs = any(
|
|
|
|
is_path_input(node) for node in flake_data["locks"]["nodes"].values()
|
|
|
|
)
|
|
|
|
if not has_path_inputs and not is_path_input(flake_data):
|
|
|
|
# No need to upload sources, we can just build the flake url directly
|
|
|
|
# FIXME: this might fail for private repositories?
|
|
|
|
return url
|
|
|
|
if not has_path_inputs:
|
|
|
|
# Just copy the flake to the remote machine, we can substitute other inputs there.
|
|
|
|
path = flake_data["path"]
|
|
|
|
env = os.environ.copy()
|
|
|
|
# env["NIX_SSHOPTS"] = " ".join(opts.remote_ssh_options)
|
|
|
|
assert remote_url
|
|
|
|
cmd = nix_command(
|
|
|
|
[
|
|
|
|
"copy",
|
|
|
|
"--to",
|
|
|
|
f"ssh://{remote_url}",
|
|
|
|
"--no-check-sigs",
|
|
|
|
path,
|
|
|
|
]
|
|
|
|
)
|
2024-06-02 08:00:19 +00:00
|
|
|
run(cmd, env=env, error_msg="failed to upload sources")
|
2024-02-03 06:52:30 +00:00
|
|
|
return path
|
|
|
|
|
|
|
|
# Slow path: we need to upload all sources to the remote machine
|
|
|
|
assert remote_url
|
|
|
|
cmd = nix_command(
|
|
|
|
[
|
|
|
|
"flake",
|
|
|
|
"archive",
|
|
|
|
"--to",
|
|
|
|
f"ssh://{remote_url}",
|
|
|
|
"--json",
|
|
|
|
flake_url,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
log.info("run %s", shlex.join(cmd))
|
2024-06-02 08:00:19 +00:00
|
|
|
proc = run(cmd, error_msg="failed to upload sources")
|
|
|
|
|
2024-02-03 06:52:30 +00:00
|
|
|
try:
|
|
|
|
return json.loads(proc.stdout)["path"]
|
|
|
|
except (json.JSONDecodeError, OSError) as e:
|
|
|
|
raise ClanError(
|
2024-06-02 08:00:19 +00:00
|
|
|
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}"
|
2024-02-03 06:52:30 +00:00
|
|
|
)
|
2023-08-10 10:30:52 +00:00
|
|
|
|
2024-02-03 06:52:52 +00:00
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
def deploy_nixos(machines: MachineGroup) -> None:
|
2023-08-10 10:30:52 +00:00
|
|
|
"""
|
|
|
|
Deploy to all hosts in parallel
|
|
|
|
"""
|
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
def deploy(machine: Machine) -> None:
|
|
|
|
host = machine.build_host
|
|
|
|
target = f"{host.user or 'root'}@{host.host}"
|
|
|
|
ssh_arg = f"-p {host.port}" if host.port else ""
|
2023-09-13 18:33:51 +00:00
|
|
|
env = os.environ.copy()
|
|
|
|
env["NIX_SSHOPTS"] = ssh_arg
|
2024-04-12 12:38:21 +00:00
|
|
|
|
2024-05-26 20:55:48 +00:00
|
|
|
generate_facts([machine], None, False)
|
2024-04-12 12:38:21 +00:00
|
|
|
upload_secrets(machine)
|
|
|
|
|
2024-02-03 06:52:30 +00:00
|
|
|
path = upload_sources(".", target)
|
2023-08-10 10:30:52 +00:00
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
if host.host_key_check != HostKeyCheck.STRICT:
|
2023-08-10 10:30:52 +00:00
|
|
|
ssh_arg += " -o StrictHostKeyChecking=no"
|
2024-04-15 19:06:03 +00:00
|
|
|
if host.host_key_check == HostKeyCheck.NONE:
|
2023-08-10 10:30:52 +00:00
|
|
|
ssh_arg += " -o UserKnownHostsFile=/dev/null"
|
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
ssh_arg += " -i " + host.key if host.key else ""
|
2023-08-10 10:30:52 +00:00
|
|
|
|
2023-11-29 11:38:00 +00:00
|
|
|
cmd = [
|
|
|
|
"nixos-rebuild",
|
|
|
|
"switch",
|
|
|
|
"--fast",
|
|
|
|
"--option",
|
|
|
|
"keep-going",
|
|
|
|
"true",
|
|
|
|
"--option",
|
|
|
|
"accept-flake-config",
|
|
|
|
"true",
|
|
|
|
"--build-host",
|
|
|
|
"",
|
2024-05-29 07:08:23 +00:00
|
|
|
*machine.nix_options,
|
2023-11-29 11:38:00 +00:00
|
|
|
"--flake",
|
2024-02-06 14:44:07 +00:00
|
|
|
f"{path}#{machine.name}",
|
2023-11-29 11:38:00 +00:00
|
|
|
]
|
2024-04-15 19:06:03 +00:00
|
|
|
if target_host := host.meta.get("target_host"):
|
2024-02-06 15:11:55 +00:00
|
|
|
target_host = f"{target_host.user or 'root'}@{target_host.host}"
|
2023-08-10 10:30:52 +00:00
|
|
|
cmd.extend(["--target-host", target_host])
|
2024-04-15 19:06:03 +00:00
|
|
|
ret = host.run(cmd, check=False)
|
2023-08-10 10:30:52 +00:00
|
|
|
# re-retry switch if the first time fails
|
|
|
|
if ret.returncode != 0:
|
2024-04-15 19:06:03 +00:00
|
|
|
ret = host.run(cmd)
|
2023-09-28 12:13:23 +00:00
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
machines.run_function(deploy)
|
2023-09-28 12:13:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
def update(args: argparse.Namespace) -> None:
|
2023-10-29 19:48:09 +00:00
|
|
|
if args.flake is None:
|
2023-11-03 11:51:17 +00:00
|
|
|
raise ClanError("Could not find clan flake toplevel directory")
|
2024-04-15 19:06:03 +00:00
|
|
|
machines = []
|
2023-10-04 21:28:15 +00:00
|
|
|
if len(args.machines) == 1 and args.target_host is not None:
|
2024-05-29 07:08:23 +00:00
|
|
|
machine = Machine(
|
|
|
|
name=args.machines[0], flake=args.flake, nix_options=args.option
|
|
|
|
)
|
2024-02-06 14:54:12 +00:00
|
|
|
machine.target_host_address = args.target_host
|
2024-04-15 19:06:03 +00:00
|
|
|
machines.append(machine)
|
2023-10-04 21:28:15 +00:00
|
|
|
|
|
|
|
elif args.target_host is not None:
|
|
|
|
print("target host can only be specified for a single machine")
|
|
|
|
exit(1)
|
2023-09-28 12:13:23 +00:00
|
|
|
else:
|
2023-10-04 21:28:15 +00:00
|
|
|
if len(args.machines) == 0:
|
2024-04-15 19:06:03 +00:00
|
|
|
ignored_machines = []
|
2024-05-29 07:08:23 +00:00
|
|
|
for machine in get_all_machines(args.flake, args.option):
|
2024-04-15 19:06:03 +00:00
|
|
|
if machine.deployment_info.get("requireExplicitUpdate", False):
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
machine.build_host
|
|
|
|
except ClanError: # check if we have a build host set
|
|
|
|
ignored_machines.append(machine)
|
|
|
|
continue
|
|
|
|
|
|
|
|
machines.append(machine)
|
|
|
|
|
|
|
|
if not machines and ignored_machines != []:
|
|
|
|
print(
|
|
|
|
"WARNING: No machines to update. The following defined machines were ignored because they do not have `clan.networking.targetHost` nixos option set:",
|
|
|
|
file=sys.stderr,
|
|
|
|
)
|
|
|
|
for machine in ignored_machines:
|
|
|
|
print(machine, file=sys.stderr)
|
|
|
|
|
2023-10-04 21:28:15 +00:00
|
|
|
else:
|
2024-05-29 07:08:23 +00:00
|
|
|
machines = get_selected_machines(args.flake, args.option, args.machines)
|
2023-09-28 12:13:23 +00:00
|
|
|
|
2024-04-15 19:06:03 +00:00
|
|
|
deploy_nixos(MachineGroup(machines))
|
2023-08-10 10:30:52 +00:00
|
|
|
|
|
|
|
|
2023-08-24 14:25:06 +00:00
|
|
|
def register_update_parser(parser: argparse.ArgumentParser) -> None:
|
2024-05-30 17:51:53 +00:00
|
|
|
machines_parser = parser.add_argument(
|
2023-09-28 12:13:23 +00:00
|
|
|
"machines",
|
|
|
|
type=str,
|
|
|
|
nargs="*",
|
|
|
|
default=[],
|
2024-04-30 16:53:00 +00:00
|
|
|
metavar="MACHINE",
|
|
|
|
help="machine to update. If no machine is specified, all machines will be updated.",
|
2023-09-28 12:13:23 +00:00
|
|
|
)
|
2024-05-30 17:51:53 +00:00
|
|
|
|
|
|
|
add_dynamic_completer(machines_parser, complete_machines)
|
|
|
|
|
2023-10-04 21:28:15 +00:00
|
|
|
parser.add_argument(
|
|
|
|
"--target-host",
|
|
|
|
type=str,
|
|
|
|
help="address of the machine to update, in the format of user@host:1234",
|
|
|
|
)
|
2023-08-10 10:30:52 +00:00
|
|
|
parser.set_defaults(func=update)
|