Files
Numbus/deploy.sh
T
Raphaël Billet 6827785db7 Standardize system disk on LVM-on-LUKS for snapshot support.
Add dedicated parity disk and correct data disk mountpoints.
Resolve various Nix syntax errors in disk templates.
Set data disk filesystem to XFS for better large-file performance.
2025-11-18 22:35:09 +01:00

415 lines
19 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
prerun_action() {
echo -e "$1"
SETUP_ANSWER="$(gum input --placeholder "Type 'done' when you have finished.")"
if [[ "$SETUP_ANSWER" == "done" ]]; then
:
else
echo " Aborting - you did not type 'done'."
exit 1
fi
}
necessary_credentials() {
#TARGET SETTINGS
echo -e "\n\n ➡️ Please provide the IP address of the target host :"
TARGET_HOST="$(gum input --placeholder "192.168.1.100")"
echo -e "\n\n ➡️ Please provide the public SSH key of an authorized device :"
SSH_PUBLIC_KEY="$(gum input --placeholder "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc")"
# TRAEFIK SETTINGS
echo -e "\n\n ➡️ Please provide the domain name (FQDN) your home server will use :"
DOMAIN_NAME="$(gum input --placeholder "yourdomain.com")"
echo -e "\n\n ➡️ Please provide a valid email address (will be used for ACME, and your services) :"
EMAIL_ADDRESS="$(gum input --placeholder "myemail@gmail.com")"
echo -e "\n\n ➡️ Please provide a cloudflare API token with DNS zone permission :"
CF_DNS_API_TOKEN="$(gum input --placeholder "bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE")"
# SMTP SETTINGS
echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails.\n Please provide a valid sender email address :"
SENDER_EMAIL_ADDRESS="$(gum input --placeholder "myemail@gmail.com")"
echo -e "\n\n ➡️ Please provide the password of this email address :"
SENDER_EMAIL_ADDRESS_PASSWORD="$(gum input --placeholder "abcd efgh ijkl mnop")"
echo -e "\n\n ➡️ Please provide the SMTP server endpoint :"
SENDER_EMAIL_DOMAIN="$(gum input --placeholder "smtp.gmail.com")"
echo -e "\n\n ➡️ Please provide the smtp TLS port (for gmail : 587) :"
SENDER_EMAIL_PORT="$(gum input --placeholder "587")"
# NETWORK SETTINGS
echo -e "\n\n ➡️ Please provide your home network subnet :"
HOME_ROUTER_SUBNET="$(gum input --placeholder "192.168.1.1/24")"
echo -e "\n\n ➡️ Please provide the ip address of your router :"
HOME_ROUTER_IP="$(gum input --placeholder "192.168.1.1")"
echo -e "\n\n ➡️ Please choose the ip address that your server will use (i.e. any address in the 192.168.1.1/24 range that is not in use.) :"
HOME_SERVER_IP="$(gum input --placeholder "192.168.1.5")"
}
necessary_credentials_with_config() {
echo -e "\n\n ➡️ Please choose your configuration file :"
CONFIG_PATH="$(gum file)"
source "$CONFIG_PATH"
REQUIRED_VARS=(TARGET_HOST TARGET_DISK SSH_PUBLIC_KEY DOMAIN_NAME EMAIL_ADDRESS CF_DNS_API_TOKEN SENDER_EMAIL_ADDRESS SENDER_EMAIL_ADDRESS_PASSWORD SENDER_EMAIL_DOMAIN SENDER_EMAIL_PORT HOME_ROUTER_SUBNET HOME_ROUTER_IP HOME_SERVER_IP)
MISSING=0
for VAR in "${REQUIRED_VARS[@]}"; do
if [[ -v $VAR && -n ${!VAR} ]]; then
echo -e "\n ✅ $VAR imported successfully from the config file"
else
echo "\n ❌ $VAR is missing or empty"
MISSING=1
fi
done
if [[ "$MISSING" == "1" ]]; then
exit 1
fi
}
hardware_detection() {
echo -e "\n\n ➡️ Please provide the password of the target host :"
ssh-copy-id -i extra-files/home/numbus-admin/.ssh/id_ed25519.pub nixos@$TARGET_HOST
ssh_to_host() {
ssh -i extra-files/home/numbus-admin/.ssh/id_ed25519 nixos@$TARGET_HOST "$1"
}
echo -e "\n\n 🔎 Detecting graphics card on target host..."
VGA_INFO=$(ssh_to_host "lspci -nn | grep -i 'vga'")
if echo "$VGA_INFO" | grep -iq "intel"; then
echo -e " ✅ Intel graphics card detected."
TARGET_GRAPHICS="true"
elif echo "$VGA_INFO" | grep -iq "amd"; then
echo -e " ✅ AMD graphics card detected."
TARGET_GRAPHICS="true"
elif echo "$VGA_INFO" | grep -iq "nvidia"; then
echo -e " ✅ NVIDIA graphics card detected."
TARGET_GRAPHICS="true"
else
echo -e " ️ No dedicated graphics card detected."
TARGET_GRAPHICS="false"
fi
echo -e "\n\n 🔎 Detecting transconding acceleration on target host..."
if ssh_to_host "ls /dev/dri/renderD128"; then
echo -e " ✅ Transcoding capable card detected."
TARGET_GRAPHICS_RENDERER="true"
else
echo -e " ️ No transcoding capable card detected."
TARGET_GRAPHICS_RENDERER="false"
fi
echo -e "\n\n 🔎 Detecting USB Google Coral TPU on target host..."
if ssh_to_host "lsusb | grep -iq 'google'"; then
echo -e " ✅ USB Google Coral TPU detected."
TARGET_USB_CORAL="true"
else
echo -e " ️ No USB Google Coral TPU detected."
TARGET_USB_CORAL="false"
fi
echo -e "\n\n 🔎 Detecting Zigbee coordinator on target host..."
if ssh_to_host "ls /dev/serial/by-id/ | grep -i 'zigbee'"; then
echo -e " ✅ Zigbee device found in /dev/serial/by-id/."
TARGET_ZIGBEE_DEVICE=$(ssh_to_host "ls /dev/serial/by-id/ | grep -i 'zigbee'")
TARGET_ZIGBEE="true"
else
echo -e " ️ No Zigbee device found."
TARGET_ZIGBEE="false"
fi
}
files_generation() {
echo -e "\n\n ✅ Generating necessary folder tree..."
mkdir -p extra-files/home/numbus-admin/.ssh/
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\n ✅ Generating new SSH for numbus-admin..."
ssh-keygen -t ed25519 -C numbus-admin@numbus-server -f extra-files/home/numbus-admin/.ssh/id_ed25519 -N "" -q
echo -e "\n\n ✅ Generating sops-nix keys..."
nix run nixpkgs#ssh-to-age -- -private-key -i extra-files/home/numbus-admin/.ssh/id_ed25519 > extra-files/var/lib/sops-nix/key.txt
SOPS_PUBLIC_KEY=$(nix shell nixpkgs#age -c age-keygen -y extra-files/var/lib/sops-nix/key.txt)
echo -e "\n\n ✅ Generating sops-nix configuration files..."
envsubst < config-files/sops-nix/.sops.yaml > extra-files/etc/nixos/.sops.yaml
echo -e "\n\n ✅ Generating secure random database passwords..."
HOME_ASSISTANT_MQTT_USER=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
HOME_ASSISTANT_MQTT_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
PASSBOLT_MYSQL_DATABASE=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
PASSBOLT_MYSQL_USER=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
PASSBOLT_MYSQL_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
FTLCONF_WEBSERVER_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
echo -e "\n\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
echo -e "\n\n ✅ Writing correct ips to configuration.nix..."
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g configuration.nix
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g configuration.nix
echo -e "\n\n ✅ Adapting the docker configuration to your hardware..."
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" && "$TARGET_USB_CORAL" == "true" ]]; then
sed -i.bak '
/^[[:space:]]*# ----------------------------------------- #/{
N;
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
N;
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
N;
/----------------------------------------- #/c\
devices:\
- /dev/dri/renderD128:/dev/dri/renderD128\
- /dev/bus/usb:/dev/bus/usb
}
}
}' docker/frigate.nix
elif [[ "$TARGET_GRAPHICS_RENDERER" == "true" && "$TARGET_USB_CORAL" == "false" ]]; then
sed -i.bak '
/^[[:space:]]*# ----------------------------------------- #/{
N;
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
N;
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
N;
/----------------------------------------- #/c\
devices:\
- /dev/dri/renderD128:/dev/dri/renderD128\
}
}
}' docker/frigate.nix
elif [[ "$TARGET_GRAPHICS_RENDERER" == "false" && "$TARGET_USB_CORAL" == "true" ]]; then
sed -i.bak '
/^[[:space:]]*# ----------------------------------------- #/{
N;
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
N;
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
N;
/----------------------------------------- #/c\
devices:\
- /dev/bus/usb:/dev/bus/usb
}
}
}' docker/frigate.nix
fi
if [[ "$TARGET_ZIGBEE" == "true" ]]; then
sed -i.bak "
/^[[:space:]]*# ----------------------------------- #/{
N;
/DEVICES SECTION WILL APPEAR HERE IF/{
N;
/ZIGBEE USB DEVICE IS PRESENT/{
N;
/----------------------------------- #/c\
devices:\
- /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0
}
}
}" docker/hass.nix
fi
echo -e "\n\n ✅ Copying files to the new installation..."
cp -ravu secrets/ .sops.yaml hardware-configuration.nix extra-files/etc/nixos/
echo -e "\n\n ✅ Writing docker configuration files..."
envsubst < config-files/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
envsubst < config-files/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
envsubst < config-files/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
envsubst < config-files/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
envsubst < config-files/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
nix shell nixpkgs#mosquitto -c mosquitto_passwd -b extra-files/mnt/config-storage/hass/mqtt/config/password.txt $HOME_ASSISTANT_MQTT_USER $HOME_ASSISTANT_MQTT_PASSWORD
}
disk_config_generation() {
ssh_to_host() {
ssh -i extra-files/home/numbus-admin/.ssh/id_ed25519 nixos@$TARGET_HOST "$1"
}
echo -e "\n\n ⚠️ WARNING: you will choose the disks you want to install NixOS on."
echo -e " !! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !!"
echo -e " !! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !!"
echo -e " Press CTRL+C to abort."
SETUP_ANSWER="$(gum input --placeholder "Type 'understood' once you have read the warning.")"
if [[ "$SETUP_ANSWER" == "understood" ]]; then
:
else
echo " Aborting - you did not type 'understood'."
exit 1
fi
echo -e "\n\n 🔎 Fetching disks from target host..."
DISK_JSON=$(ssh_to_host "lsblk -d --json -o NAME,ROTA,SIZE,PATH")
BY_ID_RAW=$(ssh_to_host "ls -l /dev/disk/by-id/")
if [ -z "$DISK_JSON" ]; then
echo " ❌ Could not find any disks on the target host. Aborting."
exit 1
fi
declare -A BY_ID_MAP
while read -r line; do
if [[ "$line" =~ "-> ../../"(.*)$ ]]; then
dev_name="${BASH_REMATCH[1]}"
by_id_path="/dev/disk/by-id/$(echo "$line" | awk '{print $9}')"
if [[ ! -v "BY_ID_MAP[$dev_name]" && ! "$by_id_path" =~ -part ]]; then
BY_ID_MAP["$dev_name"]="$by_id_path"
fi
fi
done <<< "$BY_ID_RAW"
declare -A DISK_MAP
declare -a DISK_OPTIONS
while read -r name type size; do
by_id=${BY_ID_MAP[$name]}
if [ -z "$by_id" ]; then continue; fi
option=$(printf "%-8s %-5s %-8s (%s)" "$name" "$type" "$size" "$by_id")
DISK_OPTIONS+=("$option")
DISK_MAP["$option"]="$by_id"
done < <(echo "$DISK_JSON" | jq -r '.blockdevices[] | "\(.name) \(if .name | test("^nvme") then "NVMe" else (if .rota == "0" then "SSD" else "HDD" end) end) \(.size)"')
echo -e "\n\n ➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation:"
mapfile -t SELECTED_BOOT_OPTIONS < <(gum choose --limit 2 "${DISK_OPTIONS[@]}")
if [ ${#SELECTED_BOOT_OPTIONS[@]} -eq 0 ]; then
echo " ❌ No boot disk selected. Aborting."
exit 1
fi
NUMBER_OF_BOOT_DISKS=${#SELECTED_BOOT_OPTIONS[@]}
BOOT_DISK_1=${DISK_MAP["${SELECTED_BOOT_OPTIONS[0]}"]}
if [ "$NUMBER_OF_BOOT_DISKS" -eq 2 ]; then
BOOT_DISK_2=${DISK_MAP["${SELECTED_BOOT_OPTIONS[1]}"]}
fi
REMAINING_DISKS=()
for option in "${DISK_OPTIONS[@]}"; do
is_boot_disk=false
for selected in "${SELECTED_BOOT_OPTIONS[@]}"; do
if [[ "$option" == "$selected" ]]; then
is_boot_disk=true
break
fi
done
if ! $is_boot_disk; then
REMAINING_DISKS+=("$option")
fi
done
if [ ${#REMAINING_DISKS[@]} -gt 0 ]; then
echo -e "\n\n ➡️ Please choose your data disks (up to 4):"
mapfile -t SELECTED_DATA_OPTIONS < <(gum choose --limit 4 "${REMAINING_DISKS[@]}")
NUMBER_OF_DATA_DISKS=${#SELECTED_DATA_OPTIONS[@]}
for i in $(seq 0 $(($NUMBER_OF_DATA_DISKS - 1))); do
declare "DATA_DISK_$(($i + 1))"="${DISK_MAP["${SELECTED_DATA_OPTIONS[$i]}"]}"
done
else
echo -e "\n\n ️ No remaining disks available for data storage."
NUMBER_OF_DATA_DISKS=0
fi
DISK_CONFIG_TEMPLATE="config-files/disks/boot-${NUMBER_OF_BOOT_DISKS}-data-${NUMBER_OF_DATA_DISKS}.nix"
if [[ -f "$DISK_CONFIG_TEMPLATE" ]]; then
echo -e "\n\n ✅ Generating disk configuration from template: $DISK_CONFIG_TEMPLATE"
envsubst < "$DISK_CONFIG_TEMPLATE" > disk-config.nix
else
echo -e "\n\n ❌ Error: No disk configuration template found for $NUMBER_OF_BOOT_DISKS boot disk(s) and $NUMBER_OF_DATA_DISKS data disk(s)."
echo " Looked for: $DISK_CONFIG_TEMPLATE"
exit 1
}
deploy() {
echo -e "\n\n 🔄 Deploying to the remote server..."
nix run github:nix-community/nixos-anywhere -- \
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
--flake .#numbus-server \
--extra-files "extra-files/" \
--chown "/home/numbus-admin/" 1000:1000 \
--target-host nixos@$TARGET_HOST
echo -e "\n\n ✅ Installation successfull !!"
sleep 1
}
nixos_update() {
}
set -euo pipefail
cat <<EOF
██████ █████ ███ ███████ █████████
░░██████ ░░███ ░░░ ███░░░░░███ ███░░░░░███
░███░███ ░███ ████ █████ █████ ███ ░░███░███ ░░░
░███░░███░███ ░░███ ░░███ ░░███ ░███ ░███░░█████████
░███ ░░██████ ░███ ░░░█████░ ░███ ░███ ░░░░░░░░███
░███ ░░█████ ░███ ███░░░███ ░░███ ███ ███ ░███
█████ ░░█████ █████ █████ █████ ░░░███████░ ░░█████████
░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░ ░░░░░░░░░
█████████ █████
███░░░░░███ ░░███
░███ ░███ ████████ █████ ████ █████ ███ █████ ░███████ ██████ ████████ ██████
░███████████ ░░███░░███ ░░███ ░███ ░░███ ░███░░███ ░███░░███ ███░░███░░███░░███ ███░░███
░███░░░░░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███████ ░███ ░░░ ░███████
░███ ░███ ░███ ░███ ░███ ░███ ░░███████████ ░███ ░███ ░███░░░ ░███ ░███░░░
█████ █████ ████ █████ ░░███████ ░░████░████ ████ █████░░██████ █████ ░░██████
░░░░░ ░░░░░ ░░░░ ░░░░░ ░░░░░███ ░░░░ ░░░░ ░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░
███ ░███
░░██████
░░░░░░
EOF
sleep 1
# Pre-run checks
if ! command -v gum &> /dev/null; then
echo " ❌ 'gum' is not installed. Please install it to use the interactive TUI."
echo " Add 'gum' to your configuration.nix in the system packages section."
exit 1
fi
if ! command -v openssl &> /dev/null; then
echo " ❌ 'openssl' is not installed."
echo " Add 'openssl' to your configuration.nix in the system packages section."
exit 1
fi
# Choose the action
ACTION_ANSWER=$(gum choose "[1] 🌐 Deploy NixOS on a remote machine" "[2] 💽 Deploy NixOS on a remote machine with a file configuration" "[3] 🛠️ Update a NixOS remote machine")
echo $ACTION_ANSWER
if [[ "$ACTION_ANSWER" == "[1] 🌐 Deploy NixOS on a remote machine" ]]; then
echo -e "\n ➡️ Proceeding with deployment…"
prerun_action "\n\n ➡️ On the target host : start the computer and boot into the NixOS iso.\n Launch a console and set up a new user password."
necessary_credentials
hardware_detection
files_generation
disk_config_generation
elif [[ "$ACTION_ANSWER" == "[2] 💽 Deploy NixOS on a remote machine with a file configuration" ]]; then
echo -e "\n ➡️ Proceeding with deployment using a config file…"
prerun_action "\n\n ➡️ On the target host : start the computer and boot into the NixOS iso.\n Launch a console and set up a new user password."
necessary_credentials_with_config
hardware_detection
files_generation
disk_config_generation
elif [[ "$ACTION_ANSWER" == "[3] 🛠️ Update a NixOS remote machine" ]]; then
echo -e "\n ➡️ Proceeding with update…"
prerun_action "\n\n ➡️ On the target host : make sure the NixOS installation you want to update is up-and-running, accessible with SSH."
nixos_update
else
echo "Aborting - you did not type '1, 2 or 3'."
exit 1
fi