diff --git a/README.md b/README.md index c1faf59..0d21df2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ However to override aspects of the target host for specific or all hosts, infra- ## Configuration -infra-rebuild accepts optional configuration in a `deployment_configuration.json`. \ +infra-rebuild accepts optional configuration in a `deployment_configuration.json` present in the flake repos root. \ The following keys are available to be set for configuring various aspects of deployment for specific or all hosts: - `default.targetPort`: A default port to use for connecting to all host. diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py index 05cef25..fe31d77 100644 --- a/src/infra_rebuild/cli/__init__.py +++ b/src/infra_rebuild/cli/__init__.py @@ -3,54 +3,108 @@ import click from infra_rebuild import operations reboot_option_help_text = "Reboot the target hosts after running the operation." +flake_option_help_text = "URI of the flake to use. Defaults to the current direcory." + + +def initialize_recursive_option(ctx, option_name, default_value): + """Helper function for initializing a recursive option. + + Arguments: + `ctx` -- The click context passed to the calling command. + `option_name` -- The name of the option in question. + `default_value` -- A default value for the option. + + Simply sets the value of the recursive option (`ctx.obj[option_name]`) to `default_value`. + """ + ctx.obj[option_name] = default_value + + +def update_recursive_option(ctx, option_name, given_value): + """Helper function for updating a recursive option. + + Arguments: + ctx -- The click context passed to the calling command. + option_name -- The name of the option in question. + given_value -- The value the calling command received for the option. + + It overrides the current value for the recursive option (`ctx.obj[option_name]`) with `given_value`, if `given_value` isn't `None`. + This ensure precedence of the innermost command (since the innermost command would call this function last). + """ # noqa: E501 + if given_value is not None: + ctx.obj[option_name] = given_value @click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True) @click.version_option(prog_name="infra-rebuild") -def infra_rebuild(): - pass +@click.pass_context +@click.option("--flake", help=flake_option_help_text) +def infra_rebuild(ctx, flake): + ctx.ensure_object(dict) + initialize_recursive_option(ctx, "flake", ".") + update_recursive_option(ctx, "flake", flake) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build(hosts): - operations.run("build", hosts) +def build(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build_vm(hosts): - operations.run("build-vm", hosts) +def build_vm(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build-vm", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def build_vm_with_bootloader(hosts): - operations.run("build-vm-with-bootloader", hosts) +def build_vm_with_bootloader(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("build-vm-with-bootloader", hosts, ctx.obj["flake"]) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def switch(hosts, reboot): - operations.run("switch", hosts, reboot) +def switch(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("switch", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def boot(hosts, reboot): - operations.run("boot", hosts, reboot) +def boot(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("boot", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command() +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.option("--reboot", is_flag=True, help=reboot_option_help_text) @click.argument("hosts", nargs=-1) -def test(hosts, reboot): - operations.run("test", hosts, reboot) +def test(ctx, flake, reboot, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("test", hosts, ctx.obj["flake"], reboot) @infra_rebuild.command(short_help=" ", help="This operation is a convenience alias for boot --reboot.") +@click.pass_context +@click.option("--flake", help=flake_option_help_text) @click.argument("hosts", nargs=-1) -def reboot(hosts): - operations.run("boot", hosts, True) +def reboot(ctx, flake, hosts): + update_recursive_option(ctx, "flake", flake) + operations.run("boot", hosts, ctx.obj["flake"], True) diff --git a/src/infra_rebuild/operations/__init__.py b/src/infra_rebuild/operations/__init__.py index b8d3f28..5368848 100644 --- a/src/infra_rebuild/operations/__init__.py +++ b/src/infra_rebuild/operations/__init__.py @@ -6,7 +6,27 @@ import sys from infra_rebuild import msg -def run(operation, hosts, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip +def run(operation, hosts, flake_uri, reboot=False): # noqa: FBT002 - having reboot as a Boolean positional argument is fine I think # fmt: skip + try: + flake_archive_json = subprocess.run( + ["nix", "flake", "archive", "--json", f"{flake_uri}"], # noqa: S607, S603 - can't know what the users system looks like, maybe do some kind of validation for the flake_uri in the future? + capture_output=True, + text=True, + check=True + ) # fmt: skip + try: + flake_archive = json.loads(flake_archive_json.stdout) + except json.JSONDecodeError: + msg.error("Internal Error: Couldn't parse the output of \"nix flake archive\".") + sys.exit(1) + + if not flake_archive["path"]: + msg.error("Internal Error: The \"nix flake archive\" output doesn't include a path.") + sys.exit(1) + except subprocess.CalledProcessError: + msg.error("Error: Couldn't archive the provided flake.") + sys.exit(1) + match operation: case "build" | "build-vm" | "build-vm-with-bootloader": act_remotely = False @@ -22,23 +42,23 @@ def run(operation, hosts, reboot=False): # noqa: FBT002 - having reboot as a Bo continue if act_remotely: - remote(operation, host, reboot) + remote(operation, host, flake_archive, reboot) else: - local(operation, host) + local(operation, host, flake_archive) -def local(operation, host): +def local(operation, host, flake_archive): 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? + ["nixos-rebuild", operation, "--flake", f"{flake_archive['path']}#{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 +def remote(operation, host, flake_archive, 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: + with open(f"{flake_archive['path']}/deployment_configuration.json") as config_file: try: config = json.load(config_file) except json.JSONDecodeError: @@ -62,7 +82,7 @@ def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a 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? + ["nix", "eval", "--raw", f"{flake_archive['path']}#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, @@ -119,7 +139,7 @@ def remote(operation, host, reboot=False): # noqa: FBT002 - having reboot as a 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 + ["nixos-rebuild", operation, "--flake", f"{flake_archive['path']}#{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