Files
numbus-server/deploy.sh
T
2025-12-31 14:23:41 +01:00

784 lines
35 KiB
Bash
Executable File

#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash coreutils gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
### --> Default settings
export GUM_SPIN_SPINNER="minidot"
export GUM_SPIN_SPINNER_BOLD=true
export GUM_SPIN_SHOW_ERROR=true
export GUM_SPIN_TITLE_BOLD=true
NECESSARY_VARIABLES_LIST=("TARGET_HOST" "REMOTE_PASS" "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")
### Default settings <--
user_input() {
local VAR_NAME="${1}"
local HEADER="${2}"
local PLACEHOLDER="${3}"
local REGEX="${4}"
local ERROR_MSG="${5}"
local SENSITIVE="${6:-false}"
while true; do
[[ "$SENSITIVE" == "false" ]] && INPUT_VALUE=$(gum input --placeholder "${PLACEHOLDER}" --header "${HEADER}")
[[ "$SENSITIVE" == "true" ]] && INPUT_VALUE=$(gum input --password --placeholder "${PLACEHOLDER}" --header "${HEADER}")
if [[ -z "${INPUT_VALUE}" ]]; then
echo "❌ Error: Input cannot be empty. Please provide the necessary information."
continue
fi
if [[ -n "${REGEX}" ]]; then
if [[ ! "${INPUT_VALUE}" =~ ${REGEX} ]]; then
echo "❌ Error: ${ERROR_MSG}"
continue
fi
fi
export "${VAR_NAME}"="${INPUT_VALUE}"
break
done
}
necessary_credentials() {
# Regex Definitions
local IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
local SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
local DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
local EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
local PORT_REGEX='^[0-9]{1,5}$'
local SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
#TARGET SETTINGS
user_input "TARGET_HOST" "➡️ Please provide the IP address of the target host :" "192.168.1.100" "${IP_REGEX}" "Invalid IP address format."
user_input "REMOTE_PASS" "➡️ Please enter the password for '${TARGET_USER}@${TARGET_HOST}' :" "${TARGET_HOST}'s password" "" "" "true"
user_input "SSH_PUBLIC_KEY" "➡️ Please provide the public SSH key of an authorized device :" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." "true"
# TRAEFIK SETTINGS
user_input "DOMAIN_NAME" "➡️ Please provide the domain name (FQDN) your home server will use :" "yourdomain.com" "${DOMAIN_REGEX}" "Invalid domain name format."
user_input "EMAIL_ADDRESS" "➡️ Please provide a valid email address (will be used for ACME, and your services) :" "myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
user_input "CF_DNS_API_TOKEN" "➡️ Please provide a cloudflare API token with DNS zone permission :" "bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
# 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 (like Gmail for example).\n"
user_input "SENDER_EMAIL_ADDRESS" "➡️ Please provide a valid sender email address :" "myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
user_input "SENDER_EMAIL_ADDRESS_PASSWORD" "➡️ Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
user_input "SENDER_EMAIL_DOMAIN" "➡️ Please provide the SMTP server endpoint :" "smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
user_input "SENDER_EMAIL_PORT" "➡️ Please provide the smtp TLS port (for gmail : 587) :" "587" "${PORT_REGEX}" "Invalid port number."
# NETWORK SETTINGS
user_input "HOME_ROUTER_SUBNET" "➡️ Please provide your home network subnet :" "192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
user_input "HOME_ROUTER_IP" "➡️ Please provide the ip address of your router :" "192.168.1.1" "${IP_REGEX}" "Invalid IP address format."
user_input "HOME_SERVER_IP" "➡️ 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.) :" "192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
}
necessary_credentials_with_config() {
echo -e "\n\n➡️ Please choose your configuration file :"
local CONFIG_PATH="$(gum file)"
source "${CONFIG_PATH}"
local MISSING=0
for VAR in "${NECESSARY_VARIABLES_LIST[@]}"; do
if [[ -v "${VAR}" && -n "${!VAR}" ]]; then
gum spin --title "✅ "${VAR}" imported successfully from the config file" -- sleep 0.5
else
gum spin --title "❌ "${VAR}" is missing or empty" -- sleep 0.5
MISSING=1
fi
done
if [[ "${MISSING}" -eq 1 ]]; then
echo -e "\n❌ Please check your configuration file to include all necessary variables"
exit 1
fi
}
generate_folder_tree() {
mkdir -p final-nix-config/
mkdir -p final-nix-config/home/
mkdir -p final-nix-config/home/numbus-admin/
mkdir -p final-nix-config/home/numbus-admin/.ssh/
mkdir -p final-nix-config/mnt/
mkdir -p final-nix-config/mnt/config/
mkdir -p final-nix-config/mnt/data/
mkdir -p final-nix-config/mnt/config/traefik/
mkdir -p final-nix-config/mnt/config/traefik/rules/
mkdir -p final-nix-config/mnt/config/traefik/certs/
mkdir -p final-nix-config/etc/
mkdir -p final-nix-config/etc/nixos/
mkdir -p final-nix-config/etc/secrets/
mkdir -p final-nix-config/etc/numbus-server/
mkdir -p final-nix-config/etc/nixos/misc/
mkdir -p final-nix-config/etc/nixos/pcie-coral/
mkdir -p final-nix-config/etc/nixos/podman/
mkdir -p final-nix-config/etc/nixos/secrets/
mkdir -p final-nix-config/var/
mkdir -p final-nix-config/var/lib/
mkdir -p final-nix-config/var/lib/sops-nix
}
setup_ssh() {
echo -e "\n\n✅ Generating new SSH key for numbus-admin..."
chmod 700 final-nix-config/home/numbus-admin/.ssh/
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "final-nix-config/home/numbus-admin/.ssh/id_ed25519" -N "" -q
echo -e "\n\n➡️ Copying SSH key to target host '${TARGET_USER}@${TARGET_HOST}'..."
if sshpass -p "${REMOTE_PASS}" ssh-copy-id -o StrictHostKeyChecking=no -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${TARGET_HOST}"; then
echo "✅ SSH key copied successfully."
else
echo "❌ Failed to copy SSH key. Please check the host IP and password."
exit 1
fi
}
ssh_to_host() {
local COMMAND="${1}"
ssh -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${TARGET_HOST}" "${COMMAND}"
}
hardware_detection() {
### --> Get hardware information
local TMPFILE="/tmp/nixos-installation-hardware-detection-temp-file"
ssh_to_host 'bash -s' << SSHEND
for brand in Intel AMD NVIDIA; do
if lspci -nn 2>/dev/null | grep -i "vga" | grep -iq "\${brand}"; then
TARGET_GRAPHICS="true"
TARGET_GRAPHICS_BRAND+=("\${brand}")
else
TARGET_GRAPHICS="false"
fi
done
ls /dev/dri/ 2>/dev/null | grep -iq "renderD128" && TARGET_GRAPHICS_RENDERER="true" || TARGET_GRAPHICS_RENDERER="false"
lsusb 2>/dev/null | grep -iq "google" && TARGET_USB_CORAL="true" || TARGET_USB_CORAL="false"
lspci -nn 2>/dev/null | grep -iq "089a" && TARGET_PCIE_CORAL="true" || TARGET_PCIE_CORAL="false"
ls /dev/serial/by-id/ 2>/dev/null | grep -i "zigbee" && TARGET_ZIGBEE_DEVICE=\$(ls /dev/serial/by-id/ 2>/dev/null | grep -i "zigbee" | head -n 1) || TARGET_ZIGBEE_DEVICE=""
for var in TARGET_GRAPHICS TARGET_GRAPHICS_BRAND TARGET_GRAPHICS_RENDERER TARGET_USB_CORAL TARGET_PCIE_CORAL TARGET_ZIGBEE_DEVICE; do
echo "export \${var}=\${!var}" >> "${TMPFILE}"
done
SSHEND
### Get hardware information <--
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${TARGET_HOST}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
source "${TMPFILE}" && rm "${TMPFILE}"
### --> Generate hardware-configuration.nix
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > final-nix-config/etc/nixos/hardware-configuration.nix; then
echo -e "✅ Hardware configuration generated"
else
echo -e "❌ Failed to generate hardware configuration"
exit 1
fi
### Generate hardware-configuration.nix <--
}
services_selection() {
echo -e "\n\n➡️ You will now select the services you want installed on your server:"
local AVAILABLE_SERVICES=( "frigate" "gitea" "home-assistant" "immich" "it-tools" \
"nextcloud" "passbolt" "pi-hole" )
local 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"
)
local SELECTED_SERVICES_DESCRIPTION=$(gum choose --no-limit --header "Homelab services:" "${SERVICES_DESCRIPTION[@]}")
for i in ${!AVAILABLE_SERVICES[@]}; do
if printf '%s' "$SELECTED_SERVICES_DESCRIPTION" | grep -iq "${AVAILABLE_SERVICES[$i]}"; then
export SELECTED_SERVICES+=(${AVAILABLE_SERVICES[$i]})
fi
done
for service in ${SELECTED_SERVICES[@]}; do
mkdir -p final-nix-config/mnt/config/"${service}"
mkdir -p final-nix-config/mnt/data/"${service}"
done
}
files_generation() {
# Helper to generate standard DB credentials
generate_db_creds() {
local SERVICE_UPPER="${1}"
export "${SERVICE_UPPER}_DB_NAME"="$(xkcdpass -d "-" -n 2)"
export "${SERVICE_UPPER}_DB_USERNAME"="$(xkcdpass -d "-" -n 2)"
export "${SERVICE_UPPER}_DB_PASSWORD"="$(xkcdpass -d "-")"
}
echo -e "\n✅ Copying the configuration to the new machine..."
cp -avu templates/nix-config/configuration.nix final-nix-config/etc/nixos/
cp -avu templates/nix-config/flake.nix final-nix-config/etc/nixos/
echo -e "\n✅ Generating sops-nix keys..."
ssh-to-age -private-key -i final-nix-config/home/numbus-admin/.ssh/id_ed25519 > final-nix-config/var/lib/sops-nix/key.txt
export SOPS_PUBLIC_KEY=$(age-keygen -y final-nix-config/var/lib/sops-nix/key.txt)
echo -e "\n✅ Generating sops-nix configuration files..."
envsubst < templates/nix-config/sops-nix/.sops.yaml > final-nix-config/etc/nixos/.sops.yaml
echo -e "\n✅ Encrypting secrets in the correct file..."
envsubst < "templates/nix-config/sops-nix/secrets.yaml" \
| sops encrypt --filename-override secrets.yaml \
--input-type yaml --output-type yaml \
--age $SOPS_PUBLIC_KEY \
--output final-nix-config/etc/nixos/secrets/secrets.yaml
echo -e "\n✅ Writing correct ips to configuration.nix..."
sed -i "s|HOME_SERVER_IP|${HOME_SERVER_IP}|g" final-nix-config/etc/nixos/misc/networking.nix
sed -i "s|HOME_ROUTER_IP|${HOME_ROUTER_IP}|g" final-nix-config/etc/nixos/misc/networking.nix
echo -e "\n✅ Writing configuration files for the selected homelab services..."
envsubst < templates/podman-config/traefik/traefik.yaml > final-nix-config/mnt/config/traefik/traefik.yaml
for service in "${SELECTED_SERVICES[@]}"; do
cp templates/nix-config/podman/${service}.nix final-nix-config/etc/nixos/podman/${service}.nix
case "${service}" in
frigate)
local FRIGATE_DEVICES_BLOCK=""
[[ "$TARGET_GRAPHICS_RENDERER" == "true" ]] && local FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
[[ "$TARGET_USB_CORAL" == "true" ]] && local FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n"
if [[ "$TARGET_PCIE_CORAL" == "true" ]]; then
local FRIGATE_DEVICES_BLOCK+=" - /dev/apex_0:/dev/apex_0\n"
sed -i "s|# ./pcie-coral/coral.nix| ./pcie-coral/coral.nix|" final-nix-config/etc/nixos/configuration.nix
cp -avu templates/nix-config/pcie-coral/* final-nix-config/etc/nixos/pcie-coral/
fi
if [[ -n "$FRIGATE_DEVICES_BLOCK" ]]; then
local REPLACEMENT="devices:\n${FRIGATE_DEVICES_BLOCK%\\n}"
sed -i "s|# --- frigate devices --- #|$REPLACEMENT|" final-nix-config/etc/nixos/podman/frigate.nix
fi
;;
home-assistant)
if [[ -n "$TARGET_ZIGBEE_DEVICE" ]]; then
local REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0"
sed -i "s|# --- hass devices --- #|$REPLACEMENT|" final-nix-config/etc/nixos/podman/home-assistant.nix
fi
export HOME_ASSISTANT_MQTT_USER="$(xkcdpass -d "-" -n 2)"
export HOME_ASSISTANT_MQTT_PASSWORD="$(xkcdpass -d "-")"
mkdir -p final-nix-config/mnt/config/mqtt/
envsubst < templates/podman-config/hass/mosquitto.conf > final-nix-config/mnt/config/mqtt/mosquitto.conf
touch final-nix-config/mnt/config/mqtt/password.txt
chmod 0700 final-nix-config/mnt/config/mqtt/password.txt
mosquitto_passwd -b final-nix-config/mnt/config/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD"
;;
passbolt)
generate_db_creds "PASSBOLT"
envsubst < templates/podman-config/traefik/headers.yaml > final-nix-config/mnt/config/traefik/rules/headers.yaml
envsubst < templates/podman-config/traefik/tls.yaml > final-nix-config/mnt/config/traefik/rules/tls.yaml
;;
pi-hole)
export FTLCONF_WEBSERVER_PASSWORD="$(xkcdpass -d "-")"
;;
immich)
local IMMICH_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
local IMMICH_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
fi
if [[ -n "$IMMICH_DEVICES_BLOCK" ]]; then
local REPLACEMENT="devices:\n${IMMICH_DEVICES_BLOCK%\\n}"
sed -i "s|# --- immich devices --- #|$REPLACEMENT|" final-nix-config/etc/nixos/podman/immich.nix
fi
generate_db_creds "IMMICH"
;;
gitea)
generate_db_creds "GITEA"
;;
nextcloud)
envsubst < templates/podman-config/traefik/nextcloud.yaml > final-nix-config/mnt/config/traefik/rules/nextcloud.yaml
;;
esac
done
}
disk_config_generation() {
### --> Disk wiping warning
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on.
!! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !!
!! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !!
Please press CTRL+C to abort.
"
gum confirm "Do you understand and wish to proceed?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)"
### Disk wiping warning <--
### --> Get disk information
local TMPFILE="/tmp/nixos-installation-disk-detection-temp-file"
ssh_to_host 'bash -s' << EOF
HDD=1
DISK_DEVPATH=()
DISK_NAME=()
DISK_TYPE=()
DISK_HEALTH=()
DISK_ID=()
DISK_SIZE=()
for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do
# Disk name and simple path
DISK_DEVPATH+=("/dev/\$DISK")
DISK_NAME+=("\$DISK")
# Disk type
HDD=\$(cat /sys/block/\$DISK/queue/rotational)
TRANSPORT_PROTOCOL=\$(lsblk -x SIZE -d -n -e 7,11 -o TRAN /dev/\$DISK)
if [[ "\$DISK" == "nvme*" ]]; then DISK_TYPE+=("NVMe");
elif [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB");
elif [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD");
elif [[ "\$HDD" -eq 0 ]]; then DISK_TYPE+=("SSD");
else DISK_TYPE+=("Other")
fi
# Disk health
if [[ \$(echo "$REMOTE_PASS" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') == "PASSED" ]]; then
DISK_HEALTH+=("PASSED")
else
DISK_HEALTH+=("N/A")
fi
# Disk ID
DISK_ID+=("\$(ls -l /dev/disk/by-id | grep -m1 "../../\$DISK" | awk '{print "/dev/disk/by-id/" \$9}')")
# Disk size
DISK_SIZE+=("\$(lsblk -x SIZE -d -n -e 7,11 -o SIZE /dev/\$DISK)")
done
echo "DISK_DEVPATH=(\${DISK_DEVPATH[@]})" > "${TMPFILE}"
echo "DISK_NAME=(\${DISK_NAME[@]})" >> "${TMPFILE}"
echo "DISK_TYPE=(\${DISK_TYPE[@]})" >> "${TMPFILE}"
echo "DISK_HEALTH=(\${DISK_HEALTH[@]})" >> "${TMPFILE}"
echo "DISK_ID=(\${DISK_ID[@]})" >> "${TMPFILE}"
echo "DISK_SIZE=(\${DISK_SIZE[@]})" >> "${TMPFILE}"
EOF
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${TARGET_HOST}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
source "${TMPFILE}" && rm "${TMPFILE}"
### --> Disk selection
if [[ "${#DISK_NAME[@]}" -eq 0 ]]; then
echo -e "\n\n❌ No disks found on the target host. Aborting."
exit 1
fi
local HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path")
for i in ${!DISK_NAME[@]}; do
local GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \
"${DISK_NAME[${i}]}" "${DISK_TYPE[${i}]}" "${DISK_SIZE[${i}]}" \
"${DISK_HEALTH[${i}]}" "${DISK_DEVPATH[${i}]}")
local GUM_PRINTED_ELEMENTS+=("$GUM_PRINTED_ELEMENT")
done
gum style --foreground 212 "➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation :"
local SELECTED_BOOT_DISK=$(gum choose --limit 2 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
for i in ${!DISK_NAME[@]}; do
if printf '%s' "$SELECTED_BOOT_DISK" | grep -iqw "${DISK_NAME[${i}]}"; then
export BOOT_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}")
unset "GUM_PRINTED_ELEMENTS[${i}]"
fi
done
if [[ "${#BOOT_DISKS_ID[@]}" -eq 0 ]]; then
echo -e "\n\n❌ No boot disk selected. Aborting."
exit 1
elif [[ "${#BOOT_DISKS_ID[@]}" -eq 1 ]]; then
echo -e "\n\n ⚠️ One boot disk selected, continuing with striped boot disk configuration."
echo -e " Consider using 2 boot disks instead to get data protection features on the boot disks."
export BOOT_DISK_1_ID=${BOOT_DISKS_ID[0]}
elif [[ "${#BOOT_DISKS_ID[@]}" -eq 2 ]]; then
echo -e "\n\n✅ Two boot disks selected, continuing with mirrored boot disks configuration."
echo -e "\n\n ⚠️ If the two disks are different sizes, the resulting usable space size will be \
the one of the smallest disk."
export BOOT_DISK_1_ID=${BOOT_DISKS_ID[0]}
export BOOT_DISK_2_ID=${BOOT_DISKS_ID[1]}
else
echo -e "\n\n❌ Unexpected bug. Please contact the developer. Aborting."
exit 1
fi
gum style --foreground 212 "➡️ Please choose data and parity disks (up to 9 total) :"
local SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
for i in ${!DISK_NAME[@]}; do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[${i}]}"; then
export DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}")
export DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}")
fi
done
PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3))
CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER))
### Disk selection <--
### --> Selection recap
RECAP_CONTENT=$(cat << EOF
### Disk Configuration Summary
Please review the selected disk layout before proceeding.
**Boot Disks (${#BOOT_DISKS_ID[@]}):**
* **Boot 1:** \`${BOOT_DISKS_ID[0]}\`
$( [[ -n "${BOOT_DISKS_ID[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID[1]}\`" || echo "* **Boot 2:** *Not configured*")
**Parity Disks ($PARITY_DISK_NUMBER):**
$(for i in $(seq 0 $((${#DATA_DISKS_ID[@]} - CONTENT_DISK_NUMBER))); do echo "* **Parity ${i}:** `${DATA_DISKS_ID[${i}]}`"; done)
$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*")
**Data Disks ($CONTENT_DISK_NUMBER):**
$(for i in $(seq $PARITY_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Data ${i}:** `${DATA_DISKS_ID[${i}]}`"; done)
$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*")
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "$RECAP_CONTENT")"
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..."
local TEMPLATE_FILE="templates/nix-config/disks/boot-${#BOOT_DISKS_ID[@]}.nix"
(envsubst < "$TEMPLATE_FILE") > final-nix-config/etc/nixos/disks/disko.nix
# Striped configuration
if [[ "$CONTENT_DISK_NUMBER" -eq 1 && "$PARITY_DISK_NUMBER" -eq 0 ]]; then
export j="1"
export CONTENT_DISK_ID="${DATA_DISKS_ID[0]}"
(envsubst < "templates/nix-config/disks/content.nix") >> final-nix-config/etc/nixos/disks/disko.nix
sed -i "s|/mnt/content-1|/mnt/data-storage|" final-nix-config/etc/nixos/disks/disko.nix
# Mirror configuration
elif [[ "$CONTENT_DISK_NUMBER" -eq 1 && "$PARITY_DISK_NUMBER" -eq 1 ]]; then
export CONTENT_DISK_ID="${DATA_DISKS_ID[0]}"
export PARITY_DISK_ID="${DATA_DISKS_ID[1]}"
(envsubst < "templates/nix-config/disks/mirror.nix") >> final-nix-config/etc/nixos/disks/disko.nix
# SnapRAID configuration
elif [[ "$CONTENT_DISK_NUMBER" -gt 1 ]]; then
# Enable SnapRAID
cp -avu templates/nix-config/disks/snapraid.nix final-nix-config/etc/nixos/disks/
cp -avu templates/nix-config/disks/pcr-check.nix final-nix-config/etc/nixos/disks/
sed -i "s|# ./disks/snapraid.nix| ./disks/snapraid.nix|" final-nix-config/etc/nixos/configuration.nix
sed -i '$ d' final-nix-config/etc/nixos/disks/snapraid.nix
cat << EOF >> final-nix-config/etc/nixos/disks/snapraid.nix
# --> Automatic data disks unlock, generated by deploy.sh on $(date)
boot.initrd.luks.devices = {
EOF
j=0
for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do
export j=$((j+1))
export CONTENT_DISK_ID="${DATA_DISKS_ID[${i}]}"
(envsubst < "templates/nix-config/disks/content.nix") >> final-nix-config/etc/nixos/disks/disko.nix
cat << EOF >> final-nix-config/etc/nixos/disks/snapraid.nix
"crypted-content-disk-${j}" = {
device = "${CONTENT_DISK_ID}";
keyFile = "/etc/secrets/disks/content-disk-${j}";
};
EOF
done
echo -e "\n✅ Generated $CONTENT_DISK_NUMBER data disk configuration(s)."
j=0
for i in $(seq $PARITY_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do
export j=$((j+1))
export PARITY_DISK_ID="${DATA_DISKS_ID[${i}]}"
(envsubst < "templates/nix-config/disks/parity.nix") >> final-nix-config/etc/nixos/disks/disko.nix
cat << EOF >> final-nix-config/etc/nixos/disks/snapraid.nix
"crypted-parity-disk-${j}" = {
device = "${PARITY_DISK_ID}";
keyFile = "/etc/secrets/disks/parity-disk-${j}}";
};
EOF
done
echo -e "\n✅ Generated $PARITY_DISK_NUMBER parity disk configuration(s)."
# Close the snapraid.nix block
cat <<'EOF' >> final-nix-config/etc/nixos/disks/snapraid.nix
# Automatic data disks unlock <--
};
}
EOF
fi
# Close the disko.nix block
cat <<'EOF' >> final-nix-config/etc/nixos/disks/disko.nix
};
};
}
EOF
echo -e "\n✅ Final disko configuration created."
if [[ -n "${DATA_DISKS_ID[@]}" ]]; then
for i in ${!DATA_DISKS_ID[@]}; do
if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then
DISK_ID_LIST+=("${DATA_DISKS_ID[${i}]}")
fi
done
if [[ -n "${DISK_ID_LIST[@]}" ]]; then
cp -avu templates/nix-config/disks/spindown.nix final-nix-config/etc/nixos/disks/
sed -i "s|DISK_ID_LIST|${DISK_ID_LIST[@]}|" final-nix-config/etc/nixos/disks/spindown.nix
echo -e "\n✅ Disk spindown configuration created."
fi
fi
### Config generation <--
### --> Generate unlock keys
for i in $(seq 1 "${#BOOT_DISKS_ID[@]}"); do
PASS="$(xkcdpass -d "-")"
echo -n "$PASS" > "final-nix-config/etc/secrets/disks/boot-disk-${i}"
chmod 600 "final-nix-config/etc/secrets/disks/boot-disk-${i}"
done
if [[ "$CONTENT_DISK_NUMBER" -gt 0 ]]; then
for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do
PASS="$(xkcdpass -d "-")"
echo -n "$PASS" > "final-nix-config/etc/secrets/disks/content-disk-${i}"
chmod 600 "final-nix-config/etc/secrets/disks/content-disk-${i}"
done
fi
if [[ "$PARITY_DISK_NUMBER" -gt 0 ]]; then
for i in $(seq 1 "$PARITY_DISK_NUMBER"); do
PASS="$(xkcdpass -d "-")"
echo -n "$PASS" > "final-nix-config/etc/secrets/disks/parity-disk-${i}"
chmod 600 "final-nix-config/etc/secrets/disks/parity-disk-${i}"
done
fi
### Generate unlock keys <--
}
export_configuration() {
cp deploy.conf final-nix-config/etc/numbus-server/numbus-server.conf
local CONFIG_EXPORT_DIR="final-nix-config/etc/numbus-server/"
local CONFIG_EXPORT_FILE="${CONFIG_EXPORT_DIR}/numbus-server.conf"
cp -ravu templates/post-install/numbus-server.sh "$CONFIG_EXPORT_DIR"
echo "# SERVICE SETTINGS" >> $CONFIG_EXPORT_FILE
echo "SELECTED_SERVICES=(${SELECTED_SERVICES[@]})" >> $CONFIG_EXPORT_FILE
echo "# DISK SETTINGS" >> $CONFIG_EXPORT_FILE
echo "BOOT_DISK_ID_LIST=(${BOOT_DISKS_ID[@]})" >> $CONFIG_EXPORT_FILE
echo "DATA_DISKS_ID_LIST=(${DATA_DISKS_ID[@]})" >> $CONFIG_EXPORT_FILE
echo "SPINDOWN_DISKS_ID_LIST=(${DISK_ID_LIST[@]})" >> $CONFIG_EXPORT_FILE
echo "CONTENT_DISK_NUMBER=$CONTENT_DISK_NUMBER" >> $CONFIG_EXPORT_FILE
echo "PARITY_DISK_NUMBER=$PARITY_DISK_NUMBER" >> $CONFIG_EXPORT_FILE
}
sum_up() {
RECAP_CONTENT=$(cat << EOF
### Generated Secrets Summary
Please save these secrets in a secure location (e.g., a password manager).
**Service Credentials:**
* **Home Assistant MQTT User:** \`$HOME_ASSISTANT_MQTT_USER\`
* **Home Assistant MQTT Password:** \`$HOME_ASSISTANT_MQTT_PASSWORD\`
* **Passbolt DB Name:** \`$PASSBOLT_MYSQL_DATABASE\`
* **Passbolt DB User:** \`$PASSBOLT_MYSQL_USER\`
* **Passbolt DB Password:** \`$PASSBOLT_MYSQL_PASSWORD\`
* **Pi-hole Web Password:** \`$FTLCONF_WEBSERVER_PASSWORD\`
* **Immich DB Name:** \`$IMMICH_DB_DATABASE_NAME\`
* **Immich DB User:** \`$IMMICH_DB_USERNAME\`
* **Immich DB Password:** \`$IMMICH_DB_PASSWORD\`
**Disk Encryption Keys:**
$(for i in $(seq 1 "${#BOOT_DISKS_ID[@]}"); do f="final-nix-config/etc/secrets/disks/boot-disk-${i}"; [[ -f "$f" ]] && echo "* **Boot Disk $i Key:** \`$(cat "$f")\`"; done)
$(if [[ "$CONTENT_DISK_NUMBER" -gt 0 ]]; then for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do f="final-nix-config/etc/secrets/disks/content-disk-${i}"; [[ -f "$f" ]] && echo "* **Content Disk $i Key:** \`$(cat "$f")\`"; done; fi)
$(if [[ "$PARITY_DISK_NUMBER" -gt 0 ]]; then for i in $(seq 1 "$PARITY_DISK_NUMBER"); do f="final-nix-config/etc/secrets/disks/parity-disk-${i}"; [[ -f "$f" ]] && echo "* **Parity Disk $i Key:** \`$(cat "$f")\`"; done; fi)
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "$RECAP_CONTENT")"
gum confirm "Do you want to deploy NixOS on the target host?" || { echo -e "\n\n❌ Aborting as requested"; exit 1; }
}
deploy() {
echo -e "\n\n🔄 Deploying to the remote server..."
nix run github:nix-community/nixos-anywhere -- \
--flake ./final-nix-config/etc/nixos#numbus-server \
--extra-files final-nix-config \
--chown "/home/numbus-admin/" 1000:1000 \
--target-host ${TARGET_USER}@${TARGET_HOST}
echo -e "\n\n✅ Installation successfull !"
sleep 1
}
postrun_action() {
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 --title "Rebooting the remote..." -- sleep 120
gum confirm "➡️ Select 'yes' once the machine rebooted and you unlocked the disks." || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
gum spin --title "\n\n🔄 Waiting for the server to boot up..." --auto << EOF
while FOUND="false"; do
if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then
FOUND="true"
exit 0
(i++)
if [[ "\${i}" -gt 150 ]]; then
echo -e "\n\n❌ Could not connect to the server after 150 retries. \
This is most likely due to a networking issue. Please double check your network settings. Aborting."
exit 1
fi
fi
done
EOF
ssh_to_host 'bash -s' << EOF
sed -i "s|# ./disks/pcr-check.nix| ./disks/pcr-check.nix|" /etc/nixos/configuration.nix
if [[ ${#BOOT_DISKS_ID[@]} -eq 1 ]]; then
echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-1
elif [[ ${#BOOT_DISKS_ID[@]} -eq 2 ]]; then
echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-1
echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-2
fi
PCR_HASH=\$(echo $REMOTE_PASS | sudo -S systemd-analyze pcrs 15 --json=short)
sed -i "s|# systemIdentity.enable = true;| systemIdentity.enable = true;|" /etc/nixos/configuration.nix
sed -i "s|# systemIdentity.pcr15 = "PCR_HASH";| systemIdentity.pcr15 = "PCR_HASH";|" /etc/nixos/configuration.nix
sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix
EOF
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will now set the password of the numbus-admin user. \
You will almost never user it. Consider using a very strong password : you can write it down \
securely on a hidden sheet of paper or add it to your password manager (local with Passbolt \
any other online password manager provider.)."
gum confirm "➡️ I understand, 'yes' to proceed." || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
echo $REMOTE_PASS | sudo -S passwd numbus-admin
}
congrats() {
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'CONGRATULATIONS !!:') You now have a working home server. \
Data stored on there will be fully yours and protected. Keep in my mind this comes with the \
responsability of managing it and keeping it secure. Now, you have to log in the webpages of \
the services you installed. Create an admin account for all of them and configure them (or keep \
it simple and use defaults) and take care to note down all the passwords. Change all default passwords \
and create user accounts for your family or friends that will use the server.
Cheers !!"
}
nixos_update() {
echo -e "\n\n🔄 Updating NixOS on the remote server..."
echo "coming soon !"
}
set -euo pipefail
fastfetch --logo nixos --structure ' '
cat << EOF
██████ █████ █████
▒▒██████ ▒▒███ ▒▒███
▒███▒███ ▒███ █████ ████ █████████████ ▒███████ █████ ████ █████
▒███▒▒███▒███ ▒▒███ ▒███ ▒▒███▒▒███▒▒███ ▒███▒▒███▒▒███ ▒███ ███▒▒
▒███ ▒▒██████ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒█████
▒███ ▒▒█████ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒▒▒███
█████ ▒▒█████ ▒▒████████ █████▒███ █████ ████████ ▒▒████████ ██████
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
█████████
███▒▒▒▒▒███
▒███ ▒▒▒ ██████ ████████ █████ █████ ██████ ████████
▒▒█████████ ███▒▒███▒▒███▒▒███▒▒███ ▒▒███ ███▒▒███▒▒███▒▒███
▒▒▒▒▒▒▒▒███▒███████ ▒███ ▒▒▒ ▒███ ▒███ ▒███████ ▒███ ▒▒▒
███ ▒███▒███▒▒▒ ▒███ ▒▒███ ███ ▒███▒▒▒ ▒███
▒▒█████████ ▒▒██████ █████ ▒▒█████ ▒▒██████ █████
▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒
EOF
sleep 1
# 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")
TARGET_USER="nixos"
if [[ "$ACTION_ANSWER" == "[1] 🌐 Deploy NixOS on a remote machine" ]]; then
echo -e "\n➡️ Proceeding with deployment…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : start the computer and boot into the NixOS iso.
Launch a console and set up a new user password"
gum confirm "Do you understand and wish to proceed?" || { echo "❌ Aborting as requested"; exit 1; }
necessary_credentials
generate_folder_tree
setup_ssh
hardware_detection
services_selection
files_generation
disk_config_generation
export_configuration
sum_up
deploy
TARGET_USER="numbus-admin"
REMOTE_PASS="changeMe!"
postrun_action
congrats
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…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : start the computer and boot into the NixOS iso.
Launch a console and set up a new user password"
gum confirm "Do you understand and wish to proceed?" || { echo "❌ Aborting as requested"; exit 1; }
necessary_credentials_with_config
generate_folder_tree
setup_ssh
hardware_detection
services_selection
files_generation
disk_config_generation
export_configuration
sum_up
deploy
TARGET_USER="numbus-admin"
REMOTE_PASS="changeMe!"
postrun_action
congrats
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"
gum confirm "Do you understand and wish to proceed?" || { echo "❌ Aborting as requested."; exit 1; }
nixos_update
else
echo "Aborting - you did not type '1, 2 or 3'"
exit 1
fi