Skip to content

Commit 0e2e365

Browse files
committed
Add a script to automate the host creation via templates
The script scripts/mkHost.sh automates the generation of a new host, based on a template file. This template looks like a .nix file with placeholder \$KEY fields that will be populated dynamically such as name, site, user, etc.
1 parent 49168fb commit 0e2e365

File tree

2 files changed

+255
-28
lines changed

2 files changed

+255
-28
lines changed

scripts/mkHost.sh

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#! /usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
usage() {
6+
cat <<'EOF'
7+
Usage:
8+
nix run .#mkHost -- <name> <sitename> <username> [--system <x86_64-linux|aarch64-linux>] [--help] [--template <path>] [KEY=VALUE ...]
9+
10+
Arguments:
11+
<name> Hostname / machine name (e.g. alfa, bravo, ...)
12+
<sitename> Logical site / location name (e.g. theta, ...)
13+
<username> User name for the admin user of the new machine
14+
15+
Options:
16+
-s, --system Override detected system (default: based on uname -m)
17+
Allowed: x86_64-linux, aarch64-linux
18+
-t, --template Path to a template file to use instead of /etc/nixos/templates/hosts/default.tmpl
19+
-h, --help Show this help
20+
21+
Extra template variables:
22+
Any additional arguments of the form KEY=VALUE after the options will be
23+
exported to the environment before rendering the template, so they can be
24+
used as \$KEY placeholders inside the template.
25+
EOF
26+
}
27+
28+
info() {
29+
printf "\e[1;32m[INFO]\e[0m %s\n" "$1"
30+
}
31+
32+
error() {
33+
printf "\e[1;31m[ERROR]\e[0m %s\n\n" "$1" >&2
34+
usage >&2
35+
exit 1
36+
}
37+
38+
interaction() {
39+
printf "\e[1;33m[ACTION]\e[0m %s" "$1"
40+
}
41+
42+
prompt() {
43+
local msg="$1"
44+
interaction "$msg [Y/n] "
45+
read -r answer
46+
47+
case "${answer:-Y}" in
48+
[Yy]*)
49+
return 0
50+
;;
51+
*)
52+
return 1
53+
;;
54+
esac
55+
}
56+
57+
if [[ $# -eq 0 ]]; then
58+
usage
59+
exit 0
60+
fi
61+
62+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
63+
usage
64+
exit 0
65+
fi
66+
67+
ARCH="$(uname -m)"
68+
case "$ARCH" in
69+
x86_64)
70+
SYSTEM="x86_64-linux"
71+
;;
72+
aarch64 | arm64)
73+
SYSTEM="aarch64-linux"
74+
;;
75+
*)
76+
SYSTEM=""
77+
;;
78+
esac
79+
80+
if [[ $# -lt 3 ]]; then
81+
error "missing required arguments <name> <sitename> <username>."
82+
fi
83+
84+
NAME="$1"
85+
SITE="$2"
86+
USER="$3"
87+
shift 3
88+
89+
if [[ ! "$NAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then
90+
error "invalid host name '$NAME'.
91+
Allowed characters: letters, numbers, dot, underscore, dash."
92+
fi
93+
94+
if [[ ! "$SITE" =~ ^[a-zA-Z0-9._-]+$ ]]; then
95+
error "invalid site name '$SITE'.
96+
Allowed characters: letters, numbers, dot, underscore, dash."
97+
fi
98+
99+
if [[ ! "$USER" =~ ^[a-z_][a-z0-9_-]*$ ]]; then
100+
error "invalid username '$USER'.
101+
Allowed pattern: ^[a-z_][a-z0-9_-]*$ (like normal Unix usernames)."
102+
fi
103+
104+
BASE_DIR="/etc/nixos"
105+
HOSTS_DIR="$BASE_DIR/hosts"
106+
TARGET_DIR="${HOSTS_DIR}/${NAME}"
107+
TEMPLATE_FILE="$BASE_DIR/templates/hosts/default.tmpl"
108+
EXTRA_VARS=()
109+
110+
while [[ $# -gt 0 ]]; do
111+
case "$1" in
112+
-h | --help)
113+
usage
114+
exit 0
115+
;;
116+
-s | --system)
117+
if [[ $# -lt 2 ]]; then
118+
error "--system requires an argument."
119+
fi
120+
SYSTEM="$2"
121+
case "$SYSTEM" in
122+
x86_64-linux | aarch64-linux) ;;
123+
*)
124+
error "invalid --system value '$SYSTEM'.
125+
Allowed: x86_64-linux, aarch64-linux"
126+
;;
127+
esac
128+
shift 2
129+
;;
130+
-t | --template)
131+
if [[ $# -lt 2 ]]; then
132+
error "--template requires a path argument."
133+
fi
134+
TEMPLATE_FILE="$2"
135+
shift 2
136+
;;
137+
-* | --*)
138+
error "unknown option '$1'."
139+
;;
140+
*)
141+
if [[ "$1" == *=* ]]; then
142+
key="${1%%=*}"
143+
144+
case "$key" in
145+
NAME | SITE | USER | ARCH | SYSTEM | STATE_VERSION | BASE_DIR | HOSTS_DIR | TARGET_DIR | TEMPLATE_FILE | TARGET_FILE | EXTRA_VARS | HOST_ID)
146+
error "extra variable '$key' is reserved and cannot be overridden."
147+
;;
148+
esac
149+
150+
EXTRA_VARS+=("$1")
151+
shift
152+
else
153+
error "unexpected positional argument '$1' after <name> <sitename> <username>."
154+
fi
155+
;;
156+
esac
157+
done
158+
159+
if [[ -z "${SYSTEM:-}" ]]; then
160+
error "unsupported architecture '$ARCH' from uname -m.
161+
Please specify --system x86_64-linux or aarch64-linux explicitly."
162+
fi
163+
164+
if [[ -d "$BASE_DIR" ]]; then
165+
if [[ ! -d "$HOSTS_DIR" ]]; then
166+
info "creating host directory '$HOSTS_DIR'..."
167+
mkdir -p "$HOSTS_DIR"
168+
fi
169+
else
170+
error "this script must be run on a NixOS system (with '$BASE_DIR' available)."
171+
fi
172+
173+
if [[ -e "$TARGET_DIR" ]]; then
174+
error "host directory '$TARGET_DIR' already exists."
175+
fi
176+
177+
mkdir -p "$TARGET_DIR"
178+
179+
if ! command -v nixos-generate-config >/dev/null 2>&1; then
180+
error "nixos-generate-config not found in PATH.
181+
This script must be run on a NixOS system (or with nixos-generate-config available)."
182+
fi
183+
184+
info "generating hardware configuration for $NAME..."
185+
nixos-generate-config --show-hardware-config >"${TARGET_DIR}/hardware.nix"
186+
187+
TARGET_FILE="$TARGET_DIR/default.nix"
188+
189+
if [[ ! -f "$TEMPLATE_FILE" ]]; then
190+
error "template file '$TEMPLATE_FILE' not found."
191+
fi
192+
193+
STATE_VERSION=
194+
if [[ -z "$STATE_VERSION" ]]; then
195+
if command -v nixos-option >/dev/null 2>&1; then
196+
v="$(nixos-option system.stateVersion 2>/dev/null |
197+
sed -nE 's/^[[:space:]]*"([^"]+)".*/\1/p' |
198+
head -n1)"
199+
if [[ -n "$v" && "$v" != "null" ]]; then
200+
STATE_VERSION="$v"
201+
fi
202+
fi
203+
fi
204+
205+
if [[ -z "$STATE_VERSION" ]]; then
206+
if command -v nixos-version >/dev/null 2>&1; then
207+
v="$(nixos-version | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')"
208+
if [[ -n "$v" ]]; then
209+
STATE_VERSION="$v"
210+
fi
211+
fi
212+
fi
213+
214+
if [[ -z "$STATE_VERSION" ]]; then
215+
error "Could not determine system.stateVersion automatically.
216+
Please set it manually or ensure nixos-option / nixos-version are available."
217+
fi
218+
219+
HOST_ID="$(hexdump -n 4 -e '/4 "%08x"' /dev/urandom | tr 'A-F' 'a-f')"
220+
221+
for kv in "${EXTRA_VARS[@]}"; do
222+
export "$kv"
223+
done
224+
export NAME SITE USER STATE_VERSION HOST_ID
225+
226+
227+
if [[ "$TEMPLATE_FILE" == "$BASE_DIR/templates/hosts/default.tmpl" ]]; then
228+
info "generating configuration for $NAME from default template..."
229+
else
230+
info "generating configuration for $NAME from template '$TEMPLATE_FILE'..."
231+
fi
232+
envsubst <"$TEMPLATE_FILE" >"$TARGET_FILE"
233+
234+
if prompt "successfully generated a new config for $NAME. Do you want to edit it now?"; then
235+
"${EDITOR:-vi}" "$TARGET_FILE"
236+
else
237+
info "skipping edit."
238+
fi

hosts/templates/default-configuration.nix renamed to templates/hosts/default.tmpl

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,32 @@ let
1010
in
1111
{
1212
imports = [
13-
# The hardware-dependent options
14-
./hardware-configuration.nix
15-
# All (shared/non-specific) users
16-
../../users/lukas.nix
17-
# All custom modules
13+
# The hardware-dependent options:
14+
./hardware.nix
15+
# Common settings thath apply to all machines:
16+
../common.nix
17+
# Deployment-site specific options:
18+
../../sites/"$SITE".nix
19+
# The users for this system:
20+
../../users/"$USER".nix
21+
# The custom modules:
22+
# TODO: Import any custom modules here.
23+
# Any third-party modules or flakes:
24+
# TODO: Import any thrid-party flakes here such as:
25+
# inputs.sops-nix.nixosModules.sops
1826
];
1927

20-
nix.settings.experimental-features = [
21-
"nix-command"
22-
"flakes"
23-
];
24-
25-
time.timeZone = "Europe/Berlin";
26-
2728
boot = {
2829
loader.grub = {
2930
enable = true;
30-
zfsSupport = true;
3131
efiSupport = true;
3232
efiInstallAsRemovable = true;
33-
mirroredBoots = [
34-
{
35-
devices = [ "nodev" ];
36-
path = "/boot";
37-
}
38-
];
3933
};
40-
zfs.extraPools = [ "zpool" ];
4134
};
4235

4336
networking = {
44-
hostName = "<HOSTNAME>";
45-
hostId = "<SOME_RANDOM_8_CHARS>";
37+
hostName = "$NAME";
38+
hostId = "$HOSTID";
4639
networkmanager.enable = true;
4740
useDHCP = true;
4841
nameservers = [
@@ -57,7 +50,6 @@ in
5750
search = [ "tabby-crocodile.ts.net" ];
5851
};
5952

60-
i18n.defaultLocale = "en_US.UTF-8";
6153
console = {
6254
font = "Lat2-Terminus16";
6355
useXkbConfig = true;
@@ -78,12 +70,9 @@ in
7870
PasswordAuthentication = true;
7971
KbdInteractiveAuthentication = true;
8072
PermitRootLogin = "no";
81-
AllowUsers = [ "lukas" ];
73+
AllowUsers = [ "$USER" ];
8274
};
8375
};
84-
fail2ban = {
85-
enable = true;
86-
};
8776
envfs.enable = true;
8877
tailscale.enable = true;
8978
};
@@ -94,5 +83,5 @@ in
9483
neovim.defaultEditor = true;
9584
};
9685

97-
system.stateVersion = "25.05";
86+
system.stateVersion = "$SYSTEM_VERSION";
9887
}

0 commit comments

Comments
 (0)