#!/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 curl jq ### --> 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" "SERVER_OWNER_NAME" "SELECTED_SERVICES" \ "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 } strictly_necessary_information() { export IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' 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" } necessary_information() { # Regex Definitions 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+/]+.*' echo -e "\n\n➡️ This script needs information about the target you want to install NixOS on\n" #TARGET SETTINGS 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" 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" # TRAEFIK SETTINGS 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" user_input "SERVER_OWNER_NAME" " Please provide the name of the server owner :" "For example : Steve" "" "" 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" # SMTP SETTINGS 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." echo -e "\n\n➡️ This server will connect to your local network and you will configure its IP address\n" # NETWORK SETTINGS 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_information_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 style "✅ "${VAR}" imported successfully from the config file" else gum style "❌ "${VAR}" is missing or empty" 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 } more_information_config() { sshpass -p "${REMOTE_PASS}" scp ${TARGET_USER}@${TARGET_HOST}:/etc/numbus-server/numbus-server.conf . source "numbus-server.conf" } setup_ssh() { mkdir -p final-nix-config/ mkdir -p final-nix-config/etc/ mkdir -p final-nix-config/etc/nixos/ mkdir -p final-nix-config/home/ mkdir -p final-nix-config/home/numbus-admin/ mkdir -p final-nix-config/home/numbus-admin/.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/ > /dev/null 2>&1 | grep -iq "renderD128" && TARGET_GRAPHICS_RENDERER="true" || TARGET_GRAPHICS_RENDERER="false" lsusb > /dev/null 2>&1 | grep -iq "google" && TARGET_USB_CORAL="true" || TARGET_USB_CORAL="false" lspci -nn > /dev/null 2>&1 | grep -iq "089a" && TARGET_PCIE_CORAL="true" || TARGET_PCIE_CORAL="false" ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" && TARGET_ZIGBEE_DEVICE=\$(ls /dev/serial/by-id/ > /dev/null 2>&1 | grep -i "zigbee" | head -n 1) || TARGET_ZIGBEE_DEVICE="" TARGET_INTERFACE=\$(ip -4 route show default | awk '{print \$5}' | head -n1) if ls -l /sys/class/tpm/tpm0/ > /dev/null 2>&1; then TARGET_TPM="true" TARGET_TPM_VERSION=\$(cat /sys/class/tpm/tpm0/tpm_version_major) else TARGET_TPM="false" fi HDD=1 DISK_DEVPATH=() DISK_NAME=() DISK_TYPE=() DISK_HEALTH=() DISK_ID=() DISK_SIZE=() DISK_SIZE_BYTES=() 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 [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD"); elif [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB"); 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)") DISK_SIZE_BYTES+=("\$(lsblk -x SIZE -d -n -e 7,11 -b -o SIZE /dev/\$DISK)") done echo "# Hardware detection results on \$(date)" > "${TMPFILE}" for var in \ TARGET_GRAPHICS \ TARGET_GRAPHICS_BRAND \ TARGET_GRAPHICS_RENDERER \ TARGET_USB_CORAL \ TARGET_PCIE_CORAL \ TARGET_ZIGBEE_DEVICE \ TARGET_INTERFACE \ TARGET_TPM \ TARGET_TPM_VERSION; do echo "export \${var}=\${!var}" >> "${TMPFILE}" done for var in \ DISK_DEVPATH \ DISK_NAME \ DISK_TYPE \ DISK_HEALTH \ DISK_ID \ DISK_SIZE \ DISK_SIZE_BYTES; do declare -p \${var} | sed 's/^declare /declare -g /' >> "${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 -rf "${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" "virtualization" ) 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" \ "Virtualization : Run Virtual Machines (KVM/QEMU) with Libvirt" ) 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 export SELECTED_SERVICES } disks_selection() { ### --> 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 <-- ### --> 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}]}}") BOOT_DISKS_NAME+=("${DISK_NAME[${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]}" export BOOT_DISK_1_NAME="${DISK_NAME[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]}" export BOOT_DISK_1_NAME="${BOOT_DISKS_NAME[0]}" export BOOT_DISK_2_NAME="${BOOT_DISKS_NAME[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[@]}") ### Disk selection <-- 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 if [[ "${#DATA_DISKS_ID[@]}" -eq 1 ]]; then export PARITY_DISK_NUMBER=0 export CONTENT_DISK_NUMBER=1 else export PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3)) export CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER)) fi export DATA_DISKS_ID export DATA_DISKS_TYPE } folder_tree_generation() { 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/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 } services_generation() { 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 ✅ Writing configuration files for the selected homelab services..." cp -avu templates/nix-config/podman/traefik.nix final-nix-config/etc/nixos/podman/traefik.nix cp -avu templates/nix-config/configuration.nix final-nix-config/etc/nixos/configuration.nix envsubst < templates/podman-config/traefik/traefik.yaml > final-nix-config/mnt/config/traefik/traefik.yaml for service in "${SELECTED_SERVICES[@]}"; do if [[ "${service}" == "frigate" ]]; then local FRIGATE_DEVICES_BLOCK="" [[ "${TARGET_GRAPHICS_RENDERER}" == "true" ]] && FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n" [[ "${TARGET_USB_CORAL}" == "true" ]] && FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n" if [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then 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 elif [[ "${service}" == "gitea" ]]; then generate_db_creds "GITEA" PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"gitea_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.10.0/24\" --ip-range=\"172.16.10.0/24\" --gateway=\"172.16.10.254\" \"gitea_frontend\""$'\n' PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"gitea_backend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.1.0/24\" --ip-range=\"172.16.1.0/24\" --gateway=\"172.16.1.254\" \"gitea_backend\""$'\n' TRAEFIK_NETWORKS+=" gitea_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.10.253"$'\n' TRAEFIK_REF_NETWORKS+=" gitea_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "home-assistant" ]]; then 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" PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"home-assistant_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.20.0/24\" --ip-range=\"172.16.20.0/24\" --gateway=\"172.16.20.254\" \"home-assistant_frontend\""$'\n' PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"home-assistant_backend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.2.0/24\" --ip-range=\"172.16.2.0/24\" --gateway=\"172.16.2.254\" \"home-assistant_backend\""$'\n' TRAEFIK_NETWORKS+=" home-assistant_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.20.253"$'\n' TRAEFIK_REF_NETWORKS+=" home-assistant_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "immich" ]]; then local IMMICH_DEVICES_BLOCK="" if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then 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" PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"immich_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.30.0/24\" --ip-range=\"172.16.30.0/24\" --gateway=\"172.16.30.254\" \"immich_frontend\""$'\n' PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"immich_backend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.3.0/24\" --ip-range=\"172.16.3.0/24\" --gateway=\"172.16.3.254\" \"immich_backend\""$'\n' TRAEFIK_NETWORKS+=" immich_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.30.253"$'\n' TRAEFIK_REF_NETWORKS+=" immich_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "it-tools" ]]; then PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"it-tools_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.40.0/24\" --ip-range=\"172.16.40.0/24\" --gateway=\"172.16.40.254\" \"it-tools_frontend\""$'\n' TRAEFIK_NETWORKS+=" it-tools_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.40.253"$'\n' TRAEFIK_REF_NETWORKS+=" it-tools_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "nextcloud" ]]; then envsubst < templates/podman-config/traefik/nextcloud.yaml > final-nix-config/mnt/config/traefik/rules/nextcloud.yaml PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"nextcloud-aio\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.50.0/24\" --ip-range=\"172.16.50.0/24\" --gateway=\"172.16.50.254\" \"nextcloud-aio\""$'\n' TRAEFIK_NETWORKS+=" nextcloud-aio:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.50.253"$'\n' TRAEFIK_REF_NETWORKS+=" nextcloud-aio:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "passbolt" ]]; then 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 PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"passbolt_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.60.0/24\" --ip-range=\"172.16.60.0/24\" --gateway=\"172.16.60.254\" \"passbolt_frontend\""$'\n' PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"passbolt_backend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.6.0/24\" --ip-range=\"172.16.6.0/24\" --gateway=\"172.16.6.254\" \"passbolt_backend\""$'\n' TRAEFIK_NETWORKS+=" passbolt_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.60.253"$'\n' TRAEFIK_REF_NETWORKS+=" passbolt_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "pi-hole" ]]; then export FTLCONF_WEBSERVER_PASSWORD="$(xkcdpass -d "-")" PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"pi-hole_frontend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.70.0/24\" --ip-range=\"172.16.70.0/24\" --gateway=\"172.16.70.254\" \"pi-hole_frontend\""$'\n' PODMAN_NETWORKS+=" \${pkgs.podman}/bin/podman network exists \"pi-hole_backend\" || \${pkgs.podman}/bin/podman network create --driver=\"bridge\" --subnet=\"172.16.7.0/24\" --ip-range=\"172.16.7.0/24\" --gateway=\"172.16.7.254\" \"pi-hole_backend\""$'\n' TRAEFIK_NETWORKS+=" pi-hole_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.70.253"$'\n' TRAEFIK_REF_NETWORKS+=" pi-hole_backend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' elif [[ "${service}" == "virtualization" ]]; then sed -i "s|# virtualisation.libvirtd.enable = true;| virtualisation.libvirtd.enable = true;|" final-nix-config/etc/nixos/configuration.nix sed -i "s|# programs.virt-manager.enable = true;| programs.virt-manager.enable = true;|" final-nix-config/etc/nixos/configuration.nix sed -i 's|extraGroups = \[ "wheel" \];|extraGroups = [ "wheel" "libvirtd" ];|' final-nix-config/etc/nixos/configuration.nix fi done export PODMAN_NETWORKS export TRAEFIK_NETWORKS export TRAEFIK_REF_NETWORKS } disks_generation() { # Boot disk(s) 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]}" if [[ "${DATA_DISKS_TYPE[0]}" == "HDD" ]]; then export ALLOW_DISCARDS="false"; else export ALLOW_DISCARDS="true"; fi (envsubst < "templates/nix-config/disks/content.nix") >> final-nix-config/etc/nixos/disks/disko.nix sed -i "s|/mnt/content-1|/mnt/data|" 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]}" # Calculate partition size to avoid mdadm size mismatch warning local SIZE1=0 local SIZE2=0 for k in "${!DISK_ID[@]}"; do [[ "${DISK_ID[$k]}" == "$CONTENT_DISK_ID" ]] && SIZE1="${DISK_SIZE_BYTES[$k]}" [[ "${DISK_ID[$k]}" == "$PARITY_DISK_ID" ]] && SIZE2="${DISK_SIZE_BYTES[$k]}" done # Fallback to devpath if ID lookup failed if [[ "$SIZE1" == "0" ]]; then for k in "${!DISK_DEVPATH[@]}"; do [[ "${DISK_DEVPATH[$k]}" == "$CONTENT_DISK_ID" ]] && SIZE1="${DISK_SIZE_BYTES[$k]}"; done; fi if [[ "$SIZE2" == "0" ]]; then for k in "${!DISK_DEVPATH[@]}"; do [[ "${DISK_DEVPATH[$k]}" == "$PARITY_DISK_ID" ]] && SIZE2="${DISK_SIZE_BYTES[$k]}"; done; fi local MIN_SIZE=$SIZE1 if [[ "$SIZE2" -lt "$SIZE1" ]]; then MIN_SIZE=$SIZE2; fi # Subtract 2GB for safety (headers, rounding) local PART_SIZE_GIB=$(( (MIN_SIZE - 2147483648) / 1024 / 1024 / 1024 )) export PARTITION_SIZE="${PART_SIZE_GIB}G" (envsubst < "templates/nix-config/disks/mirror.nix") >> final-nix-config/etc/nixos/disks/disko.nix # SnapRAID configuration elif [[ "$CONTENT_DISK_NUMBER" -gt 1 ]]; then sed -i "s|# ./disks/snapraid.nix| ./disks/snapraid.nix|" final-nix-config/etc/nixos/configuration.nix j=0 for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do export j=$((j + 1)) export CONTENT_DISK_ID="${DATA_DISKS_ID[${i}]}" if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then export ALLOW_DISCARDS="false"; else export ALLOW_DISCARDS="true"; fi (envsubst < "templates/nix-config/disks/content.nix") >> final-nix-config/etc/nixos/disks/disko.nix SNAPRAID_CONTENT_FILES+=" \"/mnt/content-${j}/snapraid.content\""$'\n' SNAPRAID_DATA_DISKS+=" d${j} = \"/mnt/content-${j}\";"$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.cryptsetup}/bin/cryptsetup open ${CONTENT_DISK_ID}-part1 crypted-content-${j} --key-file /etc/secrets/disks/content-${j}"$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.coreutils}/bin/mkdir -p /mnt/content-${j}"$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.util-linux}/bin/mount /mnt/content-${j}"$'\n' MOUNT_DEPENDENCIES_STOP+=" \${pkgs.util-linux}/bin/umount /mnt/content-${j}"$'\n' MOUNT_DEPENDENCIES_STOP+=" \${pkgs.cryptsetup}/bin/cryptsetup close crypted-content-${j}"$'\n' 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}]}" if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then export ALLOW_DISCARDS="false"; else export ALLOW_DISCARDS="true"; fi (envsubst < "templates/nix-config/disks/parity.nix") >> final-nix-config/etc/nixos/disks/disko.nix SNAPRAID_PARITY_FILES+=" \"/mnt/parity-${j}/snapraid.parity\""$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.cryptsetup}/bin/cryptsetup open ${PARITY_DISK_ID}-part1 crypted-parity-${j} --key-file /etc/secrets/disks/parity-${j}"$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.coreutils}/bin/mkdir -p /mnt/parity-${j}"$'\n' MOUNT_DEPENDENCIES_START+=" \${pkgs.util-linux}/bin/mount /mnt/parity-${j}"$'\n' MOUNT_DEPENDENCIES_STOP+=" \${pkgs.util-linux}/bin/umount /mnt/parity-${j}"$'\n' MOUNT_DEPENDENCIES_STOP+=" \${pkgs.cryptsetup}/bin/cryptsetup close crypted-parity-${j}"$'\n' done echo -e "\n ✅ Generated $PARITY_DISK_NUMBER parity disk configuration(s)." export SNAPRAID_CONTENT_FILES export SNAPRAID_DATA_DISKS export SNAPRAID_PARITY_FILES export MOUNT_DEPENDENCIES_START export MOUNT_DEPENDENCIES_STOP envsubst < templates/nix-config/disks/snapraid.nix > final-nix-config/etc/nixos/disks/snapraid.nix 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 SPINDOWN_DISKS_ID+=("${DATA_DISKS_ID[${i}]}") fi done if [[ -n "${SPINDOWN_DISKS_ID[@]}" ]]; then cp -avu templates/nix-config/disks/spindown.nix final-nix-config/etc/nixos/disks/ local FORMATTED_DISKS="" for disk in "${SPINDOWN_DISKS_ID[@]}"; 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 <-- } keys_generation() { ### --> Generate disk keys for i in $(seq 1 "${#BOOT_DISKS_ID[@]}"); do PASS="$(xkcdpass)" echo -n "$PASS" > "final-nix-config/etc/secrets/disks/boot-${i}" chmod 600 "final-nix-config/etc/secrets/disks/boot-${i}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S mkdir -p /etc/secrets/disks/ echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" EOF done if [[ ${CONTENT_DISK_NUMBER} -ne 1 && ${PARITY_DISK_NUMBER} -ne 1 ]]; then for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do PASS="$(xkcdpass)" echo -n "$PASS" > "final-nix-config/etc/secrets/disks/content-${i}" chmod 600 "final-nix-config/etc/secrets/disks/content-${i}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" EOF done for i in $(seq 1 "$PARITY_DISK_NUMBER"); do PASS="$(xkcdpass)" echo -n "$PASS" > "final-nix-config/etc/secrets/disks/parity-${i}" chmod 600 "final-nix-config/etc/secrets/disks/parity-${i}" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" EOF done else PASS="$(xkcdpass)" echo -n "$PASS" > "final-nix-config/etc/secrets/disks/mirror" chmod 600 "final-nix-config/etc/secrets/disks/mirror" ssh_to_host 'bash -s' << EOF echo "$REMOTE_PASS" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/mirror" EOF fi ### Generate disk keys <-- 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 } nix_generation() { echo -e "\n ✅ Copying the configuration to the new machine..." 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 "${SERVER_OWNER_NAME:-User}" > final-nix-config/etc/numbus-server/owner 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 sed -i "s|TARGET_INTERFACE|${TARGET_INTERFACE}|g" final-nix-config/etc/nixos/misc/networking.nix sed -i "s|DOMAIN_NAME|${DOMAIN_NAME}|" final-nix-config/etc/nixos/misc/mail.nix sed -i "s|EMAIL_ADDRESS|${EMAIL_ADDRESS}|" final-nix-config/etc/nixos/misc/mail.nix sed -i "s|SENDER_MAIL_DOMAIN|${SENDER_EMAIL_DOMAIN}|" final-nix-config/etc/nixos/misc/mail.nix sed -i "s|SENDER_MAIL_ADDRESS|${SENDER_EMAIL_ADDRESS}|" final-nix-config/etc/nixos/misc/mail.nix sed -i "s*PODMAN_NETWORKS*${PODMAN_NETWORKS//$'\n'/\\n}*" final-nix-config/etc/nixos/misc/activation.nix sed -i "s|TRAEFIK_NETWORKS|${TRAEFIK_NETWORKS//$'\n'/\\n}|" final-nix-config/etc/nixos/podman/traefik.nix sed -i "s|TRAEFIK_REF_NETWORKS|${TRAEFIK_REF_NETWORKS//$'\n'/\\n}|" final-nix-config/etc/nixos/podman/traefik.nix if [[ "${TARGET_TPM}" == "true" ]]; then sed -i "s|# boot.initrd.systemd.tpm2.enable = true;| boot.initrd.systemd.tpm2.enable = true;|" final-nix-config/etc/nixos/configuration.nix cp -avu templates/nix-config/disks/pcr-check.nix final-nix-config/etc/nixos/disks/ sed -i "s|# ./disks/pcr-check.nix| ./disks/pcr-check.nix|" final-nix-config/etc/nixos/configuration.nix fi } sum_up() { ### --> Disk selection recap DISK_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):** $( [[ $PARITY_DISK_NUMBER -gt 0 ]] && 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 <<< "${DISK_RECAP_CONTENT}")" gum confirm "Proceed with this disk configuration?" || { echo -e "\n\n ❌ Aborting as requested."; exit 1; } ### Disk selection recap <-- ### Keys recap <-- KEYS_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 "* **Passbolt:** *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-${i}"; [[ -f "$f" ]] && echo "* **Boot Disk $i Key:** \`$(cat "$f")\`"; done ) $( for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do f="final-nix-config/etc/secrets/disks/content-${i}"; [[ -f "$f" ]] && echo "* **Content Disk $i Key:** \`$(cat "$f")\`"; done ) $( for i in $(seq 1 "$PARITY_DISK_NUMBER"); do f="final-nix-config/etc/secrets/disks/parity-${i}"; [[ -f "$f" ]] && echo "* **Parity Disk $i Key:** \`$(cat "$f")\`"; done ) EOF ) gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${KEYS_RECAP_CONTENT}")" gum confirm "Do you want to deploy NixOS on the target host?" || { echo -e "\n\n ❌ Aborting as requested"; exit 1; } ### Keys recap <-- } cloudflare_dns_setup() { local ZONE_ID local RECORD_COUNT local IS_MATCHING local DNS_RECORDS create_records() { local SUBDOMAIN="${1}" local CREATION_STATUS CREATION_STATUS=$(curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \ -H "Authorization: Bearer ${CF_DNS_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "{\"type\":\"A\",\"name\":\"${SUBDOMAIN}\",\"content\":\"${HOME_SERVER_IP}\",\"ttl\":1,\"proxied\":false}" | jq -r '.success') if [[ "${CREATION_STATUS}" == "true" ]]; then echo " ✅ Successfully created a DNS record for ${SUBDOMAIN}" else echo -e " ❌ Failed to create a DNS record for ${SUBDOMAIN}. Check documentation to \n learn how you can create them manually." fi } erase_records() { local SUBDOMAIN="${1}" gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " ⚠️ $(gum style --foreground 212 'WARNING:') One or more existing type A DNS records found for \`${SUBDOMAIN}\`. This script can clear those DNS records for you and create the correct ones needed for the server. If you are unsure that these records are actually in use, please select \"no\"." gum confirm "Select \"yes\" to clear ALL EXISTING type A DNS records for this subdomain and automatically create the correct ones." \ || { echo -e "\n ⚠️ DNS records for ${SUBDOMAIN} will not be updated"; return 0; } RECORD_IDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${SUBDOMAIN}&type=A" \ -H "Authorization: Bearer ${CF_DNS_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[].id') for id in ${RECORD_IDS}; do curl -s -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${id}" \ -H "Authorization: Bearer ${CF_DNS_API_TOKEN}" \ -H "Content-Type: application/json" > /dev/null 2>&1 done create_records "${SUBDOMAIN}" } echo -e "\n\n ☁️ Configuring Cloudflare DNS records..." for service in "${SELECTED_SERVICES[@]}"; do if [[ "${service}" == "nextcloud" ]]; then SELECTED_SERVICES_DNS+=("nextcloud.${DOMAIN_NAME}" "nextcloud-aio.${DOMAIN_NAME}") elif [[ "${service}" == "virtualization" ]]; then : else SELECTED_SERVICES_DNS+=("${service}.${DOMAIN_NAME}") fi done SELECTED_SERVICES_DNS+=("traefik.${DOMAIN_NAME}") # Get Zone ID ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN_NAME}" \ -H "Authorization: Bearer ${CF_DNS_API_TOKEN}" \ -H "Content-Type: application/json" | jq -r '.result[0].id') if [[ "${ZONE_ID}" == "null" || -z "${ZONE_ID}" ]]; then echo -e "\n\n ⚠️ Could not fetch Zone ID for ${DOMAIN_NAME}. Please check your Cloudflare \"DNS ZONE\" API token" echo "Check the Numbus-Server documentation to learn how to get one." fi # Check for existing records and create them if non-existent for service_domain in "${SELECTED_SERVICES_DNS[@]}"; do DNS_RECORDS=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${service_domain}&type=A" \ -H "Authorization: Bearer ${CF_DNS_API_TOKEN}" \ -H "Content-Type: application/json") RECORD_COUNT=$(echo "${DNS_RECORDS}" | jq '.result | length') if [[ "${RECORD_COUNT}" -eq 0 ]]; then echo -e "\n ⚠️ No DNS record found for ${service_domain}" create_records "${service_domain}" elif [[ "${RECORD_COUNT}" -eq 1 ]]; then if [[ $(echo "${DNS_RECORDS}" | jq ".result[0].content == \"${HOME_SERVER_IP}\"") == "true" ]]; then echo -e "\n ✅ DNS record already configured for ${service_domain}" else echo -e "\n ⚠️ No DNS record found for ${service_domain}" erase_records "${service_domain}" fi elif [[ "${RECORD_COUNT}" -gt 1 ]]; then erase_records "${service_domain}" fi done } 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 -avu templates/post-install/numbus-server.sh "$CONFIG_EXPORT_DIR" echo "export TARGET_INTERFACE=\"${TARGET_INTERFACE}\"" >> $CONFIG_EXPORT_FILE echo -e "\n# SERVER SETTINGS" >> $CONFIG_EXPORT_FILE echo "export SERVER_OWNER_NAME=\"${SERVER_OWNER_NAME:-User}\"" >> $CONFIG_EXPORT_FILE echo -e "\n# DISK SETTINGS" >> $CONFIG_EXPORT_FILE echo "export BOOT_DISKS_ID=\"(${BOOT_DISKS_ID[@]})\"" >> $CONFIG_EXPORT_FILE echo "export DATA_DISKS_ID=\"(${DATA_DISKS_ID[@]})\"" >> $CONFIG_EXPORT_FILE echo "export DATA_DISKS_TYPE=\"(${DATA_DISKS_TYPE[@]})\"" >> $CONFIG_EXPORT_FILE echo "export SPINDOWN_DISKS_ID=\"(${SPINDOWN_DISKS_ID[@]})\"" >> $CONFIG_EXPORT_FILE echo "export CONTENT_DISK_NUMBER=\"${CONTENT_DISK_NUMBER}\"" >> $CONFIG_EXPORT_FILE echo "export PARITY_DISK_NUMBER=\"${PARITY_DISK_NUMBER}\"" >> $CONFIG_EXPORT_FILE echo -e "\n# TPM SETTINGS" >> $CONFIG_EXPORT_FILE echo "export TARGET_TPM=\"${TARGET_TPM}\"" >> $CONFIG_EXPORT_FILE echo "export TARGET_TPM_VERSION=\"${TARGET_TPM_VERSION:-}\"" >> $CONFIG_EXPORT_FILE echo -e "\n# Podman SETTINGS" >> $CONFIG_EXPORT_FILE echo "export PODMAN_NETWORKS=\"${PODMAN_NETWORKS}\"" >> $CONFIG_EXPORT_FILE echo "export TRAEFIK_NETWORKS=\"${TRAEFIK_NETWORKS}\"" >> $CONFIG_EXPORT_FILE echo "export TRAEFIK_REF_NETWORKS=\"${TRAEFIK_REF_NETWORKS}\"" >> $CONFIG_EXPORT_FILE } 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() { TARGET_USER="numbus-admin" REMOTE_PASS="changeMe!" 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; } FOUND="false" i="0" while [[ "${FOUND}" == "false" ]]; do if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then FOUND="true" echo -e "\n✅ Ping ${HOME_SERVER_IP} successful ! Continuing..." else (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 ssh_to_host 'bash -s' << EOF echo "Enrolling boot disk key to TPM..." if [[ ${#BOOT_DISKS_ID[@]} -eq 1 ]]; then echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-1 /dev/${BOOT_DISK_1_NAME} elif [[ ${#BOOT_DISKS_ID[@]} -eq 2 ]]; then echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-1 /dev/${BOOT_DISK_1_NAME} echo $REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-2 /dev/${BOOT_DISK_2_NAME} fi echo "Getting PCRS 15 hash..." 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 } nix_update() { echo -e "\n\n🔄 Updating NixOS on the remote server..." nixos-rebuild --target-host numbus-admin@${TARGET_HOST} \ --use-remote-sudo switch --flake final-nix-config/etc/nixos#numbus-server } 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 !!" } 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") if [[ "$ACTION_ANSWER" == "[1] 🌐 Deploy NixOS on a remote machine" ]]; then TARGET_USER="nixos" 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; } strictly_necessary_information necessary_information setup_ssh hardware_detection services_selection disks_selection folder_tree_generation services_generation disks_generation keys_generation nix_generation cloudflare_dns_setup sum_up export_configuration deploy [[ ${TARGET_TPM} == "true" ]] && postrun_action congrats elif [[ "$ACTION_ANSWER" == "[2] 💽 Deploy NixOS on a remote machine with a file configuration" ]]; then TARGET_USER="nixos" 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_information_config setup_ssh hardware_detection disks_selection folder_tree_generation services_generation disks_generation keys_generation nix_generation cloudflare_dns_setup sum_up export_configuration deploy [[ ${TARGET_TPM} == "true" ]] && postrun_action congrats elif [[ "$ACTION_ANSWER" == "[3] 🛠️ Update a NixOS remote machine" ]]; then TARGET_USER="numbus-admin" 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; } strictly_necessary_information setup_ssh more_information_config folder_tree_generation nix_generation nix_update congrats else echo "Aborting - you did not type 1, 2 or 3" exit 1 fi