Big update. Folder reorganization. Disk selection logic finished. Improved services selection (not done yet).

This commit is contained in:
Raphael Numbus
2025-12-14 13:58:01 +01:00
parent 0e0ed4d3a3
commit f777e608b8
32 changed files with 435 additions and 261 deletions
-1
View File
@@ -1 +0,0 @@
deploy.conf
+1 -2
View File
@@ -1,6 +1,5 @@
agents/
ai-production/
extra-files/
test.sh
test2.sh
test*
deploy.conf
@@ -1,7 +1,8 @@
{ config, lib, ... }:
# --> SnapRAID disks research
let
### --> SnapRAID disks research
contentDiskMounts = lib.attrsets.attrNames (
lib.attrsets.filterAttrs (name: value: lib.strings.hasPrefix "/mnt/content-" name) config.fileSystems
);
@@ -12,12 +13,17 @@ let
(acc: path: acc // { "d${toString (acc.i + 1)}" = path; i = acc.i + 1; })
{ i = 0; }
contentDiskMounts;
### SnapRAID disks research <--
### --> Spindown disks
hardDrives = [ ${DISK_ID_LIST[@]} ];
### Spindown disks <--
in
# SnapRAID disks research <--
# --> MergerFS setup
### --> MergerFS setup
{
fileSystems."/mnt/data-storage" = {
device = "mergerfs";
@@ -32,19 +38,35 @@ in
"srcmounts=${lib.strings.concatStringsSep ":" contentDiskMounts}"
];
};
# MergerFS setup <--
### MergerFS setup <--
# --> SnapRAID setup
### --> SnapRAID setup
services.snapraid = {
enable = true;
contentFiles = map (disk: "${disk}/snapraid.content") contentDiskMounts;
parityFiles = map (disk: "${disk}/snapraid.parity") parityDiskMounts;
dataDisks = builtins.removeAttrs snapraidDataDisks [ "i" ];
};
# SnapRAID setup <--
### SnapRAID setup <--
### --> Disk spindown
systemd.services.hd-idle = {
description = "External HD spin down daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart =
let
idleTime = toString 1800;
hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives;
in
"${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}";
};
};
### Disk spindown <--
}
+92
View File
@@ -0,0 +1,92 @@
{ config, pkgs, ... }:
let
container_name = "gitea";
compose-dir = "docker-compose/gitea";
config-dir = "/mnt/config-storage/docker-data/gitea";
in
{
config = {
environment.etc."${compose-dir}/compose.yaml".text =
/*DB_NAM
yaml
*/
''
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
networks:
gitea_frontend:
gitea_backend:
volumes:
- ${config_dir}/data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=$POSTGRES_HOST:$POSTGRES_PORT
- GITEA__database__NAME=$DB_NAME
- GITEA__database__USER=$DB_USERNAME
- GITEA__database__PASSWD=$DB_PASSWORD
- GITEA__server__SSH_PORT=2424
- GITEA__server__ROOT_URL=gitea.$DOMAIN_NAME
labels:
- traefik.enable=true
- traefik.http.services.gitea.loadbalancer.server.port=3000
- traefik.http.services.gitea.loadbalancer.server.scheme=http
- traefik.http.routers.gitea-https.entrypoints=websecure
- traefik.http.routers.gitea-https.rule=Host(`gitea.$DOMAIN_NAME`)
- traefik.http.routers.gitea-https.tls=true
- traefik.http.routers.gitea-https.tls.certresolver=cloudflare
depends_on:
- gitea-database
restart: unless-stopped
gitea-database:
image: docker.io/library/postgres:17.5
container_name: gitea-database
environment:
- POSTGRES_USER=$DB_USERNAME
- POSTGRES_PASSWORD=$DB_PASSWORD
- POSTGRES_DB=$DB_NAME
networks:
gitea_backend:
volumes:
- gitea-database:/var/lib/postgresql/data
restart: unless-stopped
volumes:
gitea-database:
networks:
gitea_frontend:
external: true
gitea_backend:
external: true
'';
systemd.services.gitea = {
description = "Docker container : ${container_name}";
after = [ "network.target" "docker.service" "docker.socket" "traefik.service" ];
requires = [ "docker.service" ];
wantedBy = ["multi-user.target"];
path = [ pkgs.docker ];
serviceConfig = {
Type = "exec";
# Pull the latest image before running
ExecStartPre = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml pull";
# Bring the service up
ExecStart = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml up --remove-orphans";
# Take it down gracefully
ExecStop = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml down";
Restart = "on-failure";
};
};
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config, pkgs, ... }:
let
container_name = "it-tools";
compose-dir = "docker-compose/it-tools";
in
{
config = {
environment.etc."${compose-dir}/compose.yaml".text =
/*
yaml
*/
''
services:
it-tools:
container_name: it-tools
image: corentinth/it-tools
networks:
it-tools:
labels:
- traefik.enable=true
- traefik.http.services.it-tools.loadbalancer.server.port=80
- traefik.http.services.it-tools.loadbalancer.server.scheme=http
- traefik.http.routers.it-tools-https.entrypoints=websecure
- traefik.http.routers.it-tools-https.rule=Host(`it-tools.$DOMAIN_NAME`)
- traefik.http.routers.it-tools-https.tls=true
- traefik.http.routers.it-tools-https.tls.certresolver=cloudflare
restart: unless-stopped
networks:
it-tools:
external: true
'';
systemd.services.it-tools = {
description = "Docker container : ${container_name}";
after = [ "network.target" "docker.service" "docker.socket" "traefik.service" ];
requires = [ "docker.service" ];
wantedBy = ["multi-user.target"];
path = [ pkgs.docker ];
serviceConfig = {
Type = "exec";
# Pull the latest image before running
ExecStartPre = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml pull";
# Bring the service up
ExecStart = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml up --remove-orphans";
# Take it down gracefully
ExecStop = "${pkgs.docker}/bin/docker compose -f /etc/${compose-dir}/compose.yaml down";
Restart = "on-failure";
};
};
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ TARGET_HOST="192.168.1.10"
SSH_PUBLIC_KEY="ssh-ed25519 AAAAoefzefpoipoeCEZJCPEACPAcjapjcpajepcjAPJECJPEJAPJAZ yours@yourdomain.com"
# TRAEFIK SETTINGS
DOMAIN_NAME="yourdomain.com"
EMAIL_ADDRESS="no-reply@yourdomain.com"
EMAIL_ADDRESS="your-mail@yourdomain.com"
CF_DNS_API_TOKEN="yourToken"
#SMTP SETTINGS
SENDER_EMAIL_ADDRESS="youraddress@gmail.com"
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p gum openssl sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
+12 -4
View File
@@ -1,4 +1,5 @@
ssh_public_keys: $SSH_PUBLIC_KEY
sender_email_address_password: $SENDER_EMAIL_ADDRESS_PASSWORD
docker:
nextcloud: |
@@ -18,9 +19,9 @@ docker:
passbolt: |
DOMAIN_NAME=$DOMAIN_NAME
TZ=Europe/Paris
PASSBOLT_MYSQL_DATABASE=$PASSBOLT_MYSQL_DATABASE
PASSBOLT_MYSQL_USER=$PASSBOLT_MYSQL_USER
PASSBOLT_MYSQL_PASSWORD=$PASSBOLT_MYSQL_PASSWORD
PASSBOLT_MYSQL_DATABASE=$PASSBOLT_DB_NAME
PASSBOLT_MYSQL_USER=$PASSBOLT_DB_USERNAME
PASSBOLT_MYSQL_PASSWORD=$PASSBOLT_DB_PASSWORD
SENDER_EMAIL_ADDRESS=$SENDER_EMAIL_ADDRESS
SENDER_EMAIL_ADDRESS_PASSWORD=$SENDER_EMAIL_ADDRESS_PASSWORD
SENDER_EMAIL_DOMAIN=$SENDER_EMAIL_DOMAIN
@@ -41,10 +42,17 @@ docker:
IMMICH_TRUSTED_PROXIES=172.16.50.253
REDIS_HOSTNAME=immich-redis
DB_HOSTNAME=immich-database
DB_DATABASE_NAME=$IMMICH_DB_DATABASE_NAME
DB_DATABASE_NAME=$IMMICH_DB_NAME
DB_USERNAME=$IMMICH_DB_USERNAME
DB_PASSWORD=$IMMICH_DB_PASSWORD
DB_DATA_LOCATION=/mnt/config-storage/docker-data/immich/database
gitea: |
DOMAIN_NAME=$DOMAIN_NAME
POSTGRES_HOST=gitea-database
POSTGRES_PORT=5432
DB_NAME=$GITEA_DB_NAME
DB_USERNAME=$GITEA_DB_USERNAME
DB_PASSWORD=$GITEA_DB_PASSWORD
disks:
content-disk-1: $CONTENT_DISK_1_KEY
+144 -153
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p gum openssl sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
#!nix-shell -i bash -p gum xkcdpass openssl sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
necessary_credentials() {
#TARGET SETTINGS
@@ -127,148 +127,126 @@ hardware_detection() {
services_selection() {
echo -e "\n\n ➡️ You will now select the services you want installed on your server:"
declare -A SERVICE_MAP
SERVICE_MAP["Pi-Hole: Block ads on all your devices"]="pihole"
SERVICE_MAP["Home Assistant: Manage your smart home or security cameras"]="hass"
SERVICE_MAP["Passbolt: Secure password manager with collaboration features"]="passbolt"
SERVICE_MAP["Frigate [Home Assistant required]: Secure your house with security cameras"]="frigate"
SERVICE_MAP["Nextcloud: No fuss Office 365 replacement"]="nextcloud"
SERVICE_MAP["Immich: Pictures and videos backup with local machine-learning"]="immich"
AVAILABLE_SERVICES=( "frigate" "gitea" "home-assistant" "immich" "it-tools" \
"nextcloud" "passbolt" "pi-hole" )
AVAILABLE_SERVICES_NUMBER=${#AVAILABLE_SERVICES[@]}
mapfile -t SERVICE_DESCRIPTIONS < <(for key in "${!SERVICE_MAP[@]}"; do echo "$key"; done | sort)
SERVICES_DESCRIPTION=( "Pi-Hole : Block ads on all your devices" \
"Immich : Pictures and videos backup with local machine-learning" \
"Nextcloud : No fuss Office 365 replacement" \
"Passbolt: Security-first password manager with collaboration features" \
"Home-Assistant : Manage your smart home and security cameras" \
"Frigate [Home Assistant required] : Secure your house with security cameras" \
"Gitea : Your own git platform" \
"IT-tools : A set of useful tools when doing IT" \
)
SELECTED_DESCRIPTIONS_STRING=$(gum choose --no-limit --header "Homelab services:" "${SERVICE_DESCRIPTIONS[@]}")
SELECTED_SERVICES_DESCRIPTION=$(gum choose --no-limit --header "Homelab services:" "${SERVICES_DESCRIPTION[@]}")
SERVICES=()
if [[ -n "$SELECTED_DESCRIPTIONS_STRING" ]]; then
while IFS= read -r line; do
SERVICES+=("${SERVICE_MAP[$line]}")
done <<< "$SELECTED_DESCRIPTIONS_STRING"
fi
for i in $(seq 0 $((${#AVAILABLE_SERVICES[@]} - 1))); do
if printf '%s' "$SELECTED_SERVICES_DESCRIPTION" | grep -iq "${AVAILABLE_SERVICES[$i]}"; then
SELECTED_SERVICES+=(${AVAILABLE_SERVICES[$i]})
fi
done
}
files_generation() {
echo -e "\n\nGenerating necessary folder tree..."
echo -e "\n ✅ Writing configuration files for the selected homelab services..."
# Traefik
mkdir -p extra-files/mnt/config-storage/traefik/config/conf/
cp ./config-files/docker/compose/traefik.nix ./config-files/docker/compose/traefik.nix
envsubst < config-files/docker/config/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
for service in "${SELECTED_SERVICES[@]}"; do
# Frigate
if [[ "$service" == "frigate" ]]; then
echo -e "\n ✅ Adapting the docker configuration to your hardware..."
FRIGATE_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
if [[ "$TARGET_USB_CORAL" == "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n"
if [[ -n "$FRIGATE_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${FRIGATE_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- frigate devices --- #|$REPLACEMENT|" ./config-files/docker/compose/frigate.nix
else
sed -i.bak "/# --- frigate devices --- #/d" ./config-files/docker/compose/frigate.nix
fi
# Home-Assistant
elif [[ "$service" == "home-assistant" ]]; then
if [[ -n "$TARGET_ZIGBEE_DEVICE" ]]; then
REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0"
sed -i.bak "s|# --- hass devices --- #|$REPLACEMENT|" ./config-files/docker/compose/hass.nix
else
sed -i.bak "/# --- hass devices --- #/d" ./config-files/docker/compose/hass.nix
fi
export HOME_ASSISTANT_MQTT_USER="$(xkcdpass -d "-" -n 2)"
export HOME_ASSISTANT_MQTT_PASSWORD="$(xkcdpass -d "-")"
mkdir -p extra-files/mnt/config-storage/hass/mqtt/config/
mkdir -p extra-files/mnt/config-storage/hass/mqtt/data/
envsubst < config-files/docker/config/hass/mosquitto.conf > extra-files/mnt/config-storage/hass/mqtt/config/mosquitto.conf
touch extra-files/mnt/config-storage/hass/mqtt/config/password.txt
chmod 0700 extra-files/mnt/config-storage/hass/mqtt/config/password.txt
mosquitto_passwd -b extra-files/mnt/config-storage/hass/mqtt/config/password.txt $HOME_ASSISTANT_MQTT_USER $HOME_ASSISTANT_MQTT_PASSWORD
# Passbolt
elif [[ "$service" == "passbolt" ]]; then
export PASSBOLT_DB_NAME="$(xkcdpass -d "-" -n 2)"
export PASSBOLT_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export PASSBOLT_DB_PASSWORD="$(xkcdpass -d "-")"
envsubst < config-files/docker/config/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
envsubst < config-files/docker/config/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
# Pi-Hole
elif [[ "$service" == "pi-hole" ]]; then
export FTLCONF_WEBSERVER_PASSWORD="$(xkcdpass -d "-")"
# Immich
elif [[ "$service" == "immich" ]]; then
IMMICH_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
IMMICH_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
if [[ -n "$IMMICH_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${IMMICH_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- immich devices --- #|$REPLACEMENT|" ./config-files/docker/compose/immich.nix
else
sed -i.bak "/# --- immich devices --- #/d" ./config-files/docker/compose/immich.nix
fi
export IMMICH_DB_NAME="$(xkcdpass -d "-" -n 2)"
export IMMICH_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export IMMICH_DB_PASSWORD="$(xkcdpass -d "-")"
mkdir -p extra-files/mnt/data-storage/immich/
elif [[ "$service" == "gitea" ]]; then
export GITEA_DB_NAME="$(xkcdpass -d "-" -n 2)"
export GITEA_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export GITEA_DB_PASSWORD="$(xkcdpass -d "-")"
elif [[ "$service" == "nextcloud" ]]; then
envsubst < config-files/docker/config/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
mkdir -p extra-files/mnt/data-storage/nextcloud/
fi
cp ./config-files/docker/compose/${service}.nix ./config-files/docker/compose/${service}.nix
done
echo -e "\n ✅ Generating sops-nix keys..."
mkdir -p extra-files/etc/secrets/disks/
mkdir -p extra-files/var/lib/sops-nix/
mkdir -p extra-files/etc/nixos/secrets/
mkdir -p extra-files/mnt/config-storage/traefik/config/conf/
mkdir -p extra-files/mnt/config-storage/hass/mqtt/config/
mkdir -p extra-files/mnt/config-storage/hass/mqtt/data/
mkdir -p extra-files/mnt/data-storage/nextcloud/
mkdir -p extra-files/mnt/data-storage/immich/
echo -e "\n ✅ Generating sops-nix keys..."
ssh-to-age -private-key -i extra-files/home/numbus-admin/.ssh/id_ed25519 > extra-files/var/lib/sops-nix/key.txt
export SOPS_PUBLIC_KEY=$(age-keygen -y extra-files/var/lib/sops-nix/key.txt)
echo -e "\n ✅ Generating sops-nix configuration files..."
envsubst < config-files/sops-nix/.sops.yaml > extra-files/etc/nixos/.sops.yaml
echo -e "\n ✅ Generating secure random database passwords..."
export HOME_ASSISTANT_MQTT_USER="$(openssl rand -hex 10)"
export HOME_ASSISTANT_MQTT_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
export PASSBOLT_MYSQL_DATABASE="$(openssl rand -hex 10)"
export PASSBOLT_MYSQL_USER="$(openssl rand -hex 10)"
export PASSBOLT_MYSQL_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
export FTLCONF_WEBSERVER_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
export IMMICH_DB_DATABASE_NAME="$(openssl rand -hex 10)"
export IMMICH_DB_USERNAME="$(openssl rand -hex 10)"
export IMMICH_DB_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
export CONTENT_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export CONTENT_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export CONTENT_DISK_3_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export CONTENT_DISK_4_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export CONTENT_DISK_5_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export CONTENT_DISK_6_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
export PARITY_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
export PARITY_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
export PARITY_DISK_3_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
export BOOT_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
export BOOT_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
echo -e "\n ✅ Generating disk keyfiles in /etc/secrets/disks/..."
for i in {1..6}; do var="CONTENT_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/content-disk-$i"; done
for i in {1..3}; do var="PARITY_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/parity-disk-$i"; done
for i in {1..2}; do var="BOOT_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/boot-disk-$i"; done
echo "$REMOTE_PASS" | ssh_to_host """
sudo -S mkdir -p /etc/secrets/disks/
echo -n $CONTENT_DISK_1_KEY | sudo -S tee /etc/secrets/disks/content-disk-1 > /dev/null
echo -n $CONTENT_DISK_2_KEY | sudo -S tee /etc/secrets/disks/content-disk-2 > /dev/null
echo -n $CONTENT_DISK_3_KEY | sudo -S tee /etc/secrets/disks/content-disk-3 > /dev/null
echo -n $CONTENT_DISK_4_KEY | sudo -S tee /etc/secrets/disks/content-disk-4 > /dev/null
echo -n $CONTENT_DISK_5_KEY | sudo -S tee /etc/secrets/disks/content-disk-5 > /dev/null
echo -n $CONTENT_DISK_6_KEY | sudo -S tee /etc/secrets/disks/content-disk-6 > /dev/null
echo -n $PARITY_DISK_1_KEY | sudo -S tee /etc/secrets/disks/parity-disk-1 > /dev/null
echo -n $PARITY_DISK_2_KEY | sudo -S tee /etc/secrets/disks/parity-disk-2 > /dev/null
echo -n $PARITY_DISK_3_KEY | sudo -S tee /etc/secrets/disks/parity-disk-3 > /dev/null
echo -n $BOOT_DISK_1_KEY | sudo -S tee /etc/secrets/disks/boot-disk-1 > /dev/null
echo -n $BOOT_DISK_2_KEY | sudo -S tee /etc/secrets/disks/boot-disk-2 > /dev/null
"""
echo -e "\n ✅ Encrypting secrets in the correct file..."
envsubst < "config-files/sops-nix/secrets.yaml" | sops encrypt --filename-override secrets.yaml \
--input-type yaml --output-type yaml \
--age $SOPS_PUBLIC_KEY \
--output extra-files/etc/nixos/secrets/secrets.yaml
cp -avu extra-files/etc/nixos/secrets/secrets.yaml ./nix-config/secrets/secrets.yaml
echo -e "\n ✅ Writing correct ips to configuration.nix..."
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g ./nix-config/configuration.nix
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g ./nix-config/configuration.nix
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g ./nix-config/misc/networking.nix
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g ./nix-config/misc/networking.nix
echo -e "\n ✅ Adapting the docker configuration to your hardware..."
FRIGATE_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
fi
if [[ "$TARGET_USB_CORAL" == "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n"
fi
if [[ -n "$FRIGATE_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${FRIGATE_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- frigate devices --- #|$REPLACEMENT|" docker/frigate.original
else
sed -i.bak "/# --- frigate devices --- #/d" docker/frigate.original
fi
IMMICH_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
IMMICH_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
fi
if [[ -n "$IMMICH_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${IMMICH_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- immich devices --- #|$REPLACEMENT|" docker/immich.original
else
sed -i.bak "/# --- immich devices --- #/d" docker/immich.original
fi
if [[ -n "$TARGET_ZIGBEE_DEVICE" ]]; then
REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0"
sed -i.bak "s|# --- hass devices --- #|$REPLACEMENT|" docker/hass.original
else
sed -i.bak "/# --- hass devices --- #/d" docker/hass.original
fi
echo -e "\n ✅ Copying configuration files for the selected homelab services..."
cp docker/traefik.original docker/traefik.nix
for service in "${SERVICES[@]}"; do
cp docker/${service}.original docker/${service}.nix
done
echo -e "\n ✅ Writing docker configuration files..."
envsubst < config-files/docker/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
envsubst < config-files/docker/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
envsubst < config-files/docker/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
envsubst < config-files/docker/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
envsubst < config-files/docker/hass/mosquitto.conf > extra-files/mnt/config-storage/hass/mqtt/config/mosquitto.conf
touch extra-files/mnt/config-storage/hass/mqtt/config/password.txt
chmod 0700 extra-files/mnt/config-storage/hass/mqtt/config/password.txt
mosquitto_passwd -b extra-files/mnt/config-storage/hass/mqtt/config/password.txt $HOME_ASSISTANT_MQTT_USER $HOME_ASSISTANT_MQTT_PASSWORD
echo -e "\n ✅ Copying the configuration to the new machine..."
cp -ravu ./nix-config/* extra-files/etc/nixos/
}
disk_config_generation() {
@@ -284,8 +262,6 @@ disk_config_generation() {
echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)"
### Disk wiping warning <--
### --> Get disk information
DISK_DETAILS=$(ssh darky "
# Declare arrays and variables
@@ -340,19 +316,17 @@ echo \"\${DISK_SIZE[@]}\"
read -r -a DISK_SIZE <<<"${LINES[5]}"
### Get disk information <--
### --> Disk selection
TOTAL_NUMBER_OF_DISKS=${#DISK_NAME[@]}
if [ "$TOTAL_NUMBER_OF_DISKS" -eq 0 ]; then
if [ "${#DISK_NAME[@]}" -eq 0 ]; then
echo -e "\n\n ❌ No disks found on the target host. Aborting."
exit 1
fi
HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path")
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS-1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \
"${DISK_NAME[$i]}" "${DISK_TYPE[$i]}" "${DISK_SIZE[$i]}" \
"${DISK_HEALTH[$i]}" "${DISK_DEVPATH[$i]}")
@@ -363,13 +337,11 @@ echo \"\${DISK_SIZE[@]}\"
SELECTED_BOOT_DISK=$(gum choose --limit 2 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
j=0
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS - 1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
if printf '%s' "$SELECTED_BOOT_DISK" | grep -iq "${DISK_NAME[$i]}"; then
((j++))
export declare "BOOT_DISK_${j}_ID=${DISK_ID[$i]}"
unset "GUM_PRINTED_ELEMENTS[${i}]"
((NUMBER_OF_BOOT_DISKS++))
export declare "BOOT_DISK_${NUMBER_OF_BOOT_DISKS}_ID=${DISK_ID[$i]}"
unset "GUM_PRINTED_ELEMENTS[${i}]"
fi
done
@@ -387,7 +359,7 @@ echo \"\${DISK_SIZE[@]}\"
SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS - 1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[$i]}"; then
((NUMBER_OF_DATA_DISKS++))
fi
@@ -398,23 +370,26 @@ echo \"\${DISK_SIZE[@]}\"
elif [[ "$NUMBER_OF_DATA_DISKS" == "1" ]]; then
echo -e "\n\n ⚠️ One data disk selected, continuing with striped boot disk configuration."
echo -e " Consider using AT LEAST 2 data disks instead to get data protection features on the data disks."
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS - 1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[$i]}"; then
NUMBER_OF_CONTENT_DISKS="1"
NUMBER_OF_PARITY_DISKS="0"
export CONTENT_DISK_1_ID="${DISK_ID[$i]}"
export DISK_1_ID_LIST+=(${CONTENT_DISK_1_ID})
fi
done
elif [[ "$NUMBER_OF_DATA_DISKS" == "2" ]]; then
NUMBER_OF_CONTENT_DISKS="0"
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS - 1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[$i]}"; then
if [[ "$NUMBER_OF_CONTENT_DISKS" == "0" ]]; then
NUMBER_OF_CONTENT_DISKS="1"
export CONTENT_DISK_1_ID="${DISK_ID[$i]}"
export DISK_ID_LIST+=(${CONTENT_DISK_1_ID})
else
NUMBER_OF_PARITY_DISKS="1"
export PARITY_DISK_1_ID="${DISK_ID[$i]}"
export DISK_ID_LIST+=(${PARITY_DISK_1_ID})
fi
fi
done
@@ -425,14 +400,16 @@ echo \"\${DISK_SIZE[@]}\"
k="$NUMBER_OF_CONTENT_DISKS"
l="1"
m="1"
for i in $(seq 0 $((TOTAL_NUMBER_OF_DISKS - 1))); do
for i in $(seq 0 $((${#DISK_NAME[@]} - 1))); do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[$i]}"; then
if [[ "$k" -gt 0 ]]; then
declare "CONTENT_DISK_${l}_ID=${DISK_ID[$i]}"
export DISK_ID_LIST+=(${CONTENT_DISK_${l}_ID})
k=$((k - 1))
((l++))
elif [[ "$j" -gt 0 ]]; then
declare "PARITY_DISK_${m}_ID=${DISK_ID[$i]}"
export DISK_ID_LIST+=(${PARITY_DISK_${m}_ID})
j=$((j - 1))
((m++))
fi
@@ -441,8 +418,6 @@ echo \"\${DISK_SIZE[@]}\"
fi
### Disk selection <--
### --> Selection recap
RECAP_CONTENT=$(cat <<EOF
### Disk Configuration Summary
@@ -467,31 +442,31 @@ EOF
gum confirm "Proceed with this disk configuration?" || { echo -e "\n\n ❌ Aborting as requested."; exit 1; }
### Selection recap <--
### --> Config generation
echo -e "\n\n ✅ Generating disko configuration from templates..."
TEMPLATE_FILE="config-files/disks/boot-${NUMBER_OF_BOOT_DISKS}.nix"
TEMPLATE_FILE="config-files/disks/templates/boot-${NUMBER_OF_BOOT_DISKS}.nix"
(envsubst < "$TEMPLATE_FILE") > ./nix-config/disks/disko.nix
echo -e "\n ✅ Generated boot disk configuration."
# Mirror configuration
if [[ "$NUMBER_OF_CONTENT_DISKS" == 1 && "$NUMBER_OF_PARITY_DISKS" == 1 ]]; then
(envsubst < "config-files/disks/mirror.nix") >> ./nix-config/disks/disko.nix
(envsubst < "config-files/disks/templates/mirror.nix") >> ./nix-config/disks/disko.nix
# SnapRAID configuration
elif [[ "$NUMBER_OF_CONTENT_DISKS" -gt 0 ]]; then
(envsubst < "nix-config/disks/snapraid.nix") >> ./nix-config/disks/snapraid.nix
sed -i "s|# ./disks/snapraid.nix| ./disks/snapraid.nix|" ./nix-config/configuration.nix
for i in $(seq 1 $NUMBER_OF_CONTENT_DISKS); do
export i
LOOP_DISK="CONTENT_DISK_${i}_ID"
export CONTENT_DISK_ID=${!LOOP_DISK}
(envsubst < "config-files/disks/content.nix") >> ./nix-config/disks/disko.nix
(envsubst < "config-files/disks/templates/content.nix") >> ./nix-config/disks/disko.nix
done
echo -e "\n ✅ Generated $NUMBER_OF_CONTENT_DISKS data disk configuration(s)."
for i in $(seq 1 $NUMBER_OF_PARITY_DISKS); do
export i
LOOP_DISK="PARITY_DISK_${i}_ID"
export PARITY_DISK_ID=${!LOOP_DISK}
(envsubst < "config-files/disks/parity.nix") >> ./nix-config/disks/disko.nix
(envsubst < "config-files/disks/templates/parity.nix") >> ./nix-config/disks/disko.nix
done
echo -e "\n ✅ Generated $NUMBER_OF_PARITY_DISKS parity disk configuration(s)."
fi
@@ -504,14 +479,12 @@ EOF
echo -e "\n ✅ Final disko configuration created."
### Config generation <--
### --> Generate automatic unlock configuration
if [[ "$NUMBER_OF_CONTENT_DISKS" -gt 1 && "$NUMBER_OF_PARITY_DISKS" -gt 0 ]]; then
echo -e "\n ✅ Generating automatic disk unlocking configuration..."
sed -i '$ d' ./nix-config/disks/snapraid.nix
sed -i '$ d' ./config-files/disks/snapraid.nix
cat <<EOF >> ./nix-config/disks/snapraid.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
# --> Automatic data disks unlock, generated by deploy.sh on $(date)
boot.initrd.luks.devices = {
EOF
@@ -519,7 +492,7 @@ EOF
if [[ "$NUMBER_OF_CONTENT_DISKS" -gt 0 ]]; then
for i in $(seq 1 $NUMBER_OF_CONTENT_DISKS); do
LOOP_DISK="CONTENT_DISK_$i"
cat <<EOF >> ./nix-config/disks/snapraid.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
"crypted-content-disk-${i}" = {
device = "${!LOOP_DISK}";
keyFile = "/etc/secrets/disks/content-disk-${i}";
@@ -531,7 +504,7 @@ EOF
if [[ "$NUMBER_OF_PARITY_DISKS" -gt 0 ]]; then
for i in $(seq 1 $NUMBER_OF_PARITY_DISKS); do
LOOP_DISK="PARITY_DISK_$i"
cat <<EOF >> ./nix-config/disks/snapraid.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
"crypted-parity-disk-${i}" = {
device = "${!LOOP_DISK}";
keyFile = "/etc/secrets/disks/parity-disk-${i}";
@@ -540,13 +513,27 @@ EOF
done
fi
cat <<'EOF' >> ./nix-config/disks/snapraid.nix
cat <<'EOF' >> ./config-files/disks/snapraid.nix
# Automatic data disks unlock <--
};
}
EOF
fi
cp -avu ./config-files/disks/snapraid.nix ./nixos-config/disks/
### Generate automatic unlock configuration <--
### --> Generate unlock keys
for i in $NUMBER_OF_BOOT_DISKS; do
declare "/etc/secrets/disks/boot-disk-${i}=$(xkcdpass -d "-")"
done
for i in $NUMBER_OF_CONTENT_DISKS; do
declare "/etc/secrets/disks/content-disk-${i}=$(xkcdpass -d "-")"
done
for i in $NUMBER_OF_PARITY_DISKS; do
declare "/etc/secrets/disks/parity-disk-${i}=$(xkcdpass -d "-")"
done
### Generate unlock keys <--
}
deploy() {
@@ -555,7 +542,7 @@ deploy() {
--generate-hardware-config nixos-generate-config ./nix-config/hardware-configuration.nix \
--flake ./nix-config#numbus-server \
--extra-files extra-files \
--chown "/home/numbus-admin/" 1000:1000 \
--chown "/home/numbusing a us-admin/" 1000:1000 \
--target-host nixos@$TARGET_HOST
echo -e "\n\n ✅ Installation successfull !"
@@ -590,7 +577,12 @@ EOF
}
postrun_action() {
echo ""
echo -e "\n\n Now the remote machine will reboot. You will need to input the boot disk(s) passphrase.
This will be the only time you will have to do so, it will be automatic in the future."
gum spin --spinner dot --title "Rebooting the remote..." -- sleep 60
ssh
# Add TPM2 boot disk decryption
# Add pcr-check.nix
}
@@ -663,8 +655,7 @@ elif [[ "$ACTION_ANSWER" == "[2] 💽 Deploy NixOS on a remote machine with a fi
elif [[ "$ACTION_ANSWER" == "[3] 🛠️ Update a NixOS remote machine" ]]; then
echo -e "\n ➡️ Proceeding with update…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : make sure the NixOS installation you want
to update is up-and-running, accessible with SSH.
"
to update is up-and-running, accessible with SSH."
gum confirm "Do you understand and wish to proceed?" || { echo " ❌ Aborting as requested."; exit 1; }
nixos_update
else
-76
View File
@@ -1,76 +0,0 @@
I am working on a homelab deployer tool. The description of this homelab deployer tool is available in the README.md file, you shall read it to better understand your job. Your job as a NixOS expert will be to help me change the current broken configuration into my wanted configuration.
# Here is how I want my setup to be
### Disks selection
My script allows the selection of disks.
The disks are separated in two categories : boot and data disks. Data disks include content disks and parity disks.
First, the user chooses the boot disks : he can choose one boot disk or two boot disks in a mirror setup. User has to choose at least one boot disk.
Then, the user chooses data disks. He can choose how many data disks he desires up to 9, or no disks at all. Then the scripts automatically assigns data disks to the content and parity disks according to 3 conditions : <br>
- if there is only one data disk selected, it must be a content disk <br>
- if more than one data disk is selected, the larger (or equal if all disks are the same size) disks are necessarily parity disks <br>
- if more than one data disk is selected, the content/parity repartition must be 1 parity disk for up to 2 content disks. <br>
### RAID configuration
If there is only one boot disk selected, the boot disk will be striped. If there are 2 boot disks selected, it will be a mirror.
The data disks mountpoints are dynamically and logically set : <br>
- /mnt/content-1 <br>
- /mnt/content-2 <br>
- /mnt/content-3 <br>
- /mnt/content-4 <br>
- /mnt/content-5 <br>
- /mnt/content-6 <br>
- /mnt/parity-1 <br>
- /mnt/parity-2 <br>
- /mnt/parity-3 <br>
SnapRAID is used to get a RAID configuration working with the content and parity drives even if their size is not the same. MergerFS is used to obtain one clean path to the data storage. The script uses a combination of code to find the UUIDs of the disks to reference them in the final disk-config.nix configuration file.
### Disks unlocking
The boot disks are unlocked manually by providing the passphrase on boot.
The data disks are unlocked automatically on boot using a keyfile that is located on the root partition. This means that I need to unlock the boot disk(s), and once it is unlocked the keyfiles for the data disks are decrypted and ready to be used.
The keyfile are dynamically and logically ordered and referenced on the root partition : <br>
- /etc/secrets/disks/content-disk-1 <br>
- /etc/secrets/disks/content-disk-2 <br>
- /etc/secrets/disks/content-disk-3 <br>
- /etc/secrets/disks/content-disk-4 <br>
- /etc/secrets/disks/content-disk-5 <br>
- /etc/secrets/disks/content-disk-6 <br>
- /etc/secrets/disks/parity-disk-1 <br>
- /etc/secrets/disks/parity-disk-2 <br>
- /etc/secrets/disks/parity-disk-3 <br>
The LUKS partition on the disks are dynamically and logically ordered : <br>
- crypted-content-disk-1 <br>
- crypted-content-disk-2 <br>
- crypted-content-disk-3 <br>
- crypted-content-disk-4 <br>
- crypted-content-disk-5 <br>
- crypted-content-disk-6 <br>
- crypted-parity-disk-1 <br>
- crypted-parity-disk-2 <br>
- crypted-parity-disk-3 <br>
To automatically unlock the data disks, you need to set a crypttab entry. Since we are using NixOS, we will set the boot.initrd.luks.devices option.
This option needs to following : volume-name (i.e. crypted-content-1 for example), encrypted-device (i.e. the UUID path), key-file (i.e. the keyfile path). Here is a static example for a disk.
```
boot.initrd.luks.devices = {
"my-device-mapper" = {
device = "/dev/disk/by-uuid/YOUR-UUID-HERE";
keyFile = "/path/to/your-keyfile";
};
};
```
Volume names are logically ordered : crypted-data-1, crypted-data-2, crypted-data-3, [...], crypted-data-6, or crypted-parity-1, [...], crypted-parity-3.
Encrypted-device is the path to the device, using the /dev/by-id/YOUR-UUID-HERE UUID path. This is the tricky part since the disks are dynamically selected. The configuration needs to find the correct UUIDs, the same ones as those selected in the script.
Key-file is the path to the keyfile which are logically ordered : /etc/secrets/disks/data-disk-1, /etc/secrets/disks/data-disk-2, /etc/secrets/disks/data-disk-3, [...], /etc/secrets/disks/data-disk-6 or /etc/secrets/disks/parity-disk-1, [...], /etc/secrets/disks/parity-disk-3.
+1 -17
View File
@@ -21,6 +21,7 @@
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
sops.age.generateKey = true;
sops.secrets."ssh_public_keys" = { owner = "numbus-admin"; path = "/etc/ssh/authorized_keys.d/numbus-admin"; };
sops.secrets."sender_email_address_password" = {};
sops.secrets."docker/frigate" = { owner = "numbus-admin"; path = "/etc/docker-compose/frigate/.env"; };
sops.secrets."docker/traefik" = { owner = "numbus-admin"; path = "/etc/docker-compose/traefik/.env"; };
sops.secrets."docker/nextcloud" = { owner = "numbus-admin"; path = "/etc/docker-compose/nextcloud/.env"; };
@@ -109,23 +110,6 @@
data-root = "/mnt/config-storage/docker-volumes/";
};
# Enable networking and firewall
networking.interfaces.eth0.ipv4.addresses = [
{
address = "HOME_SERVER_IP";
prefixLength = 24;
}
];
networking.defaultGateway = "HOME_ROUTER_IP";
networking.nameservers = [ "HOME_SERVER_IP" "9.9.9.9" ];
networking.networkmanager.enable = true;
networking.firewall.enable = true;
# Open ports in the firewall
networking.firewall.allowPing = false;
networking.firewall.allowedTCPPorts = [ 53 80 443 ];
networking.firewall.allowedUDPPorts = [ 53 ];
# Hostname
networking.hostName = "numbus-server";
+1 -1
View File
@@ -1,7 +1,7 @@
{
inputs = {
# Core Nixpkgs
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
# Diskpartitioning helper
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
+72
View File
@@ -0,0 +1,72 @@
{ config, pkgs, lib, ... }:
let
cfg = config.email;
in
### --> Mail notifications configuration
{
options.email = {
enable = lib.mkEnableOption "Email sending functionality";
fromAddress = lib.mkOption {
description = "The 'from' address";
type = lib.types.str;
default = "no-reply@${DOMAIN_NAME}";
};
toAddress = lib.mkOption {
description = "The 'to' address";
type = lib.types.str;
default = "${EMAIL_ADDRESS}";
};
smtpServer = lib.mkOption {
description = "The SMTP server address";
type = lib.types.str;
default = "${SENDER_EMAIL_DOMAIN}";
};
smtpUsername = lib.mkOption {
description = "The SMTP username";
type = lib.types.str;
default = "${SENDER_EMAIL_ADDRESS}";
};
smtpPasswordPath = lib.mkOption {
description = "Path to the secret containing SMTP password";
type = lib.types.path;
default = config.sops.secrets.sender_email_address_password.path;
};
};
config = lib.mkIf cfg.enable {
programs.msmtp = {
enable = true;
accounts.default = {
auth = true;
host = config.email.smtpServer;
from = config.email.fromAddress;
user = config.email.smtpUsername;
tls = true;
passwordeval = "${pkgs.coreutils}/bin/cat ${config.email.smtpPasswordPath}";
};
};
};
### Mail notifications configuration <--
### --> SMART disk heath
services.smartd = {
enable = true;
defaults.autodetected = "-a -o on -S on -s (S/../.././02|L/../../6/03) -n standby,q";
notifications = {
wall = {
enable = true;
};
mail = {
enable = true;
sender = config.email.fromAddress;
recipient = config.email.toAddress;
};
};
};
### SMART disk heath <--
}
+25
View File
@@ -0,0 +1,25 @@
{ config, ... }:
{
# Enable networking and firewall
networking.interfaces.eth0.ipv4.addresses = [
{
address = "HOME_SERVER_IP";
prefixLength = 24;
}
];
networking.defaultGateway = "HOME_ROUTER_IP";
networking.nameservers = [ "HOME_SERVER_IP" "9.9.9.9" ];
networking.networkmanager.enable = true;
# networking.nftables.enable = false;
networking.firewall.enable = true;
# networking.firewall.extraCommands = "
# iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
# iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
# ";
# Open ports in the firewall
networking.firewall.allowPing = true;
networking.firewall.allowedTCPPorts = [ 53 80 443 ];
networking.firewall.allowedUDPPorts = [ 53 ];
}