mirror of
https://github.com/buckley310/nixos-config.git
synced 2024-12-21 19:24:15 +00:00
189 lines
4.5 KiB
Python
Executable file
189 lines
4.5 KiB
Python
Executable file
#!/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 strict_host_key_checking():
|
|
txt = run(["ssh", "-G", "localhost"], stdout=PIPE).stdout
|
|
if "stricthostkeychecking true" not in txt.decode("utf8").splitlines():
|
|
raise RuntimeError("This script requires StrictHostKeyChecking")
|
|
|
|
|
|
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("\n").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]
|
|
hosts = expand(argv[2])
|
|
args = argv[3:]
|
|
|
|
if op in ["boot", "switch", "test"]:
|
|
apply(op, hosts)
|
|
|
|
elif op == "check":
|
|
check(hosts)
|
|
|
|
elif op == "push":
|
|
push(hosts)
|
|
|
|
elif op == "exec":
|
|
rexec(hosts, args)
|
|
|
|
else:
|
|
print("Invalid op:", op)
|
|
|
|
|
|
icon_bad = "\u274c"
|
|
icon_good = "\u2705"
|
|
strict_host_key_checking()
|
|
depl = get_deployment()
|
|
if __name__ == "__main__":
|
|
main()
|