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:
parent
500438636a
commit
05c45fe5e3
10 changed files with 207 additions and 216 deletions
|
@ -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)
|
||||
|
|
13
src/infra_rebuild/msg/__init__.py
Normal file
13
src/infra_rebuild/msg/__init__.py
Normal 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)
|
146
src/infra_rebuild/operations/__init__.py
Normal file
146
src/infra_rebuild/operations/__init__.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue