#!/usr/bin/env nix-shell #!nix-shell -i bash -p bash coreutils gnused 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 echo -e "\n\n➡️ This script needs information about the target you want to install NixOS on\n" user_input "TARGET_HOST" " Please provide the IP address of the target host :" "For example : 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 :" "For example : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." "true" # TRAEFIK SETTINGS echo -e "\n\n➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains\n" user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : 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) :" "For example : 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 :" "For example : 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 :" "For example : 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 :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format." user_input "SENDER_EMAIL_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number." # NETWORK SETTINGS echo -e "\n\n➡️ This server will connect to your local network and you will configure its IP address\n" user_input "HOME_ROUTER_SUBNET" " Please provide your home network subnet :" "For example 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 :" "Most likely 192.168.1.1 or 192.168.1.254" "${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.) :" "For example 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.1 else gum spin --title "❌ "${VAR}" is missing or empty" -- sleep 1 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/secrets/disks/ mkdir -p final-nix-config/etc/numbus-server/ mkdir -p final-nix-config/etc/nixos/misc/ mkdir -p final-nix-config/etc/nixos/disks/ 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 -e "\n✅ SSH key copied successfully." else echo -e "\n❌ 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 "\n✅ Hardware configuration generated" else echo -e "\n❌ 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 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/ cp -avu templates/nix-config/misc/* final-nix-config/etc/nixos/misc/ 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❌ 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 echo "" 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 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 echo "" 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 DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}") 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*" ) **Data Disks ($CONTENT_DISK_NUMBER):** $( j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Data ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) $( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" ) **Parity Disks ($PARITY_DISK_NUMBER):** $( j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Parity ${j}:** \`${DATA_DISKS_ID[${i}]}\`" && j=$((j + 1)); done ) $( [[ $PARITY_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 $CONTENT_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/ local FORMATTED_DISKS="" for disk in "${DISK_ID_LIST[@]}"; do FORMATTED_DISKS+=" \"$disk\"\n" done sed -i "s|DISK_LIST|${FORMATTED_DISKS}|" 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}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-disk-${i}" EOF 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}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-disk-${i}" EOF 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}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-disk-${i}" EOF 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:** $( [[ -n ${HOME_ASSISTANT_MQTT_USER:-} ]] && echo "* **Home Assistant MQTT User:** \`${HOME_ASSISTANT_MQTT_USER}\`" && \ echo "* **Home Assistant MQTT Password:** \`$HOME_ASSISTANT_MQTT_PASSWORD\`" \ || echo "* **Home assistant:** *Not configured*" ) $( [[ -n ${FTLCONF_WEBSERVER_PASSWORD:-} ]] && echo "* **Pi-hole Web Password:** \`${FTLCONF_WEBSERVER_PASSWORD}\`" \ || echo "* **Pi-hole:** *Not configured*" ) $( [[ -n ${PASSBOLT_DB_NAME:-} ]] && echo "* **Passbolt DB Name:** \`${PASSBOLT_DB_NAME}\`" && \ echo "* **Passbolt DB User:** \`${PASSBOLT_DB_USERNAME}\`" && echo "* **Passbolt DB Password:** \`${PASSBOLT_DB_PASSWORD}\`" \ || echo "* **Pi-hole:** *Not configured*" ) $( [[ -n ${IMMICH_DB_NAME:-} ]] && echo "* **Immich DB Name:** \`${IMMICH_DB_NAME}\`" && \ echo "* **Immich DB User:** \`${IMMICH_DB_USERNAME}\`" && echo "* **Immich DB Password:** \`${IMMICH_DB_PASSWORD}\`" \ || echo "* **Immich:** *Not configured*" ) $( [[ -n ${GITEA_DB_NAME:-} ]] && echo "* **Gitea DB Name:** \`${GITEA_DB_NAME}\`" && \ echo "* **Gitea DB User:** \`${GITEA_DB_USERNAME}\`" && echo "* **Gitea DB Password:** \`${GITEA_DB_PASSWORD}\`" \ || echo "* **Gitea:** *Not configured*" ) **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() { git -C "/home/nixosd/numbus-server" add -f "final-nix-config" 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