replace deploy tools

This commit is contained in:
Sean Buckley 2023-03-17 00:21:32 -04:00
parent 1f344ad69f
commit e05c71b8d1
6 changed files with 218 additions and 101 deletions

View file

@ -54,7 +54,7 @@
{
lib = {
inherit forAllSystems;
deploy = import lib/deploy.nix;
gen-ssh-config = import lib/gen-ssh-config.nix lib;
};
nixosModules = mods // { default.imports = builtins.attrValues mods; };

View file

@ -1,90 +0,0 @@
{ self
, hosts
, modules ? [ ]
}:
let
inherit (self.inputs) nixpkgs;
inherit (self) nixosConfigurations;
helpers = system:
let
inherit (nixpkgs.lib) concatMapStrings;
inherit (nixpkgs.legacyPackages.${system}) pkgs;
sshKnownHostsTxt = pkgs.writeText "known_hosts" (concatMapStrings
(hostName:
let m = nixosConfigurations.${hostName}.config.sconfig;
in concatMapStrings (key: "${m.deployment.targetHost} ${key}\n") m.sshPublicKeys
)
(builtins.attrNames nixosConfigurations)
);
hostSshConfigs = concatMapStrings
(hostName: ''
Host ${hostName}
HostName ${nixosConfigurations.${hostName}.config.sconfig.deployment.targetHost}
'')
(builtins.attrNames nixosConfigurations);
sshConfig = pkgs.writeText "ssh_config" ''
StrictHostKeyChecking yes
GlobalKnownHostsFile ${sshKnownHostsTxt}
${hostSshConfigs}
'';
livecd-deploy = pkgs.writeShellScript "livecd-deploy" ''
set -eux
config=".#nixosConfigurations.\"$1\".config"
ip="$(nix eval --raw "$config.sconfig.deployment.targetHost")"
ssh-copy-id root@$ip
sys="$(nix eval --raw "$config.system.build.toplevel")"
nix build "$config.system.build.toplevel" --out-link "$(mktemp -d)/result"
nix copy --to ssh://root@$ip?remote-store=local?root=/mnt "$sys"
ssh root@$ip nix-env --store /mnt -p /mnt/nix/var/nix/profiles/system --set "$sys"
ssh root@$ip mkdir -p /mnt/etc
ssh root@$ip touch /mnt/etc/NIXOS
ssh root@$ip ln -sfn /proc/mounts /mnt/etc/mtab
ssh root@$ip NIXOS_INSTALL_BOOTLOADER=1 nixos-enter \
--root /mnt -- /run/current-system/bin/switch-to-configuration boot
'';
check-updates = pkgs.writeShellScript "check-updates" ''
set -eu
c="${pkgs.colmena}/bin/colmena"
j="$($c eval -E '{nodes,...}: builtins.mapAttrs (n: v: v.config.system.build.toplevel) nodes')"
$c exec -- '[ "$(echo '"'$j'"' | jq -r .\"$(hostname)\")" = "$(readlink /run/current-system)" ]'
'';
check-reboots = pkgs.writeShellScript "check-reboots" ''
set -eu
c="${pkgs.colmena}/bin/colmena"
$c exec -- '[ "$(readlink /run/booted-system/kernel)" = "$(readlink /run/current-system/kernel)" ]'
'';
in
{ inherit check-updates check-reboots livecd-deploy pkgs sshConfig; };
in
{
defaultPackage = system: with helpers system;
pkgs.writeShellScript "deploy-init" ''
export SSH_CONFIG_FILE=${sshConfig}
alias ssh='ssh -F${sshConfig}'
alias check-updates=${check-updates}
alias check-reboots=${check-reboots}
alias livecd-deploy=${livecd-deploy}
alias c=${pkgs.colmena}/bin/colmena
'';
colmena =
# colmena evaluates in impure mode, so currentSystem is OK for now
{ meta.nixpkgs = nixpkgs.legacyPackages.${builtins.currentSystem}; } //
builtins.mapAttrs
(name: value: {
imports = value.modules ++ [
({ config, ... }: { inherit (config.sconfig) deployment; })
];
})
(hosts);
}

23
lib/gen-ssh-config.nix Normal file
View file

@ -0,0 +1,23 @@
lib:
nixosConfigurations:
let
sshKnownHostsTxt = builtins.toFile "known_hosts" (lib.concatMapStrings
(hostName:
let d = nixosConfigurations.${hostName}.config.deploy;
in lib.concatMapStrings (key: "${d.targetHost} ${key}\n") d.sshPublicKeys
)
(builtins.attrNames nixosConfigurations)
);
in
builtins.toFile "ssh-config" (''
StrictHostKeyChecking yes
GlobalKnownHostsFile ${sshKnownHostsTxt}
'' +
lib.concatMapStrings
(host: ''
Host ${host}
HostName ${nixosConfigurations.${host}.config.deploy.targetHost}
'')
(builtins.attrNames nixosConfigurations))

View file

@ -1,17 +1,15 @@
{ lib, ... }:
with lib.types;
{
options.sconfig = {
options.deploy = {
sshPublicKeys = lib.mkOption {
type = listOf str;
type = lib.types.listOf lib.types.str;
};
targetHost = lib.mkOption {
type = lib.types.str;
};
tags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
deployment = lib.mkOption {
type = attrs;
default = { };
};
};
}

5
pkgs/deploy/default.nix Normal file
View file

@ -0,0 +1,5 @@
{ writeShellScriptBin, python3 }:
writeShellScriptBin "deploy" ''
exec ${python3}/bin/python ${./deploy.py} "$@"
''

181
pkgs/deploy/deploy.py Executable file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env python3
from json import loads
from os import readlink
from string import ascii_letters, digits
from subprocess import run, PIPE, STDOUT
from sys import argv
def get_deployment():
return loads(
run(
[
"nix",
"eval",
"--json",
"--apply",
"builtins.mapAttrs (n: v: v.config.deploy)",
".#nixosConfigurations",
],
stdout=PIPE,
check=True,
).stdout
)
def expand(ln):
hosts = set()
for item in ln.split(","):
if item == "@all":
hosts.update(depl)
elif item[0] == "@":
hosts.update(x for x in depl if item[1:] in depl[x]["tags"])
else:
hosts.add(item)
for host in hosts:
for c in host:
if not c in (ascii_letters + digits + "-"):
raise RuntimeError(f"Invalid hostname: {host}")
return sorted(hosts)
def build_system(name):
return loads(
run(
[
"nix",
"build",
"--no-link",
"--json",
f".#nixosConfigurations.{name}.config.system.build.toplevel",
],
stdout=PIPE,
check=True,
).stdout
)[0]["outputs"]["out"]
def build_all(hosts):
return dict([x, build_system(x)] for x in hosts)
def apply(goal, hosts):
for host in hosts:
print(f"\n\n{goal} on {host}...\n")
run(
[
"nixos-rebuild",
goal,
"--use-substitutes",
"--use-remote-sudo",
"--target-host",
host,
"--flake",
".#" + host,
],
check=True,
)
def check(hosts):
hostwidth = max(map(len, hosts))
new_sys = build_all(hosts)
print("#" * 64)
for host in hosts:
current_sys, cur_kernel, boot_kernel = (
run(
[
"ssh",
host,
"readlink",
"/run/current-system",
"/run/current-system/kernel",
"/run/booted-system/kernel",
],
stdout=PIPE,
check=True,
)
.stdout.decode("ascii")
.strip()
.split("\n")
)
reboot_needed = cur_kernel != boot_kernel
update_needed = current_sys != new_sys[host]
print(host.rjust(hostwidth + 1), end=" ")
if not (reboot_needed or update_needed):
print(icon_good, "[OK]")
continue
print(icon_bad, end=" ")
if update_needed:
print("[UPDATE REQUIRED]", end=" ")
if readlink(new_sys[host] + "/kernel") != boot_kernel:
print("[UPDATE REQUIRES REBOOT]", end=" ")
if reboot_needed:
print("[REBOOT REQUIRED]", end=" ")
print()
print("#" * 64)
def push(hosts):
new_sys = build_all(hosts)
for host in hosts:
print(f"Pushing to {host}...")
run(
[
"nix",
"copy",
new_sys[host],
"--to",
"ssh://" + host,
],
check=True,
)
def rexec(hosts, cmd):
hostwidth = max(map(len, hosts))
for host in hosts:
r = run(["ssh", host, "--"] + cmd, stdout=PIPE, stderr=STDOUT)
lines = r.stdout.decode("utf8").strip().splitlines()
print(host.rjust(hostwidth), end=" ")
print(icon_bad if r.returncode else icon_good, end=" ")
if len(lines) == 1:
print(lines[0])
else:
print()
for ln in lines:
print(" " * hostwidth, ln)
def main():
op = argv[1]
args = argv[2:]
if op in ["boot", "switch", "test"]:
apply(op, expand(args[0]))
elif op == "check":
check(expand(args[0]))
elif op == "push":
push(expand(args[0]))
elif op == "exec":
rexec(expand(args[0]), args[1:])
else:
print("Invalid op:", op)
icon_bad = "\u274c"
icon_good = "\u2705"
depl = get_deployment()
if __name__ == "__main__":
main()