677 lines
32 KiB
Bash
Executable File
677 lines
32 KiB
Bash
Executable File
#!/usr/bin/env nix-shell
|
|
#!nix-shell -i bash -p gum openssl sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto
|
|
|
|
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"
|
|
REQUIRED_VARS=(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)
|
|
MISSING=0
|
|
for VAR in "${REQUIRED_VARS[@]}"; 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" == "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
|
|
|
|
REMOTE_PASS=$(gum input --password --placeholder "Enter password for 'nixos' on '$TARGET_HOST'")
|
|
if [ -z "$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 "$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 REMOTE_PASS
|
|
}
|
|
|
|
ssh_to_host() {
|
|
ssh -i "extra-files/home/numbus-admin/.ssh/id_ed25519" "nixos@$TARGET_HOST" "$1"
|
|
}
|
|
|
|
hardware_detection() {
|
|
echo -e "\n\n 🔎 Detecting graphics card on target host..."
|
|
VGA_INFO=$(ssh_to_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_host "ls /dev/dri/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_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_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_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:"
|
|
|
|
declare -A SERVICE_MAP
|
|
SERVICE_MAP["Pi-Hole: Block ads on all your devices"]="pihole"
|
|
SERVICE_MAP["Home Assistant: Manage your smart home or security cameras"]="hass"
|
|
SERVICE_MAP["Passbolt: Secure password manager with collaboration features"]="passbolt"
|
|
SERVICE_MAP["Frigate [Home Assistant required]: Secure your house with security cameras"]="frigate"
|
|
SERVICE_MAP["Nextcloud: No fuss Office 365 replacement"]="nextcloud"
|
|
SERVICE_MAP["Immich: Pictures and videos backup with local machine-learning"]="immich"
|
|
|
|
mapfile -t SERVICE_DESCRIPTIONS < <(for key in "${!SERVICE_MAP[@]}"; do echo "$key"; done | sort)
|
|
|
|
SELECTED_DESCRIPTIONS_STRING=$(gum choose --no-limit --header "Homelab services:" "${SERVICE_DESCRIPTIONS[@]}")
|
|
|
|
SERVICES=()
|
|
if [[ -n "$SELECTED_DESCRIPTIONS_STRING" ]]; then
|
|
while IFS= read -r line; do
|
|
SERVICES+=("${SERVICE_MAP[$line]}")
|
|
done <<< "$SELECTED_DESCRIPTIONS_STRING"
|
|
fi
|
|
}
|
|
|
|
files_generation() {
|
|
echo -e "\n\n ✅ Generating necessary folder tree..."
|
|
mkdir -p extra-files/etc/secrets/disks/
|
|
mkdir -p extra-files/var/lib/sops-nix/
|
|
mkdir -p extra-files/etc/nixos/secrets/
|
|
mkdir -p extra-files/mnt/config-storage/traefik/config/conf/
|
|
mkdir -p extra-files/mnt/config-storage/hass/mqtt/config/
|
|
mkdir -p extra-files/mnt/config-storage/hass/mqtt/data/
|
|
mkdir -p extra-files/mnt/data-storage/nextcloud/
|
|
mkdir -p extra-files/mnt/data-storage/immich/
|
|
|
|
echo -e "\n ✅ Generating sops-nix keys..."
|
|
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 ✅ Generating secure random database passwords..."
|
|
export HOME_ASSISTANT_MQTT_USER="$(openssl rand -hex 10)"
|
|
export HOME_ASSISTANT_MQTT_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
|
|
export PASSBOLT_MYSQL_DATABASE="$(openssl rand -hex 10)"
|
|
export PASSBOLT_MYSQL_USER="$(openssl rand -hex 10)"
|
|
export PASSBOLT_MYSQL_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
|
|
export FTLCONF_WEBSERVER_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
|
|
export IMMICH_DB_DATABASE_NAME="$(openssl rand -hex 10)"
|
|
export IMMICH_DB_USERNAME="$(openssl rand -hex 10)"
|
|
export IMMICH_DB_PASSWORD="$(openssl rand -base64 32 | tr -d '\=+/')"
|
|
export CONTENT_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export CONTENT_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export CONTENT_DISK_3_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export CONTENT_DISK_4_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export CONTENT_DISK_5_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export CONTENT_DISK_6_KEY="$(openssl rand -base64 10 | tr -d '\=+/')"
|
|
export PARITY_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
|
|
export PARITY_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
|
|
export PARITY_DISK_3_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
|
|
export BOOT_DISK_1_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
|
|
export BOOT_DISK_2_KEY="$(openssl rand -base64 10 | tr -d '\=+/ ')"
|
|
|
|
echo -e "\n ✅ Generating disk keyfiles in /etc/secrets/disks/..."
|
|
for i in {1..6}; do var="CONTENT_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/content-disk-$i"; done
|
|
for i in {1..3}; do var="PARITY_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/parity-disk-$i"; done
|
|
for i in {1..2}; do var="BOOT_DISK_${i}_KEY"; [[ -n "${!var}" ]] && echo -n "${!var}" > "extra-files/etc/secrets/disks/boot-disk-$i"; done
|
|
|
|
echo "$REMOTE_PASS" | ssh_to_host """
|
|
sudo -S mkdir -p /etc/secrets/disks/
|
|
echo -n $CONTENT_DISK_1_KEY | sudo -S tee /etc/secrets/disks/content-disk-1 > /dev/null
|
|
echo -n $CONTENT_DISK_2_KEY | sudo -S tee /etc/secrets/disks/content-disk-2 > /dev/null
|
|
echo -n $CONTENT_DISK_3_KEY | sudo -S tee /etc/secrets/disks/content-disk-3 > /dev/null
|
|
echo -n $CONTENT_DISK_4_KEY | sudo -S tee /etc/secrets/disks/content-disk-4 > /dev/null
|
|
echo -n $CONTENT_DISK_5_KEY | sudo -S tee /etc/secrets/disks/content-disk-5 > /dev/null
|
|
echo -n $CONTENT_DISK_6_KEY | sudo -S tee /etc/secrets/disks/content-disk-6 > /dev/null
|
|
echo -n $PARITY_DISK_1_KEY | sudo -S tee /etc/secrets/disks/parity-disk-1 > /dev/null
|
|
echo -n $PARITY_DISK_2_KEY | sudo -S tee /etc/secrets/disks/parity-disk-2 > /dev/null
|
|
echo -n $PARITY_DISK_3_KEY | sudo -S tee /etc/secrets/disks/parity-disk-3 > /dev/null
|
|
echo -n $BOOT_DISK_1_KEY | sudo -S tee /etc/secrets/disks/boot-disk-1 > /dev/null
|
|
echo -n $BOOT_DISK_2_KEY | sudo -S tee /etc/secrets/disks/boot-disk-2 > /dev/null
|
|
"""
|
|
|
|
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
|
|
|
|
cp -avu extra-files/etc/nixos/secrets/secrets.yaml ./nix-config/secrets/secrets.yaml
|
|
|
|
echo -e "\n ✅ Writing correct ips to configuration.nix..."
|
|
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g ./nix-config/configuration.nix
|
|
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g ./nix-config/configuration.nix
|
|
|
|
echo -e "\n ✅ Adapting the docker configuration to your hardware..."
|
|
FRIGATE_DEVICES_BLOCK=""
|
|
if [[ "$TARGET_GRAPHICS_RENDERER" == "true" ]]; then
|
|
FRIGATE_DEVICES_BLOCK+=" - /dev/dri:/dev/dri\n"
|
|
fi
|
|
if [[ "$TARGET_USB_CORAL" == "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|" docker/frigate.original
|
|
else
|
|
sed -i.bak "/# --- frigate devices --- #/d" docker/frigate.original
|
|
fi
|
|
|
|
IMMICH_DEVICES_BLOCK=""
|
|
if [[ "$TARGET_GRAPHICS_RENDERER" == "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|" docker/immich.original
|
|
else
|
|
sed -i.bak "/# --- immich devices --- #/d" docker/immich.original
|
|
fi
|
|
|
|
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|" docker/hass.original
|
|
else
|
|
sed -i.bak "/# --- hass devices --- #/d" docker/hass.original
|
|
fi
|
|
|
|
echo -e "\n ✅ Copying configuration files for the selected homelab services..."
|
|
cp docker/traefik.original docker/traefik.nix
|
|
for service in "${SERVICES[@]}"; do
|
|
cp docker/${service}.original docker/${service}.nix
|
|
done
|
|
|
|
echo -e "\n ✅ Writing docker configuration files..."
|
|
envsubst < config-files/docker/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
|
|
envsubst < config-files/docker/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
|
|
envsubst < config-files/docker/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
|
|
envsubst < config-files/docker/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
|
|
envsubst < config-files/docker/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
|
|
}
|
|
|
|
disk_config_generation() {
|
|
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 " ❌ Aborting."; exit 1; }
|
|
|
|
echo -e "\n\n 🔎 Fetching and analyzing disks from target host... (This may take a moment)"
|
|
|
|
declare -A DISK_INFO_MAP
|
|
declare -A DISK_SIZE_MAP
|
|
declare -A DISK_BY_ID_MAP
|
|
declare -A DISK_LABEL_MAP
|
|
DISK_OPTIONS=()
|
|
|
|
DISK_NAMES=$(ssh_to_host "lsblk -d -n -o NAME,TYPE | awk '\$2==\"disk\" {print \$1}'")
|
|
|
|
# --> Get disks info
|
|
for name in $DISK_NAMES; do
|
|
details=$(echo "$REMOTE_PASS" | ssh_to_host "
|
|
set -e
|
|
devpath=/dev/$name
|
|
rota=1 # Default to rotational (HDD)
|
|
[ -f /sys/block/$name/queue/rotational ] && rota=\$(cat /sys/block/$name/queue/rotational)
|
|
tran=\$(lsblk -d -n -o TRAN \"\$devpath\" || echo 'unknown')
|
|
health=\$(sudo -S smartctl -H \"\$devpath\" 2>/dev/null | grep 'self-assessment' | awk '{print \$6}')
|
|
by_id=\$(ls -l /dev/disk/by-id | grep -m 1 \"../../$name\$\" | awk '{print \"/dev/disk/by-id/\"\$9}')
|
|
|
|
# Determine type
|
|
if [[ \"\$name\" == nvme* ]]; then type=\"NVMe\"; # Check for NVMe first
|
|
elif [[ \"\$rota\" == \"0\" ]]; then type=\"SSD\";
|
|
elif [[ \"\$tran\" == \"usb\" ]]; then type=\"USB\";
|
|
else type=\"HDD\"; fi
|
|
|
|
# Fallback for health and by-id
|
|
[[ -z \"\$health\" || \"\$health\" == \"\" ]] && health=\"N/A\"
|
|
[ -z \"\$by_id\" ] && by_id=\"\$devpath\"
|
|
|
|
# Get size last, after other commands that might fail
|
|
size=\$(lsblk -b -d -n -o SIZE \"\$devpath\")
|
|
echo \"\$size:::\$type:::\$health:::\$by_id\"
|
|
")
|
|
# Get disks info <--
|
|
|
|
mapfile -t parts < <(echo "$details" | tr ':' '\n')
|
|
size="${parts[0]}"
|
|
disk_type="${parts[3]}"
|
|
health="${parts[6]}"
|
|
by_id="${parts[9]}"
|
|
|
|
human_size=$(numfmt --to=iec-i --suffix=B "$size")
|
|
label=$(printf "%-12s %-12s %-12s %-12s %s" "$name" "$disk_type" "$human_size" "$health" "$by_id")
|
|
|
|
DISK_OPTIONS+=("$label")
|
|
DISK_INFO_MAP["$label"]="$name"
|
|
DISK_SIZE_MAP["$name"]="$size"
|
|
DISK_BY_ID_MAP["$name"]="$by_id"
|
|
DISK_LABEL_MAP["$name"]="$label"
|
|
done
|
|
|
|
if [ ${#DISK_OPTIONS[@]} -eq 0 ]; then
|
|
echo " ❌ No disks found on the target host. Aborting."
|
|
exit 1
|
|
fi
|
|
|
|
HEADER=$(printf " %-12s %-12s %-12s %-12s %s" "Device" "Type" "Size" "SMART" "ID")
|
|
gum style --foreground 212 " ➡️ Please choose one (stripe) or two (mirror) disks for your NixOS boot installation:"
|
|
echo -e ""
|
|
mapfile -t SELECTED_BOOT_LABELS < <(gum choose --limit 2 --header "$HEADER" "${DISK_OPTIONS[@]}")
|
|
|
|
if [ ${#SELECTED_BOOT_LABELS[@]} -eq 0 ]; then echo " ❌ No boot disk selected. Aborting."; exit 1; fi
|
|
|
|
BOOT_DISK_1_NAME=${DISK_INFO_MAP["${SELECTED_BOOT_LABELS[0]}"]}
|
|
BOOT_DISK_1=${DISK_BY_ID_MAP[$BOOT_DISK_1_NAME]}
|
|
BOOT_DISK_2=""
|
|
if [ ${#SELECTED_BOOT_LABELS[@]} -eq 2 ]; then
|
|
BOOT_DISK_2_NAME=${DISK_INFO_MAP["${SELECTED_BOOT_LABELS[1]}"]}
|
|
BOOT_DISK_2=${DISK_BY_ID_MAP[$BOOT_DISK_2_NAME]}
|
|
fi
|
|
|
|
REMAINING_DISKS=()
|
|
for label in "${DISK_OPTIONS[@]}"; do
|
|
is_boot_disk=false
|
|
for selected in "${SELECTED_BOOT_LABELS[@]}"; do
|
|
if [[ "$label" == "$selected" ]]; then is_boot_disk=true; break; fi
|
|
done
|
|
if ! $is_boot_disk; then REMAINING_DISKS+=("$label"); fi
|
|
done
|
|
|
|
if [ ${#REMAINING_DISKS[@]} -gt 0 ]; then
|
|
echo -e ""
|
|
gum style --foreground 212 " ➡️ Please choose your data and parity disks (up to 9 total)."
|
|
mapfile -t SELECTED_DATA_LABELS < <(gum choose --limit 9 --header "$HEADER" "${REMAINING_DISKS[@]}")
|
|
|
|
if [ ${#SELECTED_DATA_LABELS[@]} -gt 0 ]; then
|
|
selected_data_names=()
|
|
for label in "${SELECTED_DATA_LABELS[@]}"; do
|
|
selected_data_names+=("${DISK_INFO_MAP[$label]}")
|
|
done
|
|
|
|
num_selected=${#selected_data_names[@]}
|
|
num_parity=0
|
|
num_content=0
|
|
|
|
if (( num_selected == 2 )); then
|
|
# Special case for 2 data disks: create a mirror.
|
|
echo -e "\n ✅ Two data disks detected, creating a mirror configuration."
|
|
|
|
disk1_name=${selected_data_names[0]}
|
|
disk2_name=${selected_data_names[1]}
|
|
disk1_size=${DISK_SIZE_MAP[$disk1_name]}
|
|
disk2_size=${DISK_SIZE_MAP[$disk2_name]}
|
|
|
|
if [[ "$disk1_size" -ne "$disk2_size" ]]; then
|
|
gum style --border normal --margin "1" --padding "1 2" --border-foreground 212 "⚠️ The two selected disks for the mirror have different sizes. The mirror will be created using the size of the smaller disk, and the extra space on the larger disk will be unused."
|
|
gum confirm "Do you want to proceed anyway?" || { echo " ❌ Aborting as requested."; exit 1; }
|
|
fi
|
|
|
|
export CONTENT_DISK_1=${DISK_BY_ID_MAP[${selected_data_names[0]}]}
|
|
export PARITY_DISK_1=${DISK_BY_ID_MAP[${selected_data_names[1]}]}
|
|
export CONTENT_DISK_1_KEY_VAR="CONTENT_DISK_1_KEY"
|
|
export PARITY_DISK_1_KEY_VAR="PARITY_DISK_1_KEY"
|
|
export CONTENT_DISK_1_KEY=${!CONTENT_DISK_1_KEY_VAR}
|
|
export PARITY_DISK_1_KEY=${!PARITY_DISK_1_KEY_VAR}
|
|
(envsubst < "config-files/disks/mirror.nix") >> ./nix-config/disks/disko.nix
|
|
NUMBER_OF_CONTENT_DISKS=1 # For summary and key generation
|
|
NUMBER_OF_PARITY_DISKS=1
|
|
|
|
elif (( num_selected == 1 )); then # If only one data disk is selected, it is a content disk
|
|
num_content=1
|
|
num_parity=0
|
|
elif (( num_selected > 2 )); then # If more than two data disks are selected
|
|
# 1 parity for up to 2 content disks.
|
|
num_parity=$(( (num_selected + 2) / 3 ))
|
|
num_content=$(( num_selected - num_parity ))
|
|
fi
|
|
|
|
if (( num_selected != 2 )); then
|
|
# Sort selected disks by size (largest first) for snapraid
|
|
sorted_disks=($(
|
|
for name in "${selected_data_names[@]}"; do
|
|
echo "${DISK_SIZE_MAP[$name]} $name"
|
|
done | sort -rn | awk '{print $2}'
|
|
))
|
|
|
|
# Assign parity disks (the largest ones)
|
|
parity_disks_final=()
|
|
for i in $(seq 0 $((num_parity > 0 ? num_parity - 1 : -1))); do
|
|
[[ -n "${sorted_disks[$i]}" ]] && parity_disks_final+=("${DISK_BY_ID_MAP[${sorted_disks[$i]}]}")
|
|
done
|
|
|
|
# Assign content disks (the remaining ones)
|
|
content_disks_final=()
|
|
for i in $(seq $num_parity $((num_selected - 1))); do
|
|
[[ -n "${sorted_disks[$i]}" ]] && content_disks_final+=("${DISK_BY_ID_MAP[${sorted_disks[$i]}]}")
|
|
done
|
|
|
|
# Set exported variables (up to 6 content disks and 3 parity disks)
|
|
for i in {0..5}; do export "CONTENT_DISK_$((i+1))"="${content_disks_final[$i]:-}"; done
|
|
for i in {0..2}; do export "PARITY_DISK_$((i+1))"="${parity_disks_final[$i]:-}"; done
|
|
fi
|
|
fi
|
|
else
|
|
echo -e "\n\n ⚠️ No remaining disks to select for data."
|
|
fi
|
|
|
|
# --> Final recap
|
|
NUMBER_OF_BOOT_DISKS=0
|
|
[[ -n "$BOOT_DISK_1" ]] && NUMBER_OF_BOOT_DISKS=$((NUMBER_OF_BOOT_DISKS + 1)) && export BOOT_DISK_1
|
|
[[ -n "$BOOT_DISK_2" ]] && NUMBER_OF_BOOT_DISKS=$((NUMBER_OF_BOOT_DISKS + 1)) && export BOOT_DISK_2
|
|
|
|
NUMBER_OF_CONTENT_DISKS=0
|
|
for i in {1..6}; do
|
|
disk_var="CONTENT_DISK_$i"
|
|
[[ -n "${!disk_var}" ]] && NUMBER_OF_CONTENT_DISKS=$((NUMBER_OF_CONTENT_DISKS + 1))
|
|
done
|
|
|
|
NUMBER_OF_PARITY_DISKS=0
|
|
for i in {1..3}; do
|
|
disk_var="PARITY_DISK_$i"
|
|
[[ -n "${!disk_var}" ]] && NUMBER_OF_PARITY_DISKS=$((NUMBER_OF_PARITY_DISKS + 1))
|
|
done
|
|
|
|
RECAP_CONTENT=$(cat <<EOF
|
|
### Disk Configuration Summary
|
|
|
|
Please review the selected disk layout before proceeding.
|
|
|
|
**Boot Disks ($NUMBER_OF_BOOT_DISKS):**
|
|
* **Boot 1:** \`$BOOT_DISK_1\`
|
|
$( [[ -n "$BOOT_DISK_2" ]] && echo "* **Boot 2:** \`$BOOT_DISK_2\`" || echo "* **Boot 2:** *Not configured*")
|
|
|
|
**Data Disks ($NUMBER_OF_CONTENT_DISKS):**
|
|
$(for i in {1..6}; do disk_var="CONTENT_DISK_$i"; [[ -n "${!disk_var}" ]] && echo "* **Data $i:** \`${!disk_var}\`"; done)
|
|
$( [[ $NUMBER_OF_CONTENT_DISKS -eq 0 ]] && echo "* *Not configured*")
|
|
|
|
**Parity Disks ($NUMBER_OF_PARITY_DISKS):**
|
|
$(for i in {1..3}; do disk_var="PARITY_DISK_$i"; [[ -n "${!disk_var}" ]] && echo "* **Parity $i:** \`${!disk_var}\`"; done)
|
|
$( [[ $NUMBER_OF_PARITY_DISKS -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 " ❌ Aborting as requested."; exit 1; }
|
|
# Final recap <--
|
|
|
|
echo -e "\n\n ✅ Generating disko configuration from templates..."
|
|
template_file="config-files/disks/boot-${NUMBER_OF_BOOT_DISKS}.nix"
|
|
(envsubst < "$template_file") > ./nix-config/disks/disko.nix
|
|
echo -e "\n ✅ Generated boot disk configuration."
|
|
|
|
if (( NUMBER_OF_CONTENT_DISKS != 2 )); then
|
|
for i in $(seq 1 $NUMBER_OF_CONTENT_DISKS); do
|
|
disk_var="CONTENT_DISK_$i"
|
|
export DISK_NUMBER=$i
|
|
export DISK_PATH=${!disk_var}
|
|
(envsubst < "config-files/disks/content.nix") >> ./nix-config/disks/disko.nix
|
|
done
|
|
[[ "$NUMBER_OF_CONTENT_DISKS" -gt 0 ]] && echo -e "\n ✅ Generated $NUMBER_OF_CONTENT_DISKS data disk configuration(s)."
|
|
|
|
for i in $(seq 1 $NUMBER_OF_PARITY_DISKS); do
|
|
disk_var="PARITY_DISK_$i"
|
|
export DISK_NUMBER=$i
|
|
export DISK_PATH=${!disk_var}
|
|
(envsubst < "config-files/disks/parity.nix") >> ./nix-config/disks/disko.nix
|
|
done
|
|
[[ "$NUMBER_OF_PARITY_DISKS" -gt 0 ]] && echo -e "\n ✅ Generated $NUMBER_OF_PARITY_DISKS parity disk configuration(s)."
|
|
fi
|
|
|
|
# Close the disko.nix block
|
|
cat <<'EOF' >> ./nix-config/disks/disko.nix
|
|
};
|
|
};
|
|
}
|
|
EOF
|
|
echo -e "\n ✅ Final disko configuration created."
|
|
|
|
# --> Generate automatic unlock configuration in ./nix-config/disks/snapraid.nix
|
|
if [[ "$NUMBER_OF_CONTENT_DISKS" -gt 0 || "$NUMBER_OF_PARITY_DISKS" -gt 0 ]]; then
|
|
echo -e "\n\n ✅ Generating automatic disk unlocking configuration..."
|
|
sed -i '$ d' ./nix-config/disks/snapraid.nix
|
|
|
|
cat <<EOF >> ./nix-config/disks/snapraid.nix
|
|
# --> Automatic data disks unlock, generated by deploy.sh on $(date)
|
|
boot.initrd.luks.devices = {
|
|
EOF
|
|
|
|
if [[ "$NUMBER_OF_CONTENT_DISKS" -gt 0 ]]; then
|
|
for i in $(seq 1 $NUMBER_OF_CONTENT_DISKS); do
|
|
disk_var="CONTENT_DISK_$i"
|
|
cat <<EOF >> ./nix-config/disks/snapraid.nix
|
|
"crypted-content-disk-${i}" = {
|
|
device = "${!disk_var}";
|
|
keyFile = "/etc/secrets/disks/content-disk-${i}";
|
|
};
|
|
EOF
|
|
done
|
|
fi
|
|
|
|
if [[ "$NUMBER_OF_PARITY_DISKS" -gt 0 ]]; then
|
|
for i in $(seq 1 $NUMBER_OF_PARITY_DISKS); do
|
|
disk_var="PARITY_DISK_$i"
|
|
cat <<EOF >> ./nix-config/disks/snapraid.nix
|
|
"crypted-parity-disk-${i}" = {
|
|
device = "${!disk_var}";
|
|
keyFile = "/etc/secrets/disks/parity-disk-${i}";
|
|
};
|
|
EOF
|
|
done
|
|
fi
|
|
|
|
cat <<'EOF' >> ./nix-config/disks/snapraid.nix
|
|
# Automatic data disks unlock <--
|
|
};
|
|
}
|
|
EOF
|
|
fi
|
|
# Generate automatic unlock configuration in ./nix-config/disks/snapraid.nix <--
|
|
}
|
|
|
|
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/numbus-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 ""
|
|
# Add TPM2 boot disk decryption
|
|
# Add pcr-check.nix
|
|
}
|
|
|
|
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" == "[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
|
|
elif [[ "$ACTION_ANSWER" == "[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
|
|
elif [[ "$ACTION_ANSWER" == "[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 |