From 500438636a945d1474c53a9ccfa1c20cfc7bcc58 Mon Sep 17 00:00:00 2001 From: June Date: Wed, 5 Jun 2024 15:24:47 +0200 Subject: [PATCH 01/10] Create a skeleton python project and modify it to fit this project Create a skeleton python project using "hatch new -i --cli infra-rebuild" with Hatch version 1.7.0 and modify it to fit this project. This is the first step of porting infra-rebuild to Python. Also provide a first .gitignore ignoring relevant build directories. --- .gitignore | 3 + pyproject.toml | 162 ++++++++++++++++++++++++++++++ src/infra_rebuild/__about__.py | 1 + src/infra_rebuild/__init__.py | 0 src/infra_rebuild/__main__.py | 6 ++ src/infra_rebuild/cli/__init__.py | 9 ++ tests/__init__.py | 0 7 files changed, 181 insertions(+) create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 src/infra_rebuild/__about__.py create mode 100644 src/infra_rebuild/__init__.py create mode 100644 src/infra_rebuild/__main__.py create mode 100644 src/infra_rebuild/cli/__init__.py create mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb10265 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +/dist/ +/.coverage* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b741e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,162 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "infra-rebuild" +dynamic = ["version"] +description = "A simple NixOS deployment tool using nixos-rebuild internally, but trying to make infrastructure deployment more convenient." +readme = "README.md" +requires-python = ">=3.7" +keywords = ["nix", "NixOS"] +authors = [ + { name = "June", email = "june@jsts.xyz" }, +] +classifiers = [ + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "click", +] + +[project.urls] +Documentation = "https://git.hamburg.ccc.de/CCCHH/infra-rebuild#readme" +Issues = "https://git.hamburg.ccc.de/CCCHH/infra-rebuild/issues" +Source = "https://git.hamburg.ccc.de/CCCHH/infra-rebuild" + +[project.scripts] +infra-rebuild = "infra_rebuild.cli:infra_rebuild" + +[tool.hatch.version] +path = "src/infra_rebuild/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "coverage[toml]>=6.5", + "pytest", +] +[tool.hatch.envs.default.scripts] +test = "pytest {args:tests}" +test-cov = "coverage run -m pytest {args:tests}" +cov-report = [ + "- coverage combine", + "coverage report", +] +cov = [ + "test-cov", + "cov-report", +] + +[[tool.hatch.envs.all.matrix]] +python = ["3.7", "3.8", "3.9", "3.10", "3.11"] + +[tool.hatch.envs.lint] +detached = true +dependencies = [ + "black>=23.1.0", + "mypy>=1.0.0", + "ruff>=0.0.243", +] +[tool.hatch.envs.lint.scripts] +typing = "mypy --install-types --non-interactive {args:src/infra_rebuild tests}" +style = [ + "ruff {args:.}", + "black --check --diff {args:.}", +] +fmt = [ + "black {args:.}", + "ruff --fix {args:.}", + "style", +] +all = [ + "style", + "typing", +] + +[tool.black] +target-version = ["py37"] +line-length = 120 +skip-string-normalization = true + +[tool.ruff] +target-version = "py37" +line-length = 120 +select = [ + "A", + "ARG", + "B", + "C", + "DTZ", + "E", + "EM", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "Q", + "RUF", + "S", + "T", + "TID", + "UP", + "W", + "YTT", +] +ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +] +unfixable = [ + # Don't touch unused imports + "F401", +] + +[tool.ruff.isort] +known-first-party = ["infra_rebuild"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.coverage.run] +source_pkgs = ["infra_rebuild", "tests"] +branch = true +parallel = true +omit = [ + "src/infra_rebuild/__about__.py", +] + +[tool.coverage.paths] +infra_rebuild = ["src/infra_rebuild", "*/infra-rebuild/src/infra_rebuild"] +tests = ["tests", "*/infra-rebuild/tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/src/infra_rebuild/__about__.py b/src/infra_rebuild/__about__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/src/infra_rebuild/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/src/infra_rebuild/__init__.py b/src/infra_rebuild/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/infra_rebuild/__main__.py b/src/infra_rebuild/__main__.py new file mode 100644 index 0000000..37c16c5 --- /dev/null +++ b/src/infra_rebuild/__main__.py @@ -0,0 +1,6 @@ +import sys + +if __name__ == "__main__": + from infra_rebuild.cli import infra_rebuild + + sys.exit(infra_rebuild()) diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py new file mode 100644 index 0000000..2e350b2 --- /dev/null +++ b/src/infra_rebuild/cli/__init__.py @@ -0,0 +1,9 @@ +import click + +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!") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 05c45fe5e34caf29d52e5560d14e7d556539ffa7 Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 00:05:55 +0200 Subject: [PATCH 02/10] 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. --- helper/msg_error.sh | 3 - helper/msg_info.sh | 3 - helper/msg_warning.sh | 3 - infra-rebuild.sh | 39 ------ operations/local.sh | 45 ------- operations/remote.sh | 115 ------------------ pyproject.toml | 11 +- src/infra_rebuild/cli/__init__.py | 45 ++++++- src/infra_rebuild/msg/__init__.py | 13 ++ src/infra_rebuild/operations/__init__.py | 146 +++++++++++++++++++++++ 10 files changed, 207 insertions(+), 216 deletions(-) delete mode 100755 helper/msg_error.sh delete mode 100755 helper/msg_info.sh delete mode 100755 helper/msg_warning.sh delete mode 100755 infra-rebuild.sh delete mode 100755 operations/local.sh delete mode 100755 operations/remote.sh create mode 100644 src/infra_rebuild/msg/__init__.py create mode 100644 src/infra_rebuild/operations/__init__.py diff --git a/helper/msg_error.sh b/helper/msg_error.sh deleted file mode 100755 index 6d225de..0000000 --- a/helper/msg_error.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/bash - -echo "$(tput bold)$(tput setaf 9)$@$(tput sgr0)" diff --git a/helper/msg_info.sh b/helper/msg_info.sh deleted file mode 100755 index 1ce51df..0000000 --- a/helper/msg_info.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/bash - -echo "$(tput bold)$(tput setaf 12)$@$(tput sgr0)" diff --git a/helper/msg_warning.sh b/helper/msg_warning.sh deleted file mode 100755 index 99f2d87..0000000 --- a/helper/msg_warning.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/bash - -echo "$(tput bold)$(tput setaf 11)$@$(tput sgr0)" diff --git a/infra-rebuild.sh b/infra-rebuild.sh deleted file mode 100755 index 6490977..0000000 --- a/infra-rebuild.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -REAL_BASE_DIR=$( dirname $( realpath "$0" ) ) - -if [ $# -lt 2 ] || [ $# -gt 2 ]; then - $REAL_BASE_DIR/helper/msg_error.sh "\ -Error: Incorrect amount of arguments given. -You need to provide exactly two arguments. The first one being the operation you want to run and the second one being the host or hosts (comma separated) you want to run the operation for." - exit 1 -fi - -OPERATION="$1" -HOSTS="$2" - -case $OPERATION in - build|build-vm|build-vm-with-bootloader|switch|boot|test|reboot) - ;; - *) - $REAL_BASE_DIR/helper/msg_error.sh "\ -Error: No valid operation given. -The operation must be one of: -build, build-vm, build-vm-with-bootloader, switch, boot, test, reboot." - exit 1 - ;; -esac - - -set -e - -for HOST in ${HOSTS//,/ }; do - case $OPERATION in - build|build-vm|build-vm-with-bootloader) - env OPERATION="$OPERATION" HOST="$HOST" $REAL_BASE_DIR/operations/local.sh - ;; - switch|boot|test|reboot) - env OPERATION="$OPERATION" HOST="$HOST" $REAL_BASE_DIR/operations/remote.sh - ;; - esac -done diff --git a/operations/local.sh b/operations/local.sh deleted file mode 100755 index 96ee300..0000000 --- a/operations/local.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -REAL_BASE_DIR=$( dirname $( realpath "$0" ) ) - -# Wrapper for nixos-rebuild operations, which act locally. - -# Takes the following arguments supplied as environment variables. -# HOST: The host as defined in nixosConfigurations. -# OPERATION: The nixos-rebuild operation to execute. Can be one of: -# build, build-vm, build-vm-with-bootloader -# All operations are as defined in the nixos-rebuild man page. - -if [ -z $HOST ]; then - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No host given. -A host needs to be provided via the HOST environment variable." - exit 1 -fi - -if [ -z $OPERATION ]; then - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No operation given. -An operation needs to be provided via the OPERATION environment variable." - exit 1 -fi - -case $OPERATION in - build|build-vm|build-vm-with-bootloader) - ;; - *) - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No valid operation given. -The operation provided via the OPERATION environment variable needs to be one of: -build, build-vm, build-vm-with-bootloader." - exit 1 - ;; -esac - - -set -e - -$REAL_BASE_DIR/../helper/msg_info.sh "\ -Running nixos-rebuild $OPERATION for $HOST..." - -nixos-rebuild "$OPERATION" --flake ".#$HOST" diff --git a/operations/remote.sh b/operations/remote.sh deleted file mode 100755 index 1cbfb51..0000000 --- a/operations/remote.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -REAL_BASE_DIR=$( dirname $( realpath "$0" ) ) - -# Wrapper for nixos-rebuild operations, which act remotely. -# Gets the necessary configuration to do so. - -# Takes the following arguments supplied as environment variables. -# HOST: The host as defined in nixosConfigurations and the -# deployment_configuration.json. -# OPERATION: The nixos-rebuild operation to execute. Can be one of: -# switch, boot, test, reboot -# All operations are as defined in the nixos-rebuild man page except -# for reboot, which runs boot, but then also reboots the host. - -if [ -z $HOST ]; then - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No host given. -A host needs to be provided via the HOST environment variable." - exit 1 -fi - -if [ -z $OPERATION ]; then - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No operation given. -An operation needs to be provided via the OPERATION environment variable." - exit 1 -fi - -ACTUAL_OPERATION="" -case $OPERATION in - switch|boot|test) - ACTUAL_OPERATION="$OPERATION" - ;; - reboot) - ACTUAL_OPERATION="boot" - ;; - *) - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Internal Error: No valid operation given. -The operation provided via the OPERTION environment variable needs to be one of: -switch, boot, test, reboot." - exit 1 - ;; -esac - - -TARGET_HOSTNAME="" -TARGET_USER="" -TARGET_PORT="" - -DEPLOYMENT_CONFIGURATION_EXISTS=true -if ! [ -f deployment_configuration.json ]; then - $REAL_BASE_DIR/../helper/msg_warning.sh "\ -Warning: No deployment_configuration.json exists and therefore it can't be used to retrieve configuration values." - DEPLOYMENT_CONFIGURATION_EXISTS=false -fi - -if $DEPLOYMENT_CONFIGURATION_EXISTS && ! cat deployment_configuration.json | jq . >/dev/null 2>&1; then - $REAL_BASE_DIR/../helper/msg_warning.sh "\ -Warning: jq can't parse the deployment_configuration.json and therefore it can't be used to retrieve configuration values." -fi - -if $DEPLOYMENT_CONFIGURATION_EXISTS && CONFIG_TARGET_HOSTNAME="$(cat deployment_configuration.json | jq -re .hosts.\"$HOST\".targetHostname 2>/dev/null)"; then - TARGET_HOSTNAME=$CONFIG_TARGET_HOSTNAME -elif NIX_CONFIG_FQDN="$(nix eval --raw .\#nixosConfigurations.$HOST.config.networking.fqdn 2>/dev/null)"; then - TARGET_HOSTNAME=$NIX_CONFIG_FQDN -else - $REAL_BASE_DIR/../helper/msg_error.sh "\ -Error: Couldn't determine target hostname for $HOST. -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." - exit 1 -fi - -if $DEPLOYMENT_CONFIGURATION_EXISTS && CONFIG_TARGET_USER="$(cat deployment_configuration.json | jq -re .hosts.\"$HOST\".targetUser 2>/dev/null)"; then - TARGET_USER=$CONFIG_TARGET_USER -elif $DEPLOYMENT_CONFIGURATION_EXISTS && CONFIG_DEFAULT_TARGET_USER="$(cat deployment_configuration.json | jq -re '.default.targetUser' 2>/dev/null)"; then - TARGET_USER=$CONFIG_DEFAULT_TARGET_USER -fi - -if $DEPLOYMENT_CONFIGURATION_EXISTS && CONFIG_TARGET_PORT="$(cat deployment_configuration.json | jq -re .hosts.\"$HOST\".targetPort 2>/dev/null)"; then - TARGET_PORT=$CONFIG_TARGET_PORT -elif $DEPLOYMENT_CONFIGURATION_EXISTS && CONFIG_DEFAULT_TARGET_PORT="$(cat deployment_configuration.json | jq -re '.default.targetPort' 2>/dev/null)"; then - TARGET_PORT=$CONFIG_DEFAULT_TARGET_PORT -fi - - -TARGET_HOST="$TARGET_HOSTNAME" -if [ -n "$TARGET_USER" ]; then - TARGET_HOST="$TARGET_USER@$TARGET_HOST" -fi - -SSHOPTS="" -if [ -n "$TARGET_PORT" ]; then - SSHOPTS="-o Port=$TARGET_PORT" -fi - - -set -e - -if [ -n "$TARGET_PORT" ]; then - $REAL_BASE_DIR/../helper/msg_info.sh "\ -Running nixos-rebuild $ACTUAL_OPERATION for $HOST on $TARGET_HOST:$TARGET_PORT..." -else - $REAL_BASE_DIR/../helper/msg_info.sh "\ -Running nixos-rebuild $ACTUAL_OPERATION for $HOST on $TARGET_HOST..." -fi - -env NIX_SSHOPTS="$SSHOPTS" nixos-rebuild "$ACTUAL_OPERATION" --flake ".#$HOST" --target-host "$TARGET_HOST" --use-substitutes --use-remote-sudo - -if [ "$OPERATION" = "reboot" ]; then - $REAL_BASE_DIR/../helper/msg_info.sh "\ -Rebooting $TARGET_HOSTNAME..." - ssh $SSH_OPTS "$TARGET_HOST" sudo systemctl reboot -fi diff --git a/pyproject.toml b/pyproject.toml index 3b741e4..3eb6ea1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "infra-rebuild" dynamic = ["version"] description = "A simple NixOS deployment tool using nixos-rebuild internally, but trying to make infrastructure deployment more convenient." readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" keywords = ["nix", "NixOS"] authors = [ { name = "June", email = "june@jsts.xyz" }, @@ -16,9 +16,6 @@ classifiers = [ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", "Development Status :: 4 - Beta", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", @@ -57,7 +54,7 @@ cov = [ ] [[tool.hatch.envs.all.matrix]] -python = ["3.7", "3.8", "3.9", "3.10", "3.11"] +python = ["3.10", "3.11"] [tool.hatch.envs.lint] detached = true @@ -83,12 +80,12 @@ all = [ ] [tool.black] -target-version = ["py37"] +target-version = ["py310"] line-length = 120 skip-string-normalization = true [tool.ruff] -target-version = "py37" +target-version = "py310" line-length = 120 select = [ "A", diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py index 2e350b2..f70c035 100644 --- a/src/infra_rebuild/cli/__init__.py +++ b/src/infra_rebuild/cli/__init__.py @@ -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) diff --git a/src/infra_rebuild/msg/__init__.py b/src/infra_rebuild/msg/__init__.py new file mode 100644 index 0000000..3af001d --- /dev/null +++ b/src/infra_rebuild/msg/__init__.py @@ -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) diff --git a/src/infra_rebuild/operations/__init__.py b/src/infra_rebuild/operations/__init__.py new file mode 100644 index 0000000..4a581e0 --- /dev/null +++ b/src/infra_rebuild/operations/__init__.py @@ -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 From cb9f2e3a1197d37b105f8c07def125f51624533f Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 00:37:51 +0200 Subject: [PATCH 03/10] docs: specify a commit message format to use --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0b3ba2c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing + +## Commit Message Format + +The commit message format is as follows: + +``` +tag(!): short description + +Longer description here if necessary +``` + +The `tag` should be one of the following: + +- `fix` - for a bug fix +- `update` - for a backwards compatible enhancement +- `feature` - for a new feature +- `perf` - for a code change that improves performance +- `refactor` - for a code change that isn't one of `fix`, `update`, `feature` or `perf` +- `build` - for changes that affect the build system or external dependencies +- `test` - for adding or correcting tests +- `style` - for changes to the linter or formatter configuration and its results +- `docs` - for changes to documentation only +- `other` - for anything that isn't covered by the tags above + +If a change is a breaking change then that should be indicated by adding a `!` after the tag. + +These tags are an adapted version of the tags [eslint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) and of the tags [Angular](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#type) use. \ +This commit message format is also inspired by [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). From c7f7771e26c8b19b230dba7986f469cd57b1c33a Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 01:07:39 +0200 Subject: [PATCH 04/10] other: Use "ruff check" instead of just "ruff" as recommended by ruff This is as of ruff 0.3.0. See here: https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md#030 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3eb6ea1..f051452 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,12 +66,12 @@ dependencies = [ [tool.hatch.envs.lint.scripts] typing = "mypy --install-types --non-interactive {args:src/infra_rebuild tests}" style = [ - "ruff {args:.}", + "ruff check {args:.}", "black --check --diff {args:.}", ] fmt = [ "black {args:.}", - "ruff --fix {args:.}", + "ruff check --fix {args:.}", "style", ] all = [ From 7cfa2726d33eac5cf24053c4deb9d5aea0b0a31a Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 00:48:17 +0200 Subject: [PATCH 05/10] other: Use more recent min. vers. for the lint, fmt and typing tooling --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f051452..01c40d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,9 +59,9 @@ python = ["3.10", "3.11"] [tool.hatch.envs.lint] detached = true dependencies = [ - "black>=23.1.0", - "mypy>=1.0.0", - "ruff>=0.0.243", + "black>=24.4.0", + "mypy>=1.10.0", + "ruff>=0.4.0", ] [tool.hatch.envs.lint.scripts] typing = "mypy --install-types --non-interactive {args:src/infra_rebuild tests}" From 3410f647f539ccb6e84338c89cd8ce1fc88f7296 Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 00:54:56 +0200 Subject: [PATCH 06/10] other: Put ruff linter settings in lint section as recommended by ruff --- pyproject.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01c40d8..ba2d00b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,8 @@ skip-string-normalization = true [tool.ruff] target-version = "py310" line-length = 120 + +[tool.ruff.lint] select = [ "A", "ARG", @@ -129,13 +131,13 @@ unfixable = [ "F401", ] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["infra_rebuild"] -[tool.ruff.flake8-tidy-imports] +[tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # Tests can use magic values, assertions, and relative imports "tests/**/*" = ["PLR2004", "S101", "TID252"] From 5c55bd3e97a312885fb08061926f36cc6d584dfd Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 20:18:52 +0200 Subject: [PATCH 07/10] build: Add Nix flake for building x86_64- and aarch64-linux Nix packages --- .gitignore | 1 + flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index eb10265..706c555 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ /dist/ /.coverage* +/result diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4535cb0 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1717428950, + "narHash": "sha256-Pr3lhu2No1GHJarhjt+Jsfxye1wNLoY12E44p0b3VO0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "4fad892a8f64635a55423e4acfefeefb6caf4d0d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-23.11-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9e6a42f --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + description = "A simple NixOS deployment tool using nixos-rebuild internally, but trying to make infrastructure deployment more convenient."; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11-small"; + }; + + outputs = { nixpkgs, ... }: + let + version = "0.0.1"; + in + { + packages = nixpkgs.lib.attrsets.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: + let + pkgs = nixpkgs.legacyPackages."${system}"; + in + rec { + infra-rebuild = pkgs.python3Packages.buildPythonApplication { + pname = "infra-rebuild"; + inherit version; + pyproject = true; + + src = ./.; + + nativeBuildInputs = with pkgs.python3Packages; [ + hatchling + ]; + + propagatedBuildInputs = [ + pkgs.python3Packages.click + pkgs.nixos-rebuild + ]; + + meta = with nixpkgs.lib; { + description = "A simple NixOS deployment tool using nixos-rebuild internally"; + longDescription = "A simple NixOS deployment tool using nixos-rebuild internally, but trying to make infrastructure deployment more convenient."; + homepage = "https://git.hamburg.ccc.de/CCCHH/infra-rebuild"; + license = licenses.gpl3Plus; + }; + }; + default = infra-rebuild; + } + ); + }; +} From 89cb3586c2a887c71b8e2af5af16b664cd0aed34 Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 20:23:48 +0200 Subject: [PATCH 08/10] build: set version statically in pyproject.toml --- pyproject.toml | 5 +---- src/infra_rebuild/__about__.py | 1 - src/infra_rebuild/cli/__init__.py | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 src/infra_rebuild/__about__.py diff --git a/pyproject.toml b/pyproject.toml index ba2d00b..286d8b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "infra-rebuild" -dynamic = ["version"] +version = "0.0.1" description = "A simple NixOS deployment tool using nixos-rebuild internally, but trying to make infrastructure deployment more convenient." readme = "README.md" requires-python = ">=3.10" @@ -33,9 +33,6 @@ Source = "https://git.hamburg.ccc.de/CCCHH/infra-rebuild" [project.scripts] infra-rebuild = "infra_rebuild.cli:infra_rebuild" -[tool.hatch.version] -path = "src/infra_rebuild/__about__.py" - [tool.hatch.envs.default] dependencies = [ "coverage[toml]>=6.5", diff --git a/src/infra_rebuild/__about__.py b/src/infra_rebuild/__about__.py deleted file mode 100644 index f102a9c..0000000 --- a/src/infra_rebuild/__about__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.0.1" diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py index f70c035..fac2d3f 100644 --- a/src/infra_rebuild/cli/__init__.py +++ b/src/infra_rebuild/cli/__init__.py @@ -1,11 +1,10 @@ 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") +@click.version_option(prog_name="infra-rebuild") def infra_rebuild(): pass From 11a2b74aee613c9303c70fede550cca2327ebfc0 Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 21:04:48 +0200 Subject: [PATCH 09/10] docs: redefine commit message "update" tag to be for tagging non-feat. Redefine the commit message "update" tag to be for tagging an enhancement or update, which doesn't qualify as a feature, and also have it not be restricted from tagging a breaking change. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b3ba2c..54b06ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Longer description here if necessary The `tag` should be one of the following: - `fix` - for a bug fix -- `update` - for a backwards compatible enhancement +- `update` - for an enhancement or update not qualifying as a feature - `feature` - for a new feature - `perf` - for a code change that improves performance - `refactor` - for a code change that isn't one of `fix`, `update`, `feature` or `perf` From 3cdd4ba8b57ed95f63be70860b009b518ee08d79 Mon Sep 17 00:00:00 2001 From: June Date: Thu, 6 Jun 2024 21:08:38 +0200 Subject: [PATCH 10/10] update!: switch from one comma-sep. argument to variadic args. for hosts Since the hosts are what nixos-rebuild acts on it makes to have them represented by unlimited variadic arguments similar to how "git add", "cat", "nix build", etc. work. --- src/infra_rebuild/cli/__init__.py | 14 +++++++------- src/infra_rebuild/operations/__init__.py | 3 +-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/infra_rebuild/cli/__init__.py b/src/infra_rebuild/cli/__init__.py index fac2d3f..38030ea 100644 --- a/src/infra_rebuild/cli/__init__.py +++ b/src/infra_rebuild/cli/__init__.py @@ -10,42 +10,42 @@ def infra_rebuild(): @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def build(hosts): operations.run("build", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def build_vm(hosts): operations.run("build-vm", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def build_vm_with_bootloader(hosts): operations.run("build-vm-with-bootloader", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def switch(hosts): operations.run("switch", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def boot(hosts): operations.run("boot", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def test(hosts): operations.run("test", hosts) @infra_rebuild.command() -@click.argument("hosts") +@click.argument("hosts", nargs=-1) def reboot(hosts): operations.run("reboot", hosts) diff --git a/src/infra_rebuild/operations/__init__.py b/src/infra_rebuild/operations/__init__.py index 4a581e0..0ae726e 100644 --- a/src/infra_rebuild/operations/__init__.py +++ b/src/infra_rebuild/operations/__init__.py @@ -23,8 +23,7 @@ def run(operation, hosts): msg.error("Internal Error: The given operation isn't valid.") sys.exit(1) - for host_string in hosts.split(","): - host = host_string.strip() + for host in hosts: if not host: msg.warning("Skipping empty string provided for host.") continue