Files
numbus-server/deploy.sh
T

699 lines
32 KiB
Bash

#!/usr/bin/env nix-shell
#!nix-shell -i bash -p gum xkcdpass sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
NECESSARY_VARIABLES_LIST=("TARGET_HOST" "SSH_PUBLIC_KEY" "DOMAIN_NAME" "EMAIL_ADDRESS" \
"CF_DNS_API_TOKEN" "SENDER_EMAIL_ADDRESS" "SENDER_EMAIL_ADDRESS_PASSWORD" \
"SENDER_EMAIL_DOMAIN" "SENDER_EMAIL_PORT" "HOME_ROUTER_SUBNET" "HOME_ROUTER_IP" \
"HOME_SERVER_IP")
INSTALLED_REMOTE_PASS="changeMe!"
necessary_credentials() {
#TARGET SETTINGS
echo -e "\n\n ➡️ Please provide the IP address of the target host :"
export TARGET_HOST="$(gum input --placeholder "192.168.1.100")"
echo -e "\n\n ➡️ Please provide the public SSH key of an authorized device :"
export SSH_PUBLIC_KEY="$(gum input --placeholder "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc")"
# TRAEFIK SETTINGS
echo -e "\n\n ➡️ Please provide the domain name (FQDN) your home server will use :"
export DOMAIN_NAME="$(gum input --placeholder "yourdomain.com")"
echo -e "\n\n ➡️ Please provide a valid email address (will be used for ACME, and your services) :"
export EMAIL_ADDRESS="$(gum input --placeholder "myemail@gmail.com")"
echo -e "\n\n ➡️ Please provide a cloudflare API token with DNS zone permission :"
export CF_DNS_API_TOKEN="$(gum input --placeholder "bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE")"
# SMTP SETTINGS
echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails.\n Please provide a valid sender email address :"
export SENDER_EMAIL_ADDRESS="$(gum input --placeholder "myemail@gmail.com")"
echo -e "\n\n ➡️ Please provide the password of this email address :"
export SENDER_EMAIL_ADDRESS_PASSWORD="$(gum input --placeholder "abcd efgh ijkl mnop")"
echo -e "\n\n ➡️ Please provide the SMTP server endpoint :"
export SENDER_EMAIL_DOMAIN="$(gum input --placeholder "smtp.gmail.com")"
echo -e "\n\n ➡️ Please provide the smtp TLS port (for gmail : 587) :"
export SENDER_EMAIL_PORT="$(gum input --placeholder "587")"
# NETWORK SETTINGS
echo -e "\n\n ➡️ Please provide your home network subnet :"
export HOME_ROUTER_SUBNET="$(gum input --placeholder "192.168.1.1/24")"
echo -e "\n\n ➡️ Please provide the ip address of your router :"
export HOME_ROUTER_IP="$(gum input --placeholder "192.168.1.1")"
echo -e "\n\n ➡️ 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.) :"
export HOME_SERVER_IP="$(gum input --placeholder "192.168.1.5")"
}
necessary_credentials_with_config() {
echo -e "\n\n ➡️ Please choose your configuration file :"
CONFIG_PATH="$(gum file)"
source "$CONFIG_PATH"
MISSING=0
for VAR in "${NECESSARY_VARIABLES_LIST[@]}"; do
if [[ -v $VAR && -n ${!VAR} ]]; then
echo -e "\n ✅ $VAR imported successfully from the config file"
export $VAR
else
echo "\n ❌ $VAR is missing or empty"
MISSING=1
fi
done
if [[ "$MISSING" -eq "1" ]]; then
exit 1
fi
}
setup_ssh() {
echo -e "\n\n ✅ Generating new SSH for numbus-admin..."
mkdir -p extra-files/home/numbus-admin/.ssh/
chmod 700 extra-files/home/numbus-admin/.ssh/
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "extra-files/home/numbus-admin/.ssh/id_ed25519" -N "" -q
LIVE_REMOTE_PASS=$(gum input --password --placeholder "Enter password for 'nixos@$TARGET_HOST'")
if [ -z "$LIVE_REMOTE_PASS" ]; then
echo " ❌ Password is required to proceed. Aborting."
exit 1
fi
echo -e "\n\n ➡️ Copying SSH key to target host 'nixos@$TARGET_HOST'..."
if sshpass -p "$LIVE_REMOTE_PASS" ssh-copy-id -o StrictHostKeyChecking=no -i "extra-files/home/numbus-admin/.ssh/id_ed25519" "nixos@$TARGET_HOST"; then
echo " ✅ SSH key copied successfully."
else
echo " ❌ Failed to copy SSH key. Please check the host IP and password."
exit 1
fi
export LIVE_REMOTE_PASS
}
ssh_to_live_host() {
ARG="$1"
ssh -i "extra-files/home/numbus-admin/.ssh/id_ed25519" "nixos@$TARGET_HOST" $ARG
}
ssh_to_installed_host() {
ARG="$1"
ssh -i "extra-files/home/numbus-admin/.ssh/id_ed25519" "numbus-admin@$TARGET_HOST" $ARG
}
hardware_detection() {
echo -e "\n\n 🔎 Detecting graphics card on target host..."
VGA_INFO=$(ssh_to_live_host "lspci -nn | grep -i 'vga'")
if echo "$VGA_INFO" | grep -iq "intel" 2>/dev/null; then
echo -e " ✅ Intel graphics card detected."
export TARGET_GRAPHICS="true"
elif echo "$VGA_INFO" | grep -iq "amd" 2>/dev/null; then
echo -e " ✅ AMD graphics card detected."
export TARGET_GRAPHICS="true"
elif echo "$VGA_INFO" | grep -iq "nvidia" 2>/dev/null; then
echo -e " ✅ NVIDIA graphics card detected."
export TARGET_GRAPHICS="true"
else
echo -e " ⚠️ No dedicated graphics card detected."
export TARGET_GRAPHICS="false"
fi
echo -e "\n\n 🔎 Detecting transconding acceleration on target host..."
if ssh_to_live_host "ls /dev/dri/ | grep -iq 'renderD128'" 2>/dev/null; then
echo -e " ✅ Transcoding capable card detected."
TARGET_GRAPHICS_RENDERER="true"
else
echo -e " ⚠️ No transcoding capable card detected."
TARGET_GRAPHICS_RENDERER="false"
fi
echo -e "\n\n 🔎 Detecting USB Google Coral TPU on target host..."
if ssh_to_live_host "lsusb | grep -iq 'google'" 2>/dev/null; then
echo -e " ✅ USB Google Coral TPU detected."
TARGET_USB_CORAL="true"
else
echo -e " ⚠️ No USB Google Coral TPU detected."
TARGET_USB_CORAL="false"
fi
echo -e "\n\n 🔎 Detecting Zigbee coordinator on target host..."
if ssh_to_live_host "ls /dev/serial/by-id/ | grep -i 'zigbee'" 2>/dev/null; then
echo -e " ✅ Zigbee device found in /dev/serial/by-id/."
TARGET_ZIGBEE_DEVICE=$(ssh_to_live_host "ls /dev/serial/by-id/ | grep -i 'zigbee'")
else
echo -e " ⚠️ No Zigbee device found."
TARGET_ZIGBEE_DEVICE=""
fi
}
services_selection() {
echo -e "\n\n ➡️ You will now select the services you want installed on your server:"
AVAILABLE_SERVICES=( "frigate" "gitea" "home-assistant" "immich" "it-tools" \
"nextcloud" "passbolt" "pi-hole" )
AVAILABLE_SERVICES_NUMBER=${#AVAILABLE_SERVICES[@]}
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" \
)
SELECTED_SERVICES_DESCRIPTION=$(gum choose --no-limit --header "Homelab services:" "${SERVICES_DESCRIPTION[@]}")
for i in $(seq 0 $((${#AVAILABLE_SERVICES[@]} - 1))); do
if printf '%s' "$SELECTED_SERVICES_DESCRIPTION" | grep -iq "${AVAILABLE_SERVICES[$i]}"; then
SELECTED_SERVICES+=(${AVAILABLE_SERVICES[$i]})
fi
done
}
files_generation() {
echo -e "\n ✅ Writing configuration files for the selected homelab services..."
# Traefik
mkdir -p extra-files/mnt/config-storage/traefik/config/conf/
envsubst < config-files/docker/config/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
for service in "${SELECTED_SERVICES[@]}"; do
# Frigate
if [[ "$service" -eq "frigate" ]]; then
echo -e "\n ✅ Adapting the docker configuration to your hardware..."
FRIGATE_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" -eq "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
fi
if [[ "$TARGET_USB_CORAL" -eq "true" ]]; then
FRIGATE_DEVICES_BLOCK+=" - /dev/bus/usb:/dev/bus/usb\n"
fi
if [[ -n "$FRIGATE_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${FRIGATE_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- frigate devices --- #|$REPLACEMENT|" ./config-files/docker/compose/frigate.nix
else
sed -i.bak "/# --- frigate devices --- #/d" ./config-files/docker/compose/frigate.nix
fi
# Home-Assistant
elif [[ "$service" -eq "home-assistant" ]]; then
if [[ -n "$TARGET_ZIGBEE_DEVICE" ]]; then
REPLACEMENT="devices:\n - /dev/serial/by-id/${TARGET_ZIGBEE_DEVICE}:/dev/ttyUSB0"
sed -i.bak "s|# --- hass devices --- #|$REPLACEMENT|" ./config-files/docker/compose/home-assistant.nix
else
sed -i.bak "/# --- hass devices --- #/d" ./config-files/docker/compose/home-assistant.nix
fi
export HOME_ASSISTANT_MQTT_USER="$(xkcdpass -d "-" -n 2)"
export HOME_ASSISTANT_MQTT_PASSWORD="$(xkcdpass -d "-")"
mkdir -p extra-files/mnt/config-storage/hass/mqtt/config/
mkdir -p extra-files/mnt/config-storage/hass/mqtt/data/
envsubst < config-files/docker/config/hass/mosquitto.conf > extra-files/mnt/config-storage/hass/mqtt/config/mosquitto.conf
touch extra-files/mnt/config-storage/hass/mqtt/config/password.txt
chmod 0700 extra-files/mnt/config-storage/hass/mqtt/config/password.txt
mosquitto_passwd -b extra-files/mnt/config-storage/hass/mqtt/config/password.txt $HOME_ASSISTANT_MQTT_USER $HOME_ASSISTANT_MQTT_PASSWORD
# Passbolt
elif [[ "$service" -eq "passbolt" ]]; then
export PASSBOLT_DB_NAME="$(xkcdpass -d "-" -n 2)"
export PASSBOLT_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export PASSBOLT_DB_PASSWORD="$(xkcdpass -d "-")"
envsubst < config-files/docker/config/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
envsubst < config-files/docker/config/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
# Pi-Hole
elif [[ "$service" -eq "pi-hole" ]]; then
export FTLCONF_WEBSERVER_PASSWORD="$(xkcdpass -d "-")"
# Immich
elif [[ "$service" -eq "immich" ]]; then
IMMICH_DEVICES_BLOCK=""
if [[ "$TARGET_GRAPHICS_RENDERER" -eq "true" ]]; then
IMMICH_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
fi
if [[ -n "$IMMICH_DEVICES_BLOCK" ]]; then
REPLACEMENT="devices:\n${IMMICH_DEVICES_BLOCK%\\n}"
sed -i.bak "s|# --- immich devices --- #|$REPLACEMENT|" ./config-files/docker/compose/immich.nix
else
sed -i.bak "/# --- immich devices --- #/d" ./config-files/docker/compose/immich.nix
fi
export IMMICH_DB_NAME="$(xkcdpass -d "-" -n 2)"
export IMMICH_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export IMMICH_DB_PASSWORD="$(xkcdpass -d "-")"
mkdir -p extra-files/mnt/data-storage/immich/
elif [[ "$service" -eq "gitea" ]]; then
export GITEA_DB_NAME="$(xkcdpass -d "-" -n 2)"
export GITEA_DB_USERNAME="$(xkcdpass -d "-" -n 2)"
export GITEA_DB_PASSWORD="$(xkcdpass -d "-")"
elif [[ "$service" -eq "nextcloud" ]]; then
envsubst < config-files/docker/config/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
mkdir -p extra-files/mnt/data-storage/nextcloud/
fi
cp ./config-files/docker/compose/${service}.nix ./nix-config/docker/${service}.nix
done
echo -e "\n ✅ Generating sops-nix keys..."
mkdir -p extra-files/etc/secrets/disks/
mkdir -p extra-files/var/lib/sops-nix/
mkdir -p extra-files/etc/nixos/secrets/
ssh-to-age -private-key -i extra-files/home/numbus-admin/.ssh/id_ed25519 > extra-files/var/lib/sops-nix/key.txt
export SOPS_PUBLIC_KEY=$(age-keygen -y extra-files/var/lib/sops-nix/key.txt)
echo -e "\n ✅ Generating sops-nix configuration files..."
envsubst < config-files/sops-nix/.sops.yaml > extra-files/etc/nixos/.sops.yaml
echo -e "\n ✅ Encrypting secrets in the correct file..."
envsubst < "config-files/sops-nix/secrets.yaml" | sops encrypt --filename-override secrets.yaml \
--input-type yaml --output-type yaml \
--age $SOPS_PUBLIC_KEY \
--output extra-files/etc/nixos/secrets/secrets.yaml
echo -e "\n ✅ Writing correct ips to configuration.nix..."
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g ./nix-config/misc/networking.nix
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g ./nix-config/misc/networking.nix
echo -e "\n ✅ Copying the configuration to the new machine..."
cp -ravu ./nix-config/* extra-files/etc/nixos/
}
disk_config_generation() {
### --> Disk wiping warning
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will choose the disks to install NixOS on.
!! PLEASE MAKE SURE YOU BACKED UP ANY IMPORTANT DATA !!
!! ALL DATA WILL BE WIPED ON THE DISKS YOU CHOOSE !!
Please press CTRL+C to abort.
"
gum confirm "Do you understand and wish to proceed?" || { echo -e "\n\n ❌ Aborting as requested."; exit 1; }
echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)"
### Disk wiping warning <--
TMPFILE="/tmp/nixos-deployment-temp-file"
### --> Get disk information
DISK_DETAILS=$(ssh_to_live_host 'bash -s' <<EOF
HDD=1
DISK_DEVPATH=()
DISK_NAME=()
DISK_TYPE=()
DISK_HEALTH=()
DISK_ID=()
DISK_SIZE=()
for DISK in \$(lsblk -x SIZE -d -n -e 7,11 -o NAME); do
# Disk name and simple path
DISK_DEVPATH+=("/dev/\$DISK")
DISK_NAME+=("\$DISK")
# Disk type
HDD=\$(cat /sys/block/\$DISK/queue/rotational)
TRANSPORT_PROTOCOL=\$(lsblk -x SIZE -d -n -e 7,11 -o TRAN /dev/\$DISK)
if [[ "\$DISK" -eq "nvme*" ]]; then DISK_TYPE+=("NVMe");
elif [[ "\$TRANSPORT_PROTOCOL" -eq "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_REMOTE_PASS" | sudo -S smartctl -H /dev/\$DISK 2>/dev/null | grep 'self-assessment' | awk '{print \$6}') -eq "PASSED" ]]; then
DISK_HEALTH+=("PASSED")
else
DISK_HEALTH+=("N/A")
fi
# Disk ID
DISK_ID+=("\$(ls -l /dev/disk/by-id | grep -m1 "../../\$DISK" | awk '{print "/dev/disk/by-id/" \$9}')")
# Disk size
DISK_SIZE+=("\$(lsblk -x SIZE -d -n -e 7,11 -o SIZE /dev/\$DISK)")
done
echo "DISK_DEVPATH=(\${DISK_DEVPATH[@]})" > $TMPFILE
echo "DISK_NAME=(\${DISK_NAME[@]})" >> $TMPFILE
echo "DISK_TYPE=(\${DISK_TYPE[@]})" >> $TMPFILE
echo "DISK_HEALTH=(\${DISK_HEALTH[@]})" >> $TMPFILE
echo "DISK_ID=(\${DISK_ID[@]})" >> $TMPFILE
echo "DISK_SIZE=(\${DISK_SIZE[@]})" >> $TMPFILE
EOF
)
scp -i "extra-files/home/numbus-admin/.ssh/id_ed25519" nixos@$TARGET_HOST:$TMPFILE $TMPFILE &> /dev/null
source $TMPFILE && rm $TMPFILE
### --> Disk selection
if [[ "${#DISK_NAME[@]}" -eq 0 ]]; then
echo -e "\n\n ❌ No disks found on the target host. Aborting."
exit 1
fi
HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "Path")
for i in ${!DISK_NAME[@]}; do
GUM_PRINTED_ELEMENT=$(printf "%-12s %-12s %-12s %-12s %s" \
"${DISK_NAME[${i}]}" "${DISK_TYPE[${i}]}" "${DISK_SIZE[${i}]}" \
"${DISK_HEALTH[${i}]}" "${DISK_DEVPATH[${i}]}")
GUM_PRINTED_ELEMENTS+=("$GUM_PRINTED_ELEMENT")
done
gum style --foreground 212 " ➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation :"
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
if [[ -n "${DISK_ID[${i}]}" ]]; then
export BOOT_DISKS_ID+=("${DISK_NAME[${i}]}")
else
export BOOT_DISKS_ID+=("${DISK_ID[${i}]}")
fi
unset "GUM_PRINTED_ELEMENTS[${i}]"
fi
done
if [[ "${#BOOT_DISKS_ID[@]}" -eq 0 ]]; then
echo -e "\n\n ❌ No boot disk selected. Aborting."
exit 1
elif [[ "${#BOOT_DISKS_ID[@]}" -eq 1 ]]; then
echo -e "\n\n ⚠️ One boot disk selected, continuing with striped boot disk configuration."
echo -e " Consider using 2 boot disks instead to get data protection features on the boot disks."
export BOOT_DISK_1_ID=${BOOT_DISKS_ID[0]}
elif [[ "${#BOOT_DISKS_ID[@]}" -eq 2 ]]; then
echo -e "\n\n ✅ Two boot disks selected, continuing with mirrored boot disks configuration."
echo -e "\n\n ⚠️ If the two disks are different sizes, the resulting usable space size will be \
the one of the smallest disk."
export BOOT_DISK_1_ID=${BOOT_DISKS_ID[0]}
export BOOT_DISK_2_ID=${BOOT_DISKS_ID[1]}
else
echo -e "\n\n ❌ Unexpected bug. Please contact the developer. Aborting."
exit 1
fi
gum style --foreground 212 " ➡️ Please choose data and parity disks (up to 9 total) :"
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
if [[ -n ${DISK_ID[${i}]} ]]; then
export DATA_DISKS_ID+=("${DISK_NAME[${i}]}")
export DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}")
else
export DATA_DISKS_ID+=("${DISK_ID[${i}]}")
export DATA_DISKS_TYPE+=("${DISK_TYPE[${i}]}")
fi
fi
done
PARITY_DISK_NUMBER=$(((${#DATA_DISKS_ID[@]} + 2) / 3))
CONTENT_DISK_NUMBER=$((${#DATA_DISKS_ID[@]} - PARITY_DISK_NUMBER))
### Disk selection <--
### --> Selection recap
RECAP_CONTENT=$(cat <<EOF
### Disk Configuration Summary
Please review the selected disk layout before proceeding.
**Boot Disks (${#BOOT_DISKS_ID[@]}):**
* **Boot 1:** \`${BOOT_DISKS_ID[0]}\`
$( [[ -n "${BOOT_DISKS_ID[1]:-}" ]] && echo "* **Boot 2:** \`${BOOT_DISKS_ID[1]}\`" || echo "* **Boot 2:** *Not configured*")
**Parity Disks ($PARITY_DISK_NUMBER):**
$(for i in $(seq 0 $((${#DATA_DISKS_ID[@]} - CONTENT_DISK_NUMBER))); do echo "* **Parity ${i}:** `${DATA_DISKS_ID[${i}]}`"; done)
$( [[ $PARITY_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*")
**Data Disks ($CONTENT_DISK_NUMBER):**
$(for i in $(seq $PARITY_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do echo "* **Data ${i}:** `${DATA_DISKS_ID[${i}]}`"; done)
$( [[ $CONTENT_DISK_NUMBER -eq 0 ]] && echo "* *Not configured*")
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "$RECAP_CONTENT")"
gum confirm "Proceed with this disk configuration?" || { echo -e "\n\n ❌ Aborting as requested."; exit 1; }
### Selection recap <--
### --> Config generation
echo -e "\n\n ✅ Generating disko configuration from templates..."
TEMPLATE_FILE="config-files/disks/templates/boot-${#BOOT_DISKS_ID[@]}.nix"
(envsubst < "$TEMPLATE_FILE") > ./nix-config/disks/disko.nix
echo -e "\n ✅ Generated boot disk configuration."
# Striped configuration
if [[ "$CONTENT_DISK_NUMBER" -eq 1 && "$PARITY_DISK_NUMBER" -eq 0 ]]; then
export j="1"
export CONTENT_DISK_ID="${DATA_DISKS_ID[0]}"
(envsubst < "config-files/disks/templates/content.nix") >> ./nix-config/disks/disko.nix
sed -i "s|/mnt/content-1|/mnt/data-storage|" ./nix-config/disks/disko.nix
# Mirror configuration
elif [[ "$CONTENT_DISK_NUMBER" -eq 1 && "$PARITY_DISK_NUMBER" -eq 1 ]]; then
export CONTENT_DISK_ID="${DATA_DISKS_ID[0]}"
export PARITY_DISK_ID="${DATA_DISKS_ID[1]}"
(envsubst < "config-files/disks/templates/mirror.nix") >> ./nix-config/disks/disko.nix
# SnapRAID configuration
elif [[ "$CONTENT_DISK_NUMBER" -gt 1 ]]; then
# Enable SnapRAID
sed -i "s|# ./disks/snapraid.nix| ./disks/snapraid.nix|" ./nix-config/configuration.nix
sed -i '$ d' ./config-files/disks/snapraid.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
# --> Automatic data disks unlock, generated by deploy.sh on $(date)
boot.initrd.luks.devices = {
EOF
j=0
for i in $(seq 0 $(($CONTENT_DISK_NUMBER - 1))); do
export ((j++))
LOOP_DISK="${DATA_DISKS_ID[${i}]}"
export CONTENT_DISK_ID=${!LOOP_DISK}
(envsubst < "config-files/disks/templates/content.nix") >> ./nix-config/disks/disko.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
"crypted-content-disk-${j}" = {
device = "${!LOOP_DISK}";
keyFile = "/etc/secrets/disks/content-disk-${j}";
};
EOF
done
echo -e "\n ✅ Generated $CONTENT_DISK_NUMBER data disk configuration(s)."
j=0
for i in $(seq $PARITY_DISK_NUMBER $((${#DATA_DISKS_ID[@]} - 1))); do
export ((j++))
LOOP_DISK="${DATA_DISKS_ID[${i}]}"
export PARITY_DISK_ID=${!LOOP_DISK}
(envsubst < "config-files/disks/templates/parity.nix") >> ./nix-config/disks/disko.nix
cat <<EOF >> ./config-files/disks/snapraid.nix
"crypted-parity-disk-${j}" = {
device = "${!LOOP_DISK}";
keyFile = "/etc/secrets/disks/parity-disk-${j}}";
};
EOF
done
echo -e "\n ✅ Generated $PARITY_DISK_NUMBER parity disk configuration(s)."
# Close the snapraid.nix block
cat <<'EOF' >> ./config-files/disks/snapraid.nix
# Automatic data disks unlock <--
};
}
EOF
cp -avu ./config-files/disks/snapraid.nix ./nix-config/disks/
fi
# Close the disko.nix block
cat <<'EOF' >> ./nix-config/disks/disko.nix
};
};
}
EOF
echo -e "\n ✅ Final disko configuration created."
if [[ -n "${DATA_DISKS_ID[@]}" ]]; then
for i in ${!DATA_DISKS_ID[@]}; do
if [[ "${DATA_DISKS_TYPE[${i}]}" -eq "HDD" ]]; then
DISK_ID_LIST+=("${DATA_DISKS_ID[${i}]}")
fi
done
if [[ -n "${DISK_ID_LIST[@]}" ]]; then
sed -i "s|DISK_ID_LIST|${DISK_ID_LIST[@]}|" ./config-files/disks/spindown.nix
cp -avu ./config-files/disks/spindown.nix ./nix-config/disks/spindown.nix
echo -e "\n ✅ Disk spindown configuration created."
fi
fi
### Config generation <--
### --> Generate unlock keys
for i in ${#BOOT_DISKS_ID[@]}; do
declare "/etc/secrets/disks/boot-disk-${i}=$(xkcdpass -d "-")"
done
for i in $CONTENT_DISK_NUMBER; do
declare "/etc/secrets/disks/content-disk-${i}=$(xkcdpass -d "-")"
done
for i in $PARITY_DISK_NUMBER; do
declare "/etc/secrets/disks/parity-disk-${i}=$(xkcdpass -d "-")"
done
### Generate unlock keys <--
}
deploy() {
echo -e "\n\n 🔄 Deploying to the remote server..."
nix run github:nix-community/nixos-anywhere -- \
--generate-hardware-config nixos-generate-config ./nix-config/hardware-configuration.nix \
--flake ./nix-config#numbus-server \
--extra-files extra-files \
--chown "/home/numbusing a us-admin/" 1000:1000 \
--target-host nixos@$TARGET_HOST
echo -e "\n\n ✅ Installation successfull !"
sleep 1
}
sum_up() {
RECAP_CONTENT=$(cat <<EOF
### Generated Secrets Summary
Please save these secrets in a secure location (e.g., a password manager).
**Service Credentials:**
* **Home Assistant MQTT User:** \`$HOME_ASSISTANT_MQTT_USER\`
* **Home Assistant MQTT Password:** \`$HOME_ASSISTANT_MQTT_PASSWORD\`
* **Passbolt DB Name:** \`$PASSBOLT_MYSQL_DATABASE\`
* **Passbolt DB User:** \`$PASSBOLT_MYSQL_USER\`
* **Passbolt DB Password:** \`$PASSBOLT_MYSQL_PASSWORD\`
* **Pi-hole Web Password:** \`$FTLCONF_WEBSERVER_PASSWORD\`
* **Immich DB Name:** \`$IMMICH_DB_DATABASE_NAME\`
* **Immich DB User:** \`$IMMICH_DB_USERNAME\`
* **Immich DB Password:** \`$IMMICH_DB_PASSWORD\`
**Disk Encryption Keys:**
$(for i in {1..2}; do key_var="BOOT_DISK_${i}_KEY"; [[ -n "${!key_var}" ]] && echo "* **Boot Disk $i Key:** \`${!key_var}\`"; done)
$(for i in {1..6}; do key_var="CONTENT_DISK_${i}_KEY"; [[ -n "${!key_var}" ]] && echo "* **Content Disk $i Key:** \`${!key_var}\`"; done)
$(for i in {1..3}; do key_var="PARITY_DISK_${i}_KEY"; [[ -n "${!key_var}" ]] && echo "* **Parity Disk $i Key:** \`${!key_var}\`"; done)
EOF
)
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "$(gum format <<< "$RECAP_CONTENT")"
}
postrun_action() {
echo -e "\n\n Now the remote machine will reboot. You will need to input the boot disk(s) passphrase.
This will be the only time you will have to do so, it will be automatic in the future."
gum spin --spinner dot --title "Rebooting the remote..." -- sleep 120
gum confirm " ➡️ Select 'yes' once the machine rebooted and you unlocked the disks." || { echo -e "\n\n ❌ Aborting as requested."; exit 1; }
gum spin --spinner dot --title "\n\n 🔄 Waiting for the server to boot up..." --auto <<EOF
while FOUND="false"; do
if ping -c1 -W1 $HOME_SERVER_IP >/dev/null 2>&1; then
FOUND="true"
exit 0
(i++)
if [[ "\${i}" -gt 150 ]]; then
echo -e "\n\n ❌ Could not connect to the server after 150 retries. \
This is most likely due to a networking issue. Please double check your network settings. Aborting."
exit 1
fi
fi
done
EOF
ssh_to_installed_host 'bash -s' <<EOF
sed -i "s|# ./disks/pcr-check.nix| ./disks/pcr-check.nix|" /etc/nixos/configuration.nix
if [[ ${#BOOT_DISKS_ID[@]} -eq 1 ]]; then
echo $INSTALLED_REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-1
elif [[ ${#BOOT_DISKS_ID[@]} -eq 2 ]]; then
echo $INSTALLED_REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-1
echo $INSTALLED_REMOTE_PASS | sudo -S systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+7 /dev/mapper/crypted-boot-2
fi
PCR_HASH=\$(echo $INSTALLED_REMOTE_PASS | sudo -S systemd-analyze pcrs 15 --json=short)
sed -i "s|# systemIdentity.enable = true;| systemIdentity.enable = true;|" /etc/nixos/configuration.nix
sed -i "s|# systemIdentity.pcr15 = "PCR_HASH";| systemIdentity.pcr15 = "PCR_HASH";|" /etc/nixos/configuration.nix
sed -i "s|PCR_HASH|\${PCR_HASH}|" /etc/nixos/configuration.nix
EOF
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'WARNING:') You will now set the password of the numbus-admin user. \
You will almost never user it. Consider using a very strong password : you can write it down \
securely on a hidden sheet of paper or add it to your password manager (local with Passbolt \
any other online password manager provider.)."
gum confirm " ➡️ I understand, 'yes' to proceed." || { echo -e "\n\n ❌ Aborting as requested."; exit 1; }
echo $INSTALLED_REMOTE_PASS | sudo -S passwd numbus-admin
}
congrats() {
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "
⚠️ $(gum style --foreground 212 'CONGRATULATIONS !!:') You now have a working home server. \
Data stored on there will be fully yours and protected. Keep in my mind this comes with the \
responsability of managing it and keeping it secure. Now, you have to log in the webpages of \
the services you installed. Create an admin account for all of them and configure them (or keep \
it simple and use defaults) and take care to note down all the passwords. Change all default passwords \
and create user accounts for your family or friends that will use the server.
Cheers !!"
}
nixos_update() {
echo -e "\n\n 🔄 Updating NixOS on the remote server..."
echo "coming soon !"
}
set -euo pipefail
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")
echo $ACTION_ANSWER
if [[ "$ACTION_ANSWER" -eq "[1] 🌐 Deploy NixOS on a remote machine" ]]; then
echo -e "\n ➡️ Proceeding with deployment…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : start the computer and boot into the NixOS iso.
Launch a console and set up a new user password."
gum confirm "Do you understand and wish to proceed?" || { echo " ❌ Aborting as requested."; exit 1; }
necessary_credentials
setup_ssh
hardware_detection
services_selection
files_generation
disk_config_generation
deploy
postrun_action
congrats
elif [[ "$ACTION_ANSWER" -eq "[2] 💽 Deploy NixOS on a remote machine with a file configuration" ]]; then
echo -e "\n ➡️ Proceeding with deployment using a config file…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : start the computer and boot into the NixOS iso.
Launch a console and set up a new user password."
gum confirm "Do you understand and wish to proceed?" || { echo " ❌ Aborting as requested."; exit 1; }
necessary_credentials_with_config
setup_ssh
hardware_detection
services_selection
files_generation
disk_config_generation
deploy
sum_up
postrun_action
congrats
elif [[ "$ACTION_ANSWER" -eq "[3] 🛠️ Update a NixOS remote machine" ]]; then
echo -e "\n ➡️ Proceeding with update…"
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "➡️ On the target host : make sure the NixOS installation you want
to update is up-and-running, accessible with SSH."
gum confirm "Do you understand and wish to proceed?" || { echo " ❌ Aborting as requested."; exit 1; }
nixos_update
else
echo "Aborting - you did not type '1, 2 or 3'."
exit 1
fi