mirror of
https://github.com/buckley310/nixos-config.git
synced 2024-12-21 19:24:15 +00:00
replace deploy tools
This commit is contained in:
parent
1f344ad69f
commit
e05c71b8d1
6 changed files with 218 additions and 101 deletions
|
@ -54,7 +54,7 @@
|
||||||
{
|
{
|
||||||
lib = {
|
lib = {
|
||||||
inherit forAllSystems;
|
inherit forAllSystems;
|
||||||
deploy = import lib/deploy.nix;
|
gen-ssh-config = import lib/gen-ssh-config.nix lib;
|
||||||
};
|
};
|
||||||
|
|
||||||
nixosModules = mods // { default.imports = builtins.attrValues mods; };
|
nixosModules = mods // { default.imports = builtins.attrValues mods; };
|
||||||
|
|
|
@ -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
23
lib/gen-ssh-config.nix
Normal 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))
|
|
@ -1,17 +1,15 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
with lib.types;
|
|
||||||
{
|
{
|
||||||
options.sconfig = {
|
options.deploy = {
|
||||||
|
|
||||||
sshPublicKeys = lib.mkOption {
|
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 = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
deployment = lib.mkOption {
|
|
||||||
type = attrs;
|
|
||||||
default = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
5
pkgs/deploy/default.nix
Normal file
5
pkgs/deploy/default.nix
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{ writeShellScriptBin, python3 }:
|
||||||
|
|
||||||
|
writeShellScriptBin "deploy" ''
|
||||||
|
exec ${python3}/bin/python ${./deploy.py} "$@"
|
||||||
|
''
|
181
pkgs/deploy/deploy.py
Executable file
181
pkgs/deploy/deploy.py
Executable 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()
|
Loading…
Reference in a new issue