#!/usr/bin/env nix-shell #!nix-shell -i bash -p bash nano coreutils gnused gum fastfetch xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto curl jq yq python3 # --- UTILITY FUNCTIONS ---> echod() { MESSAGE=${1} if [[ ${DEBUG} -eq 1 ]]; then echo -e ${MESSAGE} fi } ssh_to_host() { local COMMAND="${1}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}" } get_valid_input() { local VAR_NAME="${1}" local HEADER="${2}" local PLACEHOLDER="${3}" local REGEX="${4}" local MANDATORY="${5:-true}" local SENSITIVE="${6:-false}" if [[ "${MANDATORY}" == "true" ]]; then local PROMPT="(Required) > " elif [[ "${MANDATORY}" == "false" ]]; then local PROMPT="(Optional) > " fi while true; do local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}") # Handle empty input if [[ -z "${INPUT}" ]]; then if [[ "${MANDATORY}" == true ]]; then gum style --foreground "#ff0000" -- " āŒ This field is mandatory." continue else INPUT="" break fi fi # Handle Regex Validation if [[ -n "${REGEX}" ]]; then if [[ "${INPUT}" =~ ${REGEX} ]]; then export "${VAR_NAME}"="${INPUT}" break else gum style --foreground "#ff0000" -- " āŒ Invalid format. Please try again." fi else export "${VAR_NAME}"="${INPUT}" break fi done } # --- UTILITY FUNCTIONS ---< # --- GLOBAL FUNCTIONS ---> cleanup() { echo -e "\n āœ… Cleaning up..." rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/ if [[ -n "${BRIDGE_PID:-}" ]] && ps -p ${BRIDGE_PID} > /dev/null; then kill ${BRIDGE_PID} fi } hierarchy_preparation() { echod "\n šŸ”„ Preparing the folder hierarchy for the final configuration..." # Script folders mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp [[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web # Secrets mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/ mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman fi echod "\n āœ… Folder hierarchy ready" } hardware_detection() { local TMPFILE="/tmp/nixos-installation-hw-detection" ssh_to_host 'bash -s' << SSHEND TARGET_GRAPHICS_BRAND=() 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 [[ "\$TRANSPORT_PROTOCOL" == "usb" ]]; then DISK_TYPE+=("USB"); elif [[ "\$HDD" -eq 1 ]]; then DISK_TYPE+=("HDD"); elif [[ "\$HDD" -eq 0 ]]; then DISK_TYPE+=("SSD"); else DISK_TYPE+=("Other") fi # Disk health if [[ \$(echo "${LIVE_TARGET_PASSWORD}" | 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_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 \ TARGET_GRAPHICS_BRAND \ DISK_DEVPATH \ DISK_NAME \ DISK_TYPE \ DISK_HEALTH \ DISK_ID \ DISK_SIZE; do declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}" done SSHEND scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null source "${TMPFILE}" local DISK_FLAT_ARRAY=() for i in "${!DISK_NAME[@]}"; do DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}") done jq -n \ --argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \ --argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \ --argjson tpu_usb "${TARGET_USB_CORAL:-false}" \ --argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \ --argjson tpm_enabled "${TARGET_TPM:-false}" \ --arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \ --arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \ --arg interface "${TARGET_INTERFACE:-}" \ --argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \ ' { graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer }, tpu: { usb: $tpu_usb, pcie: $tpu_pcie }, tpm: { enabled: $tpm_enabled, version: $tpm_version }, zigbee: { device: $zigbee_device }, network: { interface: $interface }, disks: [ $ARGS.positional | range(0; length; 6) as $i | { name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5] } ] }' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH} if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then echo -e "\nāœ… Hardware configuration generated" else echo -e "\nāŒ Failed to generate hardware configuration" exit 1 fi } # --- GLOBAL FUNCTIONS ---< # --- MAIN WEB FUNCTIONS ---> launch_gui() { echo -e "\n āž”ļø You will now proceed to the configuration of your device through your browser" echo -e "\n šŸš€ Launching Numbus Configurator..." python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 & export BRIDGE_PID=$! xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true sleep 5 echo -e "\n āž”ļø If it doesn't automatically, open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")" } # --- MAIN WEB FUNCTIONS ---< # --- MAIN TUI FUNCTIONS ---> setup_ssh() { echod "\n āœ… Generating new SSH key for numbus-admin..." chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/ ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q if [[ ${DEBUG} -eq 1 ]]; then echo -e "\n āž”ļø Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..." fi if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then if [[ ${DEBUG} -eq 1 ]]; then echo -e "\n āœ… SSH key copied successfully" fi else echo -e "\n āŒ Failed to copy SSH key. Please check the host IP and password." exit 1 fi } server_config_generation() { echod "\n šŸ“ Generating structured settings.json..." # Create a temporary JSON file with all the collected variables # This file will be read by the Nix configuration using builtins.fromJSON jq -n \ --arg tz "$INTERNATIONALIZATION_TIMEZONE" \ --arg lang "$INTERNATIONALIZATION_LANGUAGE" \ --arg owner "$SERVER_OWNER_NAME" \ --arg ip "$HOME_SERVER_IP" \ --arg iface "$TARGET_INTERFACE" \ --arg router "$NETWORK_ROUTER_IP" \ --arg domain "$DOMAIN_NAME" \ --argjson cockpit_enabled "true" \ --arg dns "${SELECTED_DNS_SERVICE[0]}" \ --argjson apps "$(printf '%s\n' "${SELECTED_WEB_APPLICATIONS[@]}" | jq -R . | jq -s .)" \ '{ system: { timeZone: $tz, language: $lang, owner: $owner }, network: { ipAddress: $ip, interface: $iface, routerIp: $router }, services: { domain: $domain, dnsProvider: $dns, enabledApps: $apps, managementConsole: $cockpit_enabled } }' > "${EXTRA_FILES_PATH}/etc/nixos/settings.json" echo -e "{\n numbus.settings = builtins.fromJSON (builtins.readFile ./settings.json);\n}" > "${CONFIGURATION_PATH}" # Ensure the settings file is writable by the management service # and that the directory is prepared for local git tracking chmod 664 "${EXTRA_FILES_PATH}/etc/nixos/settings.json" } # The existing network_config_generation and services_config_generation functions # are now redundant as the logic is centralized in the JSON export. mail_config_generation() { echo -e "\n # Mail settings" >> ${CONFIGURATION_PATH} echo -e " numbus.mail.enable = true;" >> ${CONFIGURATION_PATH} echo -e " numbus.mail.userAddress = \"${SERVER_USER_EMAIL}\";" >> ${CONFIGURATION_PATH} echo -e " numbus.mail.adminAddress = \"${SERVER_ADMIN_EMAIL}\";" >> ${CONFIGURATION_PATH} echo -e " numbus.mail.smtpUsername = \"${SMTP_SERVER_USERNAME}\";" >> ${CONFIGURATION_PATH} echo -e " numbus.mail.smtpPasswordPath = config.sops.secrets.smtpPassword.path;" >> ${CONFIGURATION_PATH} if [[ "${SMTP_SERVER_HOST}" != "smtp.gmail.com" ]]; then echo -e " numbus.mail.smtpServer = \"${SMTP_SERVER_HOST}\";" >> ${CONFIGURATION_PATH} fi if [[ "${SMTP_SERVER_PORT}" != "587" ]]; then echo -e " numbus.mail.smtpPort = ${SMTP_SERVER_PORT};" >> ${CONFIGURATION_PATH} fi } disk_config_generation() { echo -e "\n # Hardware settings" >> ${CONFIGURATION_PATH} if [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then echo " numbus.hardware.pcie-coral.enable = true;" >> ${CONFIGURATION_PATH} fi echo -e " numbus.hardware.bootDisksList = [ ${BOOT_DISKS_ID_LIST[@]} ];" >> ${CONFIGURATION_PATH} echo -e " numbus.hardware.dataDisksList = [ ${CONTENT_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH} echo -e " numbus.hardware.parityDisksList = [ ${PARITY_DISK_LIST[@]} ];" >> ${CONFIGURATION_PATH} echo -e " numbus.hardware.spindownDisksList = [ ${SPINDOWN_DISKS_LIST[@]} ];" >> ${CONFIGURATION_PATH} echo "}" >> ${CONFIGURATION_PATH} } keys_generation() { for i in $(seq 1 "${#BOOT_DISKS_ID_LIST[@]}"); do PASS="$(xkcdpass)" echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/ echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}" echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/boot-${i} EOF done for i in $(seq 1 "$CONTENT_DISK_NUMBER"); do PASS="$(xkcdpass)" echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}" echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/content-${i} EOF done for i in $(seq 1 "$PARITY_DISK_NUMBER"); do PASS="$(xkcdpass)" echo -n "$PASS" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}" ssh_to_host 'bash -s' << EOF echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}" echo "$LIVE_TARGET_PASSWORD" | sudo -S chmod 600 /etc/secrets/disks/parity-${i} EOF done local SSH_KEYS_FORMATTED="" if [[ "$(declare -p AUTHORIZED_SSH_PUBLIC_KEY 2>/dev/null)" =~ "declare -a" ]]; then for key in "${AUTHORIZED_SSH_PUBLIC_KEY[@]}"; do SSH_KEYS_FORMATTED+=" $key"$'\n' done else SSH_KEYS_FORMATTED=" $AUTHORIZED_SSH_PUBLIC_KEY"$'\n' fi export SSH_KEYS_FORMATTED echo -e "\n āœ… Generating sops-nix keys..." ssh-to-age -private-key -i ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt) echo -e "\n āœ… Generating sops-nix configuration files..." envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/etc/nixos/secrets/secrets.yaml } cloudflare_dns_setup() { gum confirm "āž”ļø This script can automatically create DNS records for your services. Proceed? (recommended)" || { echo -e "\n\n āš ļø skipping the DNS records creation step..."; return 0; } 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 ${CLOUDFLARE_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 ${CLOUDFLARE_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 ${CLOUDFLARE_DNS_API_TOKEN}" \ -H "Content-Type: application/json" > /dev/null 2>&1 done create_records "${SUBDOMAIN}" } echo -e "\n\n ā˜ļø Configuring Cloudflare DNS records..." i=0 for service in "${SELECTED_WEB_APPLICATIONS[@]}"; do if [[ -n "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]:-}" ]]; then SELECTED_SERVICES_DNS+=( "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}.${DOMAIN_NAME}" ) else SELECTED_SERVICES_DNS+=( "${service}.${DOMAIN_NAME}" ) fi i=$((i + 1)) [[ "${service}" == "nextcloud" ]] && SELECTED_SERVICES_DNS+=( "onlyoffice.${DOMAIN_NAME}" "whiteboard.${DOMAIN_NAME}" ) done if [[ -n "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]:-}" ]]; then SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}.${DOMAIN_NAME}" ) else SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE}.${DOMAIN_NAME}" ) fi # Get Zone ID ZONE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${DOMAIN_NAME}" \ -H "Authorization: Bearer ${CLOUDFLARE_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 ${CLOUDFLARE_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 } deploy() { git -C . add -f "${EXTRA_FILES_PATH}/" git -C . add -f "templates/" git -C . add -f "deploy.conf" # Initialize a git repo in the configuration to be deployed # This allows the Management UI on the appliance to commit changes # and provide a local history/rollback UI to the user. if [ ! -d "${EXTRA_FILES_PATH}/etc/nixos/.git" ]; then git -C "${EXTRA_FILES_PATH}/etc/nixos" init -q git -C "${EXTRA_FILES_PATH}/etc/nixos" add . git -C "${EXTRA_FILES_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q fi echo -e "\n\nšŸ”„ Deploying to the remote server..." nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos nix run github:nix-community/nixos-anywhere -- \ --flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \ --extra-files ${EXTRA_FILES_PATH} \ --chown "/home/numbus-admin/" 1000:1000 \ --target-host ${TARGET_USER}@${LIVE_TARGET_IP} echo -e "\n\nāœ… Installation successfull !" sleep 1 } postrun_action() { TARGET_USER="numbus-admin" LIVE_TARGET_IP="${HOME_SERVER_IP}" LIVE_TARGET_PASSWORD="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=$((i + 1)) 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 sshpass -p "${LIVE_TARGET_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" 'bash -s' << EOF echo "Enrolling boot disk key to TPM..." BOOT_DISKS_NAME=(${BOOT_DISKS_NAME[@]}) DEBUG=${DEBUG} DISK_PATH="" j=1 for i in \${!BOOT_DISKS_NAME[@]}; do if echo "\${BOOT_DISKS_NAME[\${i}]}" | grep -iq "nvme"; then [[ "\${DEBUG}" == "true" ]] && echo "NVMe detected..." DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}p2" else [[ "\${DEBUG}" == "true" ]] && echo "Non-NVMe drive detected..." DISK_PATH="/dev/\${BOOT_DISKS_NAME[\${i}]}2" fi [[ "\${DEBUG}" == "true" ]] && echo "Issuing enroll command for disk \${DISK_PATH}..." echo ${LIVE_TARGET_PASSWORD} | 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 ${LIVE_TARGET_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short) echo ${LIVE_TARGET_PASSWORD} | 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 $LIVE_TARGET_PASSWORD | sudo -S passwd numbus-admin } # --- MAIN FUNCTIONS ---< # --- DEFAULT VARIABLES ---> WEBSERVER_PORT=${WEBSERVER_PORT:-8088} LIVE_DATA_FILE="../config/live.yaml" HW_DATA_FILE="../config/hardware.yaml" CONFIG_FILE="../config/numbus.yaml" TARGET_USER="nixos" TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")" EXTRA_FILES_PATH="${TMP_FILES_PATH}/config" if [[ ${DEBUG-0} -eq 1 ]]; then FILES_CP_FLAGS="vau" FILES_RM_FLAGS="vf" DIR_RM_FLAGS="rvf" MKDIR_FLAGS="pv" MV_FLAGS="vu" else DEBUG=0 FILES_CP_FLAGS="au" FILES_RM_FLAGS="f" DIR_RM_FLAGS="rf" MKDIR_FLAGS="p" MV_FLAGS="u" fi IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$' SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$' DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$' EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' PORT_REGEX='^[0-9]{1,5}$' SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*' PHONE_REGEX='^\+[1-9][0-9]{7,14}$' GUM_INPUT_PADDING="1 1" GUM_INPUT_HEADER_FOREGROUND="212" GUM_INPUT_CURSOR_FOREGROUND="212" GUM_INPUT_TIMEOUT="3600" # --- DEFAULTS VARIABLES ---< # --- PRE MAIN LOGIC ---> set -euo pipefail clear trap cleanup EXIT # --- PRE MAIN LOGIC ---< # --- MAIN LOGIC ---> echo """ _ ____ ____ ______ __ ______ / |/ / / / / |/ / _ )/ / / / __/ / / /_/ / /|_/ / _ / /_/ /\ \ /_/|_/\____/_/ /_/____/\____/___/ """ DEPLOYMENT_STRATEGY=$(gum choose --header "Choose your preferred deployment strategy :" \ "I don't have a configuration" \ "I have a valid configuration hosted on a Git platform") if [[ "${DEPLOYMENT_STRATEGY}" == "I don't have a configuration" ]]; then BRIDGE_SCRIPT="../web/logic/interactive.py" launch_gui hierarchy_preparation until [[ -e ../web/signals/hw_detection_ready ]]; do sleep 5 done LIVE_TARGET_IP="$(yq -r '.live_target_ip' ${LIVE_DATA_FILE})" LIVE_TARGET_PASSWORD="$(yq -r '.live_target_password' ${LIVE_DATA_FILE})" until [[ -e ../web/signals/configuration_ready ]]; do done until [[ -e ../web/signals/deployment_ready ]]; do done else BRIDGE_SCRIPT="../web/logic/non-interactive.py" launch_gui hierarchy_preparation until [[ -e ../web/signals/hw_detection_ready ]]; do done until [[ -e ../web/signals/hw_detection_ready ]]; do done until [[ -e ../web/signals/hw_detection_ready ]]; do done fi # --- MAIN LOGIC ---<