#!/usr/bin/env nix-shell #!nix-shell -i bash -p gum xkcdpass openssl sops ssh-to-age age sshpass envsubst pciutils usbutils mosquitto rsync 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" -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 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() { ARG="$1" ssh -i "extra-files/home/numbus-admin/.ssh/id_ed25519" "nixos@$TARGET_HOST" $ARG } 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/ | 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_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:" 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_host 'bash -s' </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 echo "Boot ; ${BOOT_DISKS_ID[@]}, ${#BOOT_DISKS_ID[@]}" 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." elif [[ "${#BOOT_DISKS_ID[@]}" -eq 2 ]]; then echo -e "\n\n ✅ Two boot disks selected, continuing with mirrored boot disks configuration." 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 < 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 <> ./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 <> ./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 <> ./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 <