#!/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 if [[ "${DEBUG:-false}" == "true" ]]; then echo -e "\n✅ Debugging enabled." export DIR_COPY_FLAGS="ravu" export FILES_COPY_FLAGS="avu" else export DIR_COPY_FLAGS="rau" export FILES_COPY_FLAGS="au" 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" TARGET_TPM_VERSION="N/A" fi HDD=1 DISK_DEVPATH=() DISK_NAME=() DISK_TYPE=() DISK_HEALTH=() DISK_ID=() 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+=("\$(lsblk -x SIZE -d -n -e 7,11 -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; 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" ) SELECTED_SERVICES=() 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[@]}") BOOT_DISKS_ID=() BOOT_DISKS_NAME=() 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="${BOOT_DISKS_NAME[0]}" export BOOT_DISK_2_NAME="" 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 <-- DATA_DISKS_ID=() DATA_DISKS_TYPE=() 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 "-")" } generate_network() { local SERVICE="${1}" local HAS_BACKEND="${2:-0}" local NETWORK_NAME_OVERRIDE="${3:-}" if [[ -z "${NETWORK_NAME_OVERRIDE}" ]]; then NETWORK_ID=$((NETWORK_ID + 1)) PODMAN_NETWORKS+=" sudo -u numbus-admin podman network exists \"${SERVICE}_frontend\" || sudo -u numbus-admin podman network create --driver=\"bridge\" --subnet=\"172.16.${NETWORK_ID}.0/24\" --ip-range=\"172.16.${NETWORK_ID}.0/24\" --gateway=\"172.16.${NETWORK_ID}.254\" \"${SERVICE}_frontend\""$'\n' TRAEFIK_NETWORKS+=" ${SERVICE}_frontend:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.${NETWORK_ID}.253"$'\n' TRAEFIK_REF_NETWORKS+=" ${SERVICE}_frontend:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' if [[ "${HAS_BACKEND}" == "1" ]]; then NETWORK_ID=$((NETWORK_ID + 1)) PODMAN_NETWORKS+=" sudo -u numbus-admin podman network exists \"${SERVICE}_backend\" || sudo -u numbus-admin podman network create --driver=\"bridge\" --subnet=\"172.16.${NETWORK_ID}.0/24\" --ip-range=\"172.16.${NETWORK_ID}.0/24\" --gateway=\"172.16.${NETWORK_ID}.254\" \"${SERVICE}_backend\""$'\n' SERVICES_NETWORK_IDS+=("$(( ${NETWORK_ID} - 1 )),${NETWORK_ID}:${SERVICE}") else SERVICES_NETWORK_IDS+=("${NETWORK_ID}:${SERVICE}") fi else NETWORK_ID=$((NETWORK_ID + 1)) PODMAN_NETWORKS+=" sudo -u numbus-admin podman network exists \"${NETWORK_NAME_OVERRIDE}\" || sudo -u numbus-admin podman network create --driver=\"bridge\" --subnet=\"172.16.${NETWORK_ID}.0/24\" --ip-range=\"172.16.${NETWORK_ID}.0/24\" --gateway=\"172.16.${NETWORK_ID}.254\" \"${NETWORK_NAME_OVERRIDE}\""$'\n' TRAEFIK_NETWORKS+=" ${NETWORK_NAME_OVERRIDE}:"$'\n' TRAEFIK_NETWORKS+=" ipv4_address: 172.16.${NETWORK_ID}.253"$'\n' TRAEFIK_REF_NETWORKS+=" ${NETWORK_NAME_OVERRIDE}:"$'\n' TRAEFIK_REF_NETWORKS+=" external: true"$'\n' SERVICES_NETWORK_IDS+=("${NETWORK_ID}:${SERVICE}") fi export NETWORK_ID export PODMAN_NETWORKS export TRAEFIK_NETWORKS export TRAEFIK_REF_NETWORKS export SERVICES_NETWORK_IDS } NETWORK_ID=0 PODMAN_NETWORKS="" TRAEFIK_NETWORKS="" TRAEFIK_REF_NETWORKS="" SERVICES_NETWORK_IDS=() echo -e "\n ✅ Writing configuration files for the selected homelab services..." cp -${FILES_COPY_FLAGS} templates/nix-config/configuration.nix final-nix-config/etc/nixos/configuration.nix cp -${FILES_COPY_FLAGS} templates/nix-config/podman/traefik.nix final-nix-config/etc/nixos/podman/traefik.nix envsubst < templates/podman-config/traefik/traefik.yaml > final-nix-config/mnt/config/traefik/traefik.yaml for service in "${SELECTED_SERVICES[@]}"; do # Copy podman container file [[ "${service}" != "virtualization" ]] && cp -${FILES_COPY_FLAGS} templates/nix-config/podman/"${service}".nix final-nix-config/etc/nixos/podman/"${service}".nix # Frigate config 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 -${FILES_COPY_FLAGS} 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 # Gitea config elif [[ "${service}" == "gitea" ]]; then generate_network "${service}" "1" generate_db_creds "GITEA" # Home Assistant config elif [[ "${service}" == "home-assistant" ]]; then generate_network "${service}" "1" if [[ -n "${TARGET_ZIGBEE_DEVICE}" ]]; then local REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0" sed -i "s|# --- home-assistant 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/home-assistant/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" # Immich config elif [[ "${service}" == "immich" ]]; then generate_network "${service}" "1" generate_db_creds "IMMICH" 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 # Nextcloud config elif [[ "${service}" == "nextcloud" ]]; then generate_network "${service}" "0" "nextcloud-aio" envsubst < templates/podman-config/traefik/nextcloud.yaml > final-nix-config/mnt/config/traefik/rules/nextcloud.yaml # Passbolt config elif [[ "${service}" == "passbolt" ]]; then generate_network "${service}" "1" 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 config elif [[ "${service}" == "pi-hole" ]]; then generate_network "${service}" "0" export FTLCONF_WEBSERVER_PASSWORD="$(xkcdpass -d "-")" # Virtualization config 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 # Other podman containers with no special configuration else generate_network "${service}" "0" 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 SNAPRAID_CONTENT_FILES="" SNAPRAID_DATA_DISKS="" SNAPRAID_PARITY_FILES="" MOUNT_DEPENDENCIES_START="" MOUNT_DEPENDENCIES_STOP="" # 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 # SnapRAID configuration elif [[ ${CONTENT_DISK_NUMBER} -gt 0 && ${PARITY_DISK_NUMBER} -gt 0 ]]; 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)." [[ ${CONTENT_DISK_NUMBER} -eq 1 && ${PARITY_DISK_NUMBER} -eq 1 ]] && SNAPRAID_CONTENT_FILES+=" \"/mnt/content-0/snapraid.content\""$'\n' && SNAPRAID_DATA_DISKS+=" d0 = \"/mnt/content-0\";"$'\n' export SNAPRAID_CONTENT_FILES export SNAPRAID_DATA_DISKS export SNAPRAID_PARITY_FILES export MOUNT_DEPENDENCIES_START export MOUNT_DEPENDENCIES_STOP fi envsubst < templates/nix-config/disks/snapraid.nix > final-nix-config/etc/nixos/disks/snapraid.nix # Close the disko.nix block cat <<'EOF' >> final-nix-config/etc/nixos/disks/disko.nix }; }; } EOF echo -e "\n ✅ Final disko configuration created." SPINDOWN_DISKS_ID=() if [[ "${#DATA_DISKS_ID[@]}" -gt 0 ]]; 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 [[ "${#SPINDOWN_DISKS_ID[@]}" -gt 0 ]]; then cp -${FILES_COPY_FLAGS} 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 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 ### 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 -${FILES_COPY_FLAGS} templates/nix-config/flake.nix final-nix-config/etc/nixos/ cp -${FILES_COPY_FLAGS} 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" && ${TARGET_TPM_VERSION} -eq 2 ]]; then sed -i "s|# ./disks/pcr-check.nix| ./disks/pcr-check.nix|" final-nix-config/etc/nixos/configuration.nix sed -i "s|# boot.initrd.systemd.tpm2.enable = true;| boot.initrd.systemd.tpm2.enable = true;|" final-nix-config/etc/nixos/configuration.nix sed -i "s|# systemIdentity.enable = true;| systemIdentity.enable = true;|" final-nix-config/etc/nixos/configuration.nix cp -${FILES_COPY_FLAGS} templates/nix-config/disks/pcr-check.nix final-nix-config/etc/nixos/disks/ 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..." SELECTED_SERVICES_DNS=() 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 -${FILES_COPY_FLAGS} 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 echo "export SERVICES_NETWORK_IDS=\"(${SERVICES_NETWORK_IDS[@]})\"" >> $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" TARGET_HOST="${HOME_SERVER_IP}" 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 if [[ "${TARGET_TPM}" == "true" && ${TARGET_TPM_VERSION} -eq 2 ]]; then gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 " A TPM version 2 has been detected on the system. You can choose to enable automatic disk decryption on boot. Enabling automatic disk decryption on boot means that you won't have to enter your disk password everytime you start your server. This comes in very handy if you don't plan to leave your server accessible with a keyboard or if you don't have an IP KVM. Note : This feature is currently vulnerable to on-site attacks. This means that an attacker with physical access to your machine could steal the password from the TPM, and therefore have access to all your date. Do you want to enable automatic disk decryption on boot ?" if gum confirm "➡️ I understand, 'yes' to proceed."; then ssh_to_host 'bash -s' << EOF echo "Enrolling boot disk key to TPM..." BOOT_DISKS_ID=(${BOOT_DISKS_ID[@]}) DISK_PATH="" j=1 for i in \${!BOOT_DISKS_ID[@]}; do if [[ echo "\${BOOT_DISKS_ID[\${i}]}" | grep -iq "nvme" ]]; then DISK_PATH="/dev/\${BOOT_DISKS_ID[\${i}]}p2" else DISK_PATH="/dev/\${BOOT_DISKS_ID[\${i}]}2" fi echo ${REMOTE_PASS} | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 --unlock-key-file=/etc/secrets/disks/boot-\${j} \${DISK_PATH} j=\$((j + 1)) done echo "Getting PCRS 15 hash..." PCR_HASH=\$(echo ${REMOTE_PASS} | sudo -S systemd-analyze pcrs 15 --json=short) echo ${REMOTE_PASS} | sudo -S sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix EOF else echo "Skipping TPM configuration." fi else echo "No supported TPM detected (TPM version 2 required). Skipping TPM configuration." fi 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 (locally with Passbolt with 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 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 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