Files
Numbus/deploy.sh
T

822 lines
35 KiB
Bash

#!/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
launch_configurator() {
local PORT=8088
local CONFIG_FILE="numbus.yaml"
local BRIDGE_SCRIPT="configurator/bridge.py"
# Create a more robust Python Bridge
cat << EOF > "${BRIDGE_SCRIPT}"
import http.server
import json
import os
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
if self.path == '/logs':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
if os.path.exists('deploy.log'):
with open('deploy.log', 'r') as f:
lines = f.readlines()
self.wfile.write("".join(lines[-20:]).encode())
return
return http.server.SimpleHTTPRequestHandler.do_GET(self)
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
if self.path == '/discovery':
with open("live_settings.json", "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
# Signal Bash that discovery data is ready
with open(".discovery_ready", "w") as f: f.write("1")
elif self.path == '/deploy':
with open("${CONFIG_FILE}", "wb") as f:
f.write(post_data)
self.send_response(200)
self.end_headers()
with open(".deploy_signal", "w") as f: f.write("1")
os.chdir("configurator")
http.server.HTTPServer(('localhost', ${PORT}), BridgeHandler).serve_forever()
EOF
# Cleanup old signals
rm -f configurator/.discovery_ready configurator/.deploy_signal configurator/live_settings.json configurator/hardware.json
echo -e "🚀 Launching Numbus Configurator..."
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
BRIDGE_PID=$!
echo -e "➡️ Open your browser at: $(gum style --foreground 212 "http://localhost:${PORT}")"
xdg-open "http://localhost:${PORT}" 2>/dev/null || open "http://localhost:${PORT}" 2>/dev/null || true
}
hierarchy_preparation() {
if [[ -e final-nix-config/etc/nixos/configuration.nix || -e final-nix-config/home/numbus-admin/.ssh/id_ed25519 ]]; then
gum confirm "It seems you have already run this script. Previously generated files need to be cleaned up. Proceed?" || { echo -e "\n\n❌ Aborting."; exit 1; }
rm -rf final-nix-config/etc/nixos/*
rm -f final-nix-config/var/lib/sops-nix/*
rm -f final-nix-config/etc/secrets/disks/*
rm -f final-nix-config/var/lib/numbus-server/*
rm -f final-nix-config/home/numbus-admin/.ssh/*
fi
mkdir -p final-nix-config/etc/
mkdir -p final-nix-config/etc/nixos/
mkdir -p final-nix-config/etc/secrets/
mkdir -p final-nix-config/etc/secrets/disks/
mkdir -p final-nix-config/etc/nixos/secrets/
mkdir -p final-nix-config/var/
mkdir -p final-nix-config/var/lib/
mkdir -p final-nix-config/var/lib/sops-nix/
mkdir -p final-nix-config/var/lib/numbus-server/
echo -e "\n✅ Writing configuration..."
cp -${FILES_COPY_FLAGS} templates/nix-config/configuration.nix final-nix-config/etc/nixos/configuration.nix
cp -${FILES_COPY_FLAGS} templates/nix-config/flake.nix final-nix-config/etc/nixos/flake.nix
export CONFIGURATION_PATH="final-nix-config/etc/nixos/configuration.nix"
}
setup_ssh() {
mkdir -p final-nix-config/
mkdir -p final-nix-config/home/
mkdir -p final-nix-config/home/numbus-admin/
mkdir -p final-nix-config/home/numbus-admin/.ssh/
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}@${LIVE_TARGET_IP}'..."
if sshpass -p "${LIVE_TARGET_PASSWD}" ssh-copy-id -o StrictHostKeyChecking=no -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; 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}@${LIVE_TARGET_IP}" "${COMMAND}"
}
hardware_detection() {
### --> Get hardware information
local TMPFILE="/tmp/hw_detection.json"
ssh_to_host 'bash -s' << SSHEND
TARGET_GRAPHICS="false"
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}")
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 "$LIVE_TARGET_PASSWD" | 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
# Build organized JSON output for yq
cat << EOF > "\${TMPFILE}"
{
"graphics": {
"enabled": \${TARGET_GRAPHICS},
"brands": [ \$(printf '"%s",' "\${TARGET_GRAPHICS_BRAND[@]}" | sed 's/,\$//') ],
"renderer": \${TARGET_GRAPHICS_RENDERER}
},
"tpu": {
"usb": \${TARGET_USB_CORAL},
"pcie": \${TARGET_PCIE_CORAL}
},
"tpm": {
"enabled": \${TARGET_TPM},
"version": "\${TARGET_TPM_VERSION}"
},
"zigbee": {
"device": "\${TARGET_ZIGBEE_DEVICE}"
},
"network": {
"interface": "\${TARGET_INTERFACE}"
},
"disks": [
\$(
count=\${#DISK_NAME[@]}
for i in "\${!DISK_NAME[@]}"; do
echo " {\"name\": \"\${DISK_NAME[\$i]}\", \"path\": \"\${DISK_DEVPATH[\$i]}\", \"type\": \"\${DISK_TYPE[\$i]}\", \"health\": \"\${DISK_HEALTH[\$i]}\", \"id\": \"\${DISK_ID[\$i]}\", \"size\": \"\${DISK_SIZE[\$i]}\"}\$( [[ \$i -lt \$((count-1)) ]] && echo ',' )"
done
)
]
}
EOF
SSHEND
### Get hardware information <--
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "hardware.json" &> /dev/null
# Create YAML for NixOS and JSON for the Configurator Website
yq -P '.' hardware.json > hardware.yaml
yq -o=json '.' hardware.yaml > configurator/hardware.json
rm hardware.json
### --> 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() {
services_choice() {
local SERVICES_LIST=( "${1[@]}" )
local SERVICES_DESCRIPTION=( "${2[@]}" )
local FINAL_VARIABLE="${3}"
local HEADER="${4}"
local LIMIT="${5:---no-limit}"
local SELECTED_SERVICES=()
local SELECTED_SERVICES_DESCRIPTION=()
local SELECTED_SERVICES_DESCRIPTION=$(gum choose ${LIMIT} --header "${HEADER}" "${SERVICES_DESCRIPTION[@]}")
for i in ${!SERVICES_LIST[@]}; do
if printf '%s' "${SELECTED_SERVICES_DESCRIPTION}" | grep -iq "${SERVICES_LIST[${i}]}"; then
SELECTED_SERVICES+=("${SERVICES_LIST[${i}]}")
fi
done
export "${FINAL_VARIABLE}=(${SELECTED_SERVICES[@]})"
}
echo -e "\n\n ➡️ You will now select the services you want installed on your server:"
services_choice "${DNS_SERVICES_LIST[@]}" "${DNS_SERVICES_DESCRIPTION[@]}" "SELECTED_DNS_SERVICE" "Choose your preferred DNS service :" "--limit=1"
services_choice "${WEB_APPLICATIONS_LIST[@]}" "${WEB_APPLICATIONS_DESCRIPTION[@]}" "SELECTED_WEB_APPLICATIONS" "Choose the web applications you want to install :"
services_choice "${SYSTEM_SERVICES_LIST[@]}" "${SYSTEM_SERVICES_DESCRIPTION[@]}" "SELECTED_SYSTEM_SERVICES" "Choose the system services you want to install :"
gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\n✅ Continuing..."; return 0; }
for service in ${SELECTED_WEB_APPLICATIONS[@]} ${SELECTED_DNS_SERVICE[@]}; do
SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" )
done
export SELECTED_WEB_APPLICATIONS_SUBDOMAIN
}
disks_selection() {
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)"
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_LIST+=("\"${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}\"")
BOOT_DISKS_NAME+=("${DISK_NAME[${i}]}")
unset "GUM_PRINTED_ELEMENTS[${i}]"
fi
done
echo ""
gum style --foreground 212 "➡️ Please choose data and parity disks (up to 9 total) :"
local SELECTED_DATA_DISK=$(gum choose --limit 9 --header "$HEADER" "${GUM_PRINTED_ELEMENTS[@]}")
for i in ${!DISK_NAME[@]}; do
if printf '%s' "$SELECTED_DATA_DISK" | grep -iq "${DISK_NAME[${i}]}"; then
DATA_DISKS_ID+=("${DISK_ID[${i}]:-${DISK_DEVPATH[${i}]}}")
DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}")
fi
done
if [[ "${#DATA_DISKS_ID[@]}" -eq 1 ]]; then
export PARITY_DISK_NUMBER=0
export CONTENT_DISK_NUMBER=1
export PARITY_DISK_LIST=()
export CONTENT_DISK_LIST=("\"${DATA_DISKS_ID[0]}\"")
else
export PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3))
export CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER))
for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do
CONTENT_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
done
for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do
PARITY_DISK_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
done
fi
if [[ "${#DATA_DISKS_ID[@]}" -gt 0 ]]; then
for i in ${!DATA_DISKS_ID[@]}; do
if [[ "${DATA_DISKS_TYPE[${i}]}" == "HDD" ]]; then
SPINDOWN_DISKS_LIST+=("\"${DATA_DISKS_ID[${i}]}\"")
fi
done
fi
export SPINDOWN_DISKS_LIST
export BOOT_DISKS_ID_LIST
export PARITY_DISK_LIST
export CONTENT_DISK_LIST
}
server_config_generation() {
echo -e "\n # Server settings" >> ${CONFIGURATION_PATH}
echo -e " time.timeZone = \"${TIMEZONE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.locale = \"${LOCALE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.language = \"${LANGUAGE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.owner = \"${SERVER_OWNER_NAME}\";" >> ${CONFIGURATION_PATH}
}
network_config_generation() {
echo -e "\n # Network settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.ipAddress = \"${HOME_SERVER_IP}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.interface = \"${TARGET_INTERFACE}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.networking.routerIpAddress = \"${NETWORK_ROUTER_IP}\";" >> ${CONFIGURATION_PATH}
}
services_config_generation() {
echo -e "\n # DNS settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.dns = \"${SELECTED_DNS_SERVICE[0]}\";" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.${SELECTED_DNS_SERVICE[0]}.enable = true;" >> ${CONFIGURATION_PATH}
echo -e "\n # Services settings" >> ${CONFIGURATION_PATH}
echo -e " numbus.services.domain = \"${DOMAIN_NAME}\";" >> ${CONFIGURATION_PATH}
i=0
for service in "${SELECTED_WEB_APPLICATIONS[@]}"; do
if [[ -v SELECTED_WEB_APPLICATIONS_SUBDOMAIN && -n "${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}" ]]; then
echo -e " numbus.services.${service}.subdomain = \"${SELECTED_WEB_APPLICATIONS_SUBDOMAIN[${i}]}\";" >> ${CONFIGURATION_PATH}
fi
echo -e " numbus.services.${service}.enable = true;" >> ${CONFIGURATION_PATH}
i=$((i + 1))
done
if [[ -v SELECTED_DNS_SERVICE_SUBDOMAIN && -n "${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}" ]]; then
echo -e " numbus.services.${SELECTED_DNS_SERVICE[0]}.subdomain = \"${SELECTED_DNS_SERVICE_SUBDOMAIN[0]}\";" >> ${CONFIGURATION_PATH}
fi
if [[ "${TARGET_GRAPHICS_RENDERER}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/dri/D128\""
fi
if [[ "${TARGET_USB_CORAL}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/bus/usb\""
elif [[ "${TARGET_PCIE_CORAL}" == "true" ]]; then
FRIGATE_DEVICES+=" \"/dev/apex_0\""
fi
if [[ -n "${TARGET_ZIGBEE_DEVICE}" ]]; then
HOME_ASSISTANT_DEVICES+=" \"${TARGET_ZIGBEE_DEVICE}\""
fi
if [[ -n "${FRIGATE_DEVICES:-}" ]]; then
echo -e " numbus.services.frigate.devices = [${FRIGATE_DEVICES} ];" >> ${CONFIGURATION_PATH}
fi
if [[ -n "${HOME_ASSISTANT_DEVICES:-}" ]]; then
echo -e " numbus.services.home-assistant.devices = [${HOME_ASSISTANT_DEVICES} ];" >> ${CONFIGURATION_PATH}
fi
}
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" > "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 "$LIVE_TARGET_PASSWD" | sudo -S mkdir -p /etc/secrets/disks/
echo "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}"
echo "$LIVE_TARGET_PASSWD" | 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" > "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 "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}"
echo "$LIVE_TARGET_PASSWD" | 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" > "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 "$LIVE_TARGET_PASSWD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}"
echo "$LIVE_TARGET_PASSWD" | 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 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
}
sum_up() {
DISK_RECAP_CONTENT=$(cat << EOF
### Disk Configuration Summary
Please review the selected disk layout before proceeding.
**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :**
* **Boot 1:** \`${BOOT_DISKS_ID_LIST[0]}\`
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID_LIST[1]}\`" )
**Data Disks ($CONTENT_DISK_NUMBER) :**
$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && 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; }
SERVICES_RECAP_CONTENT=$(cat << EOF
### Services Configuration Summary
Please review the selected services before proceeding.
**DNS Service (${#SELECTED_DNS_SERVICE[@]}) :**
$(echo "* \`${SELECTED_DNS_SERVICE[0]^}\`")
**Web Applications (${#SELECTED_WEB_APPLICATIONS[@]}) :**
$(for app in "${SELECTED_WEB_APPLICATIONS[@]}"; do echo "* \`${app^}\`"; done)
**System Services (${#SELECTED_SYSTEM_SERVICES[@]}) :**
$(for service in "${SELECTED_SYSTEM_SERVICES[@]}"; do echo "* \`${service^}\`"; done)
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${SERVICES_RECAP_CONTENT}")"
gum confirm "➡️ Proceed with this services configuration?" || { echo -e "\n\n❌ Aborting as requested."; exit 1; }
DISK_RECAP_CONTENT=$(cat << EOF
### Secrets Summary
Please save the following secrets to a secure place (i.e. your local password manager, or a hidden sheet of paper).
**Boot Disks (${#BOOT_DISKS_ID_LIST[@]}) :**
* **Disk 1 Secret Key :** \`$( cat final-nix-config/etc/secrets/disks/boot-1 )\`
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat final-nix-config/etc/secrets/disks/boot-2 )\`" )
**Data Disks ($CONTENT_DISK_NUMBER):**
$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
$( [[ $CONTENT_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat final-nix-config/etc/secrets/disks/content-${j} )\`" && j=$((j + 1)); done )
**Parity Disks ($PARITY_DISK_NUMBER):**
$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*" )
$( [[ $PARITY_DISK_NUMBER -gt 0 ]] && j=1 && for i in $(seq $CONTENT_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Disk ${j} Secret Key :** \`$( cat final-nix-config/etc/secrets/disks/parity-${j} )\`" && j=$((j + 1)); done )
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "${DISK_RECAP_CONTENT}")"
gum confirm "✅ I have stored these credentials in a safe place" || { echo -e "\n\n❌ Please store these credentials in a safe place as you will need them later."; exit 1; }
gum confirm "➡️ Would you like to manually edit the configuration (⚠️ advanced users only)" || { echo -e "\n\n✅ continuing with the installation..."; return 0; }
nano final-nix-config/etc/nixos/configuration.nix
}
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
}
export_configuration() {
cp deploy.conf final-nix-config/var/lib/numbus-server/numbus-server.conf
local CONFIG_EXPORT_DIR="final-nix-config/var/lib/numbus-server/"
local CONFIG_EXPORT_FILE="${CONFIG_EXPORT_DIR}/numbus-server.conf"
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_LIST=\"(${BOOT_DISKS_ID_LIST[@]})\"" >> $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_LIST=\"(${SPINDOWN_DISKS_LIST[@]})\"" >> $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
}
deploy() {
git -C . add -f "final-nix-config/"
git -C . add -f "templates/"
git -C . add -f "deploy.conf"
echo -e "\n\n🔄 Deploying to the remote server..."
nix flake update --flake ./final-nix-config/etc/nixos
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}@${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_PASSWD="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_PASSWD}" ssh -i "final-nix-config/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_PASSWD} | 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_PASSWD} | sudo -S systemd-analyze pcrs 15 --json=short)
echo ${LIVE_TARGET_PASSWD} | 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_PASSWD | sudo -S passwd numbus-admin
}
nix_update() {
echo -e "\n\n🔄 Updating NixOS on the remote server..."
nixos-rebuild --target-host numbus-admin@${LIVE_TARGET_IP} \
--use-remote-sudo switch --flake final-nix-config/etc/nixos#numbus-server
}
set -euo pipefail
clear
gum style --align center --width 80 --foreground 212 "
██████ █████ █████
▒▒██████ ▒▒███ ▒▒███
▒███▒███ ▒███ █████ ████ █████████████ ▒███████ █████ ████ █████
▒███▒▒███▒███ ▒▒███ ▒▒███ ▒▒███▒▒███▒▒███ ▒███▒▒███▒▒███ ▒▒███ ███▒▒
▒███ ▒▒██████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒█████
▒███ ▒▒█████ ▒███ ▒▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒███ ▒▒▒▒███
█████ ▒▒█████ ▒▒████████ █████▒███ █████ ████████ ▒▒████████ ██████
▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒
█████████
███▒▒▒▒▒███
▒███ ▒▒▒ ██████ ████████ █████ █████ ██████ ████████
▒▒█████████ ███▒▒███▒▒███▒▒███▒▒███ ▒▒███ ███▒▒███▒▒███▒▒███
▒▒▒▒▒▒▒▒███▒███████ ▒███ ▒▒▒ ▒███ ▒███ ▒███████ ▒███ ▒▒▒
███ ▒███▒███▒▒▒ ▒███ ▒▒███ ███ ▒███▒▒▒ ▒███
▒▒█████████ ▒▒██████ █████ ▒▒█████ ▒▒██████ █████
▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒
"
sleep 1
launch_configurator