Merge pull request 'fix pname of clan-cli for nix run' (#368) from Mic92-main into main

This commit is contained in:
clan-bot
2023-09-28 13:26:20 +00:00
9 changed files with 134 additions and 96 deletions

View File

@@ -11,6 +11,7 @@ let
(builtins.fromJSON (builtins.fromJSON
(builtins.readFile (directory + /machines/${machineName}/settings.json))); (builtins.readFile (directory + /machines/${machineName}/settings.json)));
# TODO: remove default system once we have a hardware-config mechanism
nixosConfiguration = { system ? "x86_64-linux", name }: nixpkgs.lib.nixosSystem { nixosConfiguration = { system ? "x86_64-linux", name }: nixpkgs.lib.nixosSystem {
modules = [ modules = [
self.nixosModules.clanCore self.nixosModules.clanCore
@@ -19,8 +20,7 @@ let
{ {
clanCore.machineName = name; clanCore.machineName = name;
clanCore.clanDir = directory; clanCore.clanDir = directory;
# TODO: remove this once we have a hardware-config mechanism nixpkgs.hostPlatform = lib.mkForce system;
nixpkgs.hostPlatform = lib.mkDefault system;
} }
]; ];
inherit specialArgs; inherit specialArgs;
@@ -41,27 +41,32 @@ let
# This instantiates nixos for each system that we support: # This instantiates nixos for each system that we support:
# configPerSystem = <system>.<machine>.nixosConfiguration # configPerSystem = <system>.<machine>.nixosConfiguration
# We need this to build nixos secret generators for each system # We need this to build nixos secret generators for each system
configPerSystem = builtins.listToAttrs configsPerSystem = builtins.listToAttrs
(builtins.map (builtins.map
(system: lib.nameValuePair system (system: lib.nameValuePair system
(lib.mapAttrs (name: _: nixosConfiguration { inherit name system; }) allMachines)) (lib.mapAttrs (name: _: nixosConfiguration { inherit name system; }) allMachines))
supportedSystems); supportedSystems);
machinesPerSystem = lib.mapAttrs (_: machine: getMachine = machine: {
let
config = {
inherit (machine.config.system.clan) uploadSecrets generateSecrets; inherit (machine.config.system.clan) uploadSecrets generateSecrets;
inherit (machine.config.clan.networking) deploymentAddress; inherit (machine.config.clan.networking) deploymentAddress;
}; };
machinesPerSystem = lib.mapAttrs (_: machine: getMachine machine);
machinesPerSystemWithJson = lib.mapAttrs (_: machine:
let
m = getMachine machine;
in in
config // { m // {
json = machine.pkgs.writeText "config.json" (builtins.toJSON config); json = machine.pkgs.writers.writeJSON "machine.json" m;
}); });
in in
{ {
inherit nixosConfigurations; inherit nixosConfigurations;
clanInternals = { clanInternals = {
machines = lib.mapAttrs (_: machinesPerSystem) configPerSystem; machines = lib.mapAttrs (_: configs: machinesPerSystemWithJson configs) configsPerSystem;
machines-json = lib.mapAttrs (system: configs: nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (machinesPerSystem configs)) configsPerSystem;
}; };
} }

View File

@@ -1,17 +1,5 @@
{ lib, self, nixpkgs, ... }: { lib, self, nixpkgs, ... }:
{ {
findNixFiles = folder:
lib.mapAttrs'
(name: type:
if
type == "directory"
then
lib.nameValuePair name "${folder}/${name}"
else
lib.nameValuePair (lib.removeSuffix ".nix" name) "${folder}/${name}"
)
(builtins.readDir folder);
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
buildClan = import ./build-clan { inherit lib self nixpkgs; }; buildClan = import ./build-clan { inherit lib self nixpkgs; };

View File

@@ -2,15 +2,17 @@ import argparse
import json import json
import os import os
import subprocess import subprocess
from pathlib import Path
from typing import Any
from ..dirs import get_clan_flake_toplevel from ..dirs import get_clan_flake_toplevel
from ..nix import nix_command, nix_config, nix_eval from ..nix import nix_build, nix_command, nix_config
from ..secrets.generate import generate_secrets from ..secrets.generate import run_generate_secrets
from ..secrets.upload import upload_secrets from ..secrets.upload import run_upload_secrets
from ..ssh import Host, HostGroup, HostKeyCheck, parse_deployment_address from ..ssh import Host, HostGroup, HostKeyCheck, parse_deployment_address
def deploy_nixos(hosts: HostGroup) -> None: def deploy_nixos(hosts: HostGroup, clan_dir: Path) -> None:
""" """
Deploy to all hosts in parallel Deploy to all hosts in parallel
""" """
@@ -38,8 +40,11 @@ def deploy_nixos(hosts: HostGroup) -> None:
flake_attr = h.meta.get("flake_attr", "") flake_attr = h.meta.get("flake_attr", "")
generate_secrets(flake_attr) if generate_secrets_script := h.meta.get("generate_secrets"):
upload_secrets(flake_attr) run_generate_secrets(generate_secrets_script, clan_dir)
if upload_secrets_script := h.meta.get("upload_secrets"):
run_upload_secrets(upload_secrets_script, clan_dir)
target_host = h.meta.get("target_host") target_host = h.meta.get("target_host")
if target_host: if target_host:
@@ -74,31 +79,65 @@ def deploy_nixos(hosts: HostGroup) -> None:
hosts.run_function(deploy) hosts.run_function(deploy)
# FIXME: we want some kind of inventory here. def build_json(targets: list[str]) -> list[dict[str, Any]]:
def update(args: argparse.Namespace) -> None: outpaths = subprocess.run(
clan_dir = get_clan_flake_toplevel().as_posix() nix_build(targets),
machine = args.machine
config = nix_config()
system = config["system"]
address = json.loads(
subprocess.run(
nix_eval(
[
f'{clan_dir}#clanInternals.machines."{system}"."{machine}".deploymentAddress'
]
),
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
check=True, check=True,
text=True, text=True,
).stdout ).stdout
parsed = []
for outpath in outpaths.splitlines():
parsed.append(json.loads(Path(outpath).read_text()))
return parsed
def get_all_machines(clan_dir: Path) -> HostGroup:
config = nix_config()
system = config["system"]
what = f'{clan_dir}#clanInternals.machines-json."{system}"'
machines = build_json([what])[0]
hosts = []
for name, machine in machines.items():
host = parse_deployment_address(
name, machine["deploymentAddress"], meta=machine
) )
host = parse_deployment_address(machine, address) hosts.append(host)
print(f"deploying {machine}") return HostGroup(hosts)
deploy_nixos(HostGroup([host]))
def get_selected_machines(machine_names: list[str], clan_dir: Path) -> HostGroup:
config = nix_config()
system = config["system"]
what = []
for name in machine_names:
what.append(f'{clan_dir}#clanInternals.machines."{system}"."{name}".json')
machines = build_json(what)
hosts = []
for i, machine in enumerate(machines):
host = parse_deployment_address(machine_names[i], machine["deploymentAddress"])
hosts.append(host)
return HostGroup(hosts)
# FIXME: we want some kind of inventory here.
def update(args: argparse.Namespace) -> None:
clan_dir = get_clan_flake_toplevel()
if len(args.machines) == 0:
machines = get_all_machines(clan_dir)
else:
machines = get_selected_machines(args.machines, clan_dir)
deploy_nixos(machines, clan_dir)
def register_update_parser(parser: argparse.ArgumentParser) -> None: def register_update_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("machine", type=str) parser.add_argument(
"machines",
type=str,
help="machine to update. if empty, update all machines",
nargs="*",
default=[],
)
parser.set_defaults(func=update) parser.set_defaults(func=update)

View File

@@ -2,6 +2,7 @@ import argparse
import os import os
import shlex import shlex
import subprocess import subprocess
from pathlib import Path
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
@@ -9,11 +10,7 @@ from ..dirs import get_clan_flake_toplevel, module_root
from ..nix import nix_build, nix_config from ..nix import nix_build, nix_config
def generate_secrets(machine: str) -> None: def build_generate_script(machine: str, clan_dir: Path) -> str:
clan_dir = get_clan_flake_toplevel().as_posix().strip()
env = os.environ.copy()
env["CLAN_DIR"] = clan_dir
env["PYTHONPATH"] = str(module_root().parent) # TODO do this in the clanCore module
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
@@ -28,21 +25,32 @@ def generate_secrets(machine: str) -> None:
f"failed to generate secrets:\n{shlex.join(cmd)}\nexited with {proc.returncode}" f"failed to generate secrets:\n{shlex.join(cmd)}\nexited with {proc.returncode}"
) )
secret_generator_script = proc.stdout.strip() return proc.stdout.strip()
print(secret_generator_script)
secret_generator = subprocess.run(
def run_generate_secrets(secret_generator_script: str, clan_dir: Path) -> None:
env = os.environ.copy()
env["CLAN_DIR"] = str(clan_dir)
env["PYTHONPATH"] = str(module_root().parent) # TODO do this in the clanCore module
print(f"generating secrets... {secret_generator_script}")
proc = subprocess.run(
[secret_generator_script], [secret_generator_script],
env=env, env=env,
) )
if secret_generator.returncode != 0: if proc.returncode != 0:
raise ClanError("failed to generate secrets") raise ClanError("failed to generate secrets")
else: else:
print("successfully generated secrets") print("successfully generated secrets")
def generate(machine: str) -> None:
clan_dir = get_clan_flake_toplevel()
run_generate_secrets(build_generate_script(machine, clan_dir), clan_dir)
def generate_command(args: argparse.Namespace) -> None: def generate_command(args: argparse.Namespace) -> None:
generate_secrets(args.machine) generate(args.machine)
def register_generate_parser(parser: argparse.ArgumentParser) -> None: def register_generate_parser(parser: argparse.ArgumentParser) -> None:

View File

@@ -44,8 +44,8 @@ def generate_secrets_group(
text = f"""\ text = f"""\
set -euo pipefail set -euo pipefail
facts={shlex.quote(str(facts_dir))} export facts={shlex.quote(str(facts_dir))}
secrets={shlex.quote(str(secrets_dir))} export secrets={shlex.quote(str(secrets_dir))}
{generator} {generator}
""" """
try: try:

View File

@@ -1,57 +1,51 @@
import argparse import argparse
import json
import os import os
import shlex
import subprocess import subprocess
from pathlib import Path
from ..dirs import get_clan_flake_toplevel, module_root from ..dirs import get_clan_flake_toplevel, module_root
from ..errors import ClanError from ..errors import ClanError
from ..nix import nix_build, nix_config, nix_eval from ..nix import nix_build, nix_config
def upload_secrets(machine: str) -> None: def build_upload_script(machine: str, clan_dir: Path) -> str:
clan_dir = get_clan_flake_toplevel().as_posix()
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
proc = subprocess.run( cmd = nix_build(
nix_build(
[f'{clan_dir}#clanInternals.machines."{system}"."{machine}".uploadSecrets'] [f'{clan_dir}#clanInternals.machines."{system}"."{machine}".uploadSecrets']
), )
stdout=subprocess.PIPE, proc = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
text=True, if proc.returncode != 0:
check=True, raise ClanError(
f"failed to upload secrets:\n{shlex.join(cmd)}\nexited with {proc.returncode}"
) )
return proc.stdout.strip()
def run_upload_secrets(flake_attr: str, clan_dir: Path) -> None:
env = os.environ.copy() env = os.environ.copy()
env["CLAN_DIR"] = str(clan_dir)
env["PYTHONPATH"] = str(module_root().parent) # TODO do this in the clanCore module env["PYTHONPATH"] = str(module_root().parent) # TODO do this in the clanCore module
host = json.loads( print(f"uploading secrets... {flake_attr}")
subprocess.run( proc = subprocess.run(
nix_eval( [flake_attr],
[
f'{clan_dir}#clanInternals.machines."{system}"."{machine}".deploymentAddress'
]
),
stdout=subprocess.PIPE,
text=True,
check=True,
).stdout
)
secret_upload_script = proc.stdout.strip()
secret_upload = subprocess.run(
[
secret_upload_script,
host,
],
env=env, env=env,
) )
if secret_upload.returncode != 0: if proc.returncode != 0:
raise ClanError("failed to upload secrets") raise ClanError("failed to upload secrets")
else: else:
print("successfully uploaded secrets") print("successfully uploaded secrets")
def upload_secrets(machine: str) -> None:
clan_dir = get_clan_flake_toplevel()
run_upload_secrets(build_upload_script(machine, clan_dir), clan_dir)
def upload_command(args: argparse.Namespace) -> None: def upload_command(args: argparse.Namespace) -> None:
upload_secrets(args.machine) upload_secrets(args.machine)

View File

@@ -756,7 +756,9 @@ class HostGroup:
return HostGroup(list(filter(pred, self.hosts))) return HostGroup(list(filter(pred, self.hosts)))
def parse_deployment_address(machine_name: str, host: str) -> Host: def parse_deployment_address(
machine_name: str, host: str, meta: dict[str, str] = {}
) -> Host:
parts = host.split("@") parts = host.split("@")
user: Optional[str] = None user: Optional[str] = None
if len(parts) > 1: if len(parts) > 1:
@@ -776,12 +778,14 @@ def parse_deployment_address(machine_name: str, host: str) -> Host:
if len(maybe_port) > 1: if len(maybe_port) > 1:
hostname = maybe_port[0] hostname = maybe_port[0]
port = int(maybe_port[1]) port = int(maybe_port[1])
meta = meta.copy()
meta["flake_attr"] = machine_name
return Host( return Host(
hostname, hostname,
user=user, user=user,
port=port, port=port,
command_prefix=machine_name, command_prefix=machine_name,
meta=dict(flake_attr=machine_name), meta=meta,
ssh_options=options, ssh_options=options,
) )

View File

@@ -11,7 +11,7 @@
}; };
# Don't leak python packages into a devshell. # Don't leak python packages into a devshell.
# It can be very confusing if you `nix run` than than load the cli from the devshell instead. # It can be very confusing if you `nix run` than than load the cli from the devshell instead.
clan-cli = pkgs.runCommand "clan-cli" { } '' clan-cli = pkgs.runCommand "clan" { } ''
mkdir $out mkdir $out
ln -s ${self'.packages.clan-cli-unwrapped}/bin $out ln -s ${self'.packages.clan-cli-unwrapped}/bin $out
''; '';

View File

@@ -15,7 +15,7 @@ exclude = ["clan_cli.nixpkgs*"]
clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
faulthandler_timeout = 30 faulthandler_timeout = 60
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --workers auto --durations 5" addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --workers auto --durations 5"
norecursedirs = "tests/helpers" norecursedirs = "tests/helpers"
markers = [ "impure" ] markers = [ "impure" ]