Compare commits
10 commits
d085cfd3b9
...
3cdd4ba8b5
Author | SHA1 | Date | |
---|---|---|---|
June | 3cdd4ba8b5 | ||
June | 11a2b74aee | ||
June | 89cb3586c2 | ||
June | 5c55bd3e97 | ||
June | 3410f647f5 | ||
June | 7cfa2726d3 | ||
June | c7f7771e26 | ||
June | cb9f2e3a11 | ||
June | 05c45fe5e3 | ||
June | 500438636a |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__
|
||||||
|
/dist/
|
||||||
|
/.coverage*
|
||||||
|
/result
|
29
CONTRIBUTING.md
Normal file
29
CONTRIBUTING.md
Normal file
|
@ -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 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`
|
||||||
|
- `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/).
|
27
flake.lock
Normal file
27
flake.lock
Normal file
|
@ -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
|
||||||
|
}
|
45
flake.nix
Normal file
45
flake.nix
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
echo "$(tput bold)$(tput setaf 9)$@$(tput sgr0)"
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
echo "$(tput bold)$(tput setaf 12)$@$(tput sgr0)"
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
echo "$(tput bold)$(tput setaf 11)$@$(tput sgr0)"
|
|
|
@ -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
|
|
|
@ -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"
|
|
|
@ -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
|
|
158
pyproject.toml
Normal file
158
pyproject.toml
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "infra-rebuild"
|
||||||
|
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"
|
||||||
|
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.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.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.10", "3.11"]
|
||||||
|
|
||||||
|
[tool.hatch.envs.lint]
|
||||||
|
detached = true
|
||||||
|
dependencies = [
|
||||||
|
"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}"
|
||||||
|
style = [
|
||||||
|
"ruff check {args:.}",
|
||||||
|
"black --check --diff {args:.}",
|
||||||
|
]
|
||||||
|
fmt = [
|
||||||
|
"black {args:.}",
|
||||||
|
"ruff check --fix {args:.}",
|
||||||
|
"style",
|
||||||
|
]
|
||||||
|
all = [
|
||||||
|
"style",
|
||||||
|
"typing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
target-version = ["py310"]
|
||||||
|
line-length = 120
|
||||||
|
skip-string-normalization = true
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
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.lint.isort]
|
||||||
|
known-first-party = ["infra_rebuild"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-tidy-imports]
|
||||||
|
ban-relative-imports = "all"
|
||||||
|
|
||||||
|
[tool.ruff.lint.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:",
|
||||||
|
]
|
0
src/infra_rebuild/__init__.py
Normal file
0
src/infra_rebuild/__init__.py
Normal file
6
src/infra_rebuild/__main__.py
Normal file
6
src/infra_rebuild/__main__.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from infra_rebuild.cli import infra_rebuild
|
||||||
|
|
||||||
|
sys.exit(infra_rebuild())
|
51
src/infra_rebuild/cli/__init__.py
Normal file
51
src/infra_rebuild/cli/__init__.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import click
|
||||||
|
|
||||||
|
from infra_rebuild import operations
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True)
|
||||||
|
@click.version_option(prog_name="infra-rebuild")
|
||||||
|
def infra_rebuild():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def build(hosts):
|
||||||
|
operations.run("build", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def build_vm(hosts):
|
||||||
|
operations.run("build-vm", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def build_vm_with_bootloader(hosts):
|
||||||
|
operations.run("build-vm-with-bootloader", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def switch(hosts):
|
||||||
|
operations.run("switch", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def boot(hosts):
|
||||||
|
operations.run("boot", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
def test(hosts):
|
||||||
|
operations.run("test", hosts)
|
||||||
|
|
||||||
|
|
||||||
|
@infra_rebuild.command()
|
||||||
|
@click.argument("hosts", nargs=-1)
|
||||||
|
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)
|
145
src/infra_rebuild/operations/__init__.py
Normal file
145
src/infra_rebuild/operations/__init__.py
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
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 in hosts:
|
||||||
|
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
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Loading…
Reference in a new issue