Port infra-rebuild to Python

Do this for a nicer developer experience in a safer language, which has
nice libraries available to e.g. build command line interfaces (e.g.
click).

Set minimum Python version to 3.10 to support match statements.
This commit is contained in:
June 2024-06-06 00:05:55 +02:00
commit 05c45fe5e3
Signed by: june
SSH key fingerprint: SHA256:o9EAq4Y9N9K0pBQeBTqhSDrND5E7oB+60ZNx0U1yPe0
10 changed files with 207 additions and 216 deletions

View file

@ -1,9 +1,52 @@
import click
from infra_rebuild import operations
from infra_rebuild.__about__ import __version__
@click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True)
@click.version_option(version=__version__, prog_name="infra-rebuild")
def infra_rebuild():
click.echo("Hello world!")
pass
@infra_rebuild.command()
@click.argument("hosts")
def build(hosts):
operations.run("build", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def build_vm(hosts):
operations.run("build-vm", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def build_vm_with_bootloader(hosts):
operations.run("build-vm-with-bootloader", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def switch(hosts):
operations.run("switch", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def boot(hosts):
operations.run("boot", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def test(hosts):
operations.run("test", hosts)
@infra_rebuild.command()
@click.argument("hosts")
def reboot(hosts):
operations.run("reboot", hosts)

View file

@ -0,0 +1,13 @@
import click
def info(message):
click.echo(click.style(message, bold=True, fg="bright_blue"))
def warning(message):
click.echo(click.style(message, bold=True, fg="bright_yellow"), err=True)
def error(message):
click.echo(click.style(message, bold=True, fg="bright_red"), err=True)

View file

@ -0,0 +1,146 @@
import json
import os
import subprocess
import sys
from infra_rebuild import msg
def run(operation, hosts):
act_remotely = False
reboot = False
match operation:
case "build" | "build-vm" | "build-vm-with-bootloader":
actual_operation = operation
case "switch" | "boot" | "test":
act_remotely = True
actual_operation = operation
case "reboot":
act_remotely = True
actual_operation = "boot"
reboot = True
case _:
msg.error("Internal Error: The given operation isn't valid.")
sys.exit(1)
for host_string in hosts.split(","):
host = host_string.strip()
if not host:
msg.warning("Skipping empty string provided for host.")
continue
if act_remotely:
remote(actual_operation, host, reboot)
else:
local(actual_operation, host)
def local(operation, host):
msg.info(f"Running nixos-rebuild {operation} for {host}...")
subprocess.run(
["nixos-rebuild", operation, "--flake", f".#{host}"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?
check=False
) # fmt: skip
def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip
config = None
try:
with open("deployment_configuration.json") as config_file:
try:
config = json.load(config_file)
except json.JSONDecodeError:
msg.warning(
"The deployment_configuration.json is not a valid JSON document and therefore it can't be used to retrieve configuration values." # noqa: E501
)
except FileNotFoundError:
msg.warning(
"No deployment_configuration.json exists and therefore it can't be used to retrieve configuration values."
)
except OSError:
msg.warning(
"Couldn't open deployment_configuration.json and therefore it can't be used to retrieve configuration values." # noqa: E501
)
target_hostname = None
if config:
try:
target_hostname = config["hosts"][host]["targetHostname"]
except KeyError:
pass
if not target_hostname:
nix_config_fqdn = subprocess.run(
["nix", "eval", "--raw", f".#nixosConfigurations.{host}.config.networking.fqdn"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for host in the future?
capture_output=True,
text=True,
check=False,
) # fmt: skip
if nix_config_fqdn.returncode == 0 and nix_config_fqdn.stdout:
target_hostname = nix_config_fqdn.stdout
if not target_hostname:
msg.error(f"Couldn't determine target hostname for {host}.")
msg.error(
"You either need to set targetHostname for this host in the deployment_configuration.json or have an FQDN available in the NixOS configuration of this host, which then gets used for the target hostname." # noqa: E501
)
msg.error(f"Skipping {host}.")
return
target_user = None
if config:
try:
target_user = config["hosts"][host]["targetUser"]
except KeyError:
pass
if config and not target_user:
try:
target_user = config["default"]["targetUser"]
except KeyError:
pass
target_port = None
if config:
try:
target_port = config["hosts"][host]["targetPort"]
except KeyError:
pass
if config and not target_port:
try:
target_port = config["default"]["targetPort"]
except KeyError:
pass
target_host = target_hostname
if target_user:
target_host = f"{target_user}@{target_host}"
ssh_opts = None
if target_port:
ssh_opts = f"-o Port={target_port}"
nixos_rebuild_env = os.environ.copy()
if ssh_opts:
nixos_rebuild_env["NIX_SSHOPTS"] = ssh_opts
if target_port:
msg.info(f"Running nixos-rebuild {operation} for {host} on {target_host}:{target_port}...")
else:
msg.info(f"Running nixos-rebuild {operation} for {host} on {target_host}...")
nixos_rebuild = subprocess.run(
["nixos-rebuild", operation, "--flake", f".#{host}", "--target-host", target_host, "--use-substitutes", "--use-remote-sudo"], # noqa: S607, S603, E501 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?, not breaking up the command makes it more readable
env=nixos_rebuild_env,
check=False,
) # fmt: skip
if reboot and nixos_rebuild.returncode == 0:
msg.info(f"Rebooting {target_hostname}...")
if ssh_opts:
subprocess.run(
["ssh", ssh_opts, target_host, "sudo", "systemctl", "reboot"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?
check=False
) # fmt: skip
else:
subprocess.run(
["ssh", target_host, "sudo", "systemctl", "reboot"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the given host in the future?
check=False,
) # fmt: skip