343 lines
16 KiB
Bash
343 lines
16 KiB
Bash
#@GEMINI.md @agents Take the NixOS expert role. I would like to make this installer universal, this means that the disko config has to adapt
|
||
#to the available disks in the system. Since covering every possible disk configuration would be impossible, I would like to cover a few of them
|
||
#that are relevant in the context of a home server. First I want every disk to be encrypted. Second, there always has to be a boot drive on which
|
||
#nixos, docker and config data (small data) is installed. This drive can be standalone (even though that is kind of pointless in production but this
|
||
#is more for testing purposes). Third, if present, other disks (2 or 3 never more) than the boot drive must be used in a redundant way for the
|
||
#big data (nextcloud user data, immich photos, ...). Fourth, if the data disks are SSDs or NVMes, they must use ZFS (mirror or raid1).
|
||
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
cat <<EOF
|
||
██████ █████ ███ ███████ █████████
|
||
░░██████ ░░███ ░░░ ███░░░░░███ ███░░░░░███
|
||
░███░███ ░███ ████ █████ █████ ███ ░░███░███ ░░░
|
||
░███░░███░███ ░░███ ░░███ ░░███ ░███ ░███░░█████████
|
||
░███ ░░██████ ░███ ░░░█████░ ░███ ░███ ░░░░░░░░███
|
||
░███ ░░█████ ░███ ███░░░███ ░░███ ███ ███ ░███
|
||
█████ ░░█████ █████ █████ █████ ░░░███████░ ░░█████████
|
||
░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░ ░░░░░░░░░
|
||
|
||
|
||
|
||
█████████ █████
|
||
███░░░░░███ ░░███
|
||
░███ ░███ ████████ █████ ████ █████ ███ █████ ░███████ ██████ ████████ ██████
|
||
░███████████ ░░███░░███ ░░███ ░███ ░░███ ░███░░███ ░███░░███ ███░░███░░███░░███ ███░░███
|
||
░███░░░░░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███ ░███████ ░███ ░░░ ░███████
|
||
░███ ░███ ░███ ░███ ░███ ░███ ░░███████████ ░███ ░███ ░███░░░ ░███ ░███░░░
|
||
█████ █████ ████ █████ ░░███████ ░░████░████ ████ █████░░██████ █████ ░░██████
|
||
░░░░░ ░░░░░ ░░░░ ░░░░░ ░░░░░███ ░░░░ ░░░░ ░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░
|
||
███ ░███
|
||
░░██████
|
||
░░░░░░
|
||
EOF
|
||
|
||
sleep 1
|
||
|
||
hardware_detection() {
|
||
echo -e "\n\n 🔎 Detecting graphics card on target host..."
|
||
VGA_INFO=$(ssh nixos@$TARGET_HOST 'lspci -nn | grep -i "vga"')
|
||
if echo "$VGA_INFO" | grep -iq "intel"; then
|
||
echo -e " ✅ Intel graphics card detected."
|
||
TARGET_GRAPHICS="true"
|
||
elif echo "$VGA_INFO" | grep -iq "amd"; then
|
||
echo -e " ✅ AMD graphics card detected."
|
||
TARGET_GRAPHICS="true"
|
||
elif echo "$VGA_INFO" | grep -iq "nvidia"; then
|
||
echo -e " ✅ NVIDIA graphics card detected."
|
||
TARGET_GRAPHICS="true"
|
||
else
|
||
echo -e " ℹ️ No dedicated graphics card detected."
|
||
TARGET_GRAPHICS="false"
|
||
fi
|
||
|
||
echo -e "\n\n 🔎 Detecting transconding acceleration on target host..."
|
||
if ls /dev/dri/renderD128; 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 nixos@$TARGET_HOST 'lsusb | grep -iq "google"'; 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 nixos@$TARGET_HOST 'ls /dev/serial/by-id/ | grep -i "zigbee"'; then
|
||
echo -e " ✅ Zigbee device found in /dev/serial/by-id/."
|
||
TARGET_ZIGBEE_DEVICE=$(ssh nixos@$TARGET_HOST 'ls /dev/serial/by-id/ | grep -i "zigbee"')
|
||
TARGET_ZIGBEE_DEVICE_PATH="/dev/serial/by-id/$TARGET_ZIGBEE_DEVICE"
|
||
TARGET_ZIGBEE="true"
|
||
else
|
||
echo -e " ℹ️ No Zigbee device found."
|
||
TARGET_ZIGBEE="false"
|
||
fi
|
||
}
|
||
|
||
files_generation() {
|
||
echo -e "\n\n ✅ Generating necessary folder tree..."
|
||
mkdir -p extra-files/home/numbus-admin/.ssh/
|
||
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\n ✅ Generating new SSH for numbus-admin..."
|
||
ssh-keygen -t ed25519 -C numbus-admin@numbus-server -f extra-files/home/numbus-admin/.ssh/id_ed25519 -N "" -q
|
||
|
||
echo -e "\n\n ✅ Generating sops-nix keys..."
|
||
nix run nixpkgs#ssh-to-age -- -private-key -i extra-files/home/numbus-admin/.ssh/id_ed25519 > extra-files/var/lib/sops-nix/key.txt
|
||
SOPS_PUBLIC_KEY=$(nix shell nixpkgs#age -c age-keygen -y extra-files/var/lib/sops-nix/key.txt)
|
||
|
||
echo -e "\n\n ✅ Generating sops-nix configuration files..."
|
||
envsubst < config-files/sops-nix/.sops.yaml > extra-files/etc/nixos/.sops.yaml
|
||
|
||
echo -e "\n\n ✅ Generating secure random database passwords..."
|
||
HOME_ASSISTANT_MQTT_USER=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
|
||
HOME_ASSISTANT_MQTT_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
|
||
PASSBOLT_MYSQL_DATABASE=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
|
||
PASSBOLT_MYSQL_USER=$(openssl rand -base64 29 | tr -d "123456789=+/" | cut -c1-10)
|
||
PASSBOLT_MYSQL_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
|
||
FTLCONF_WEBSERVER_PASSWORD=$(openssl rand -base64 29 | tr -d "=+/" | cut -c1-64)
|
||
|
||
echo -e "\n\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\n ✅ Writing correct disk to disk-config.nix..."
|
||
sed -i s+TARGET_DISK+$TARGET_DISK+g disk-config.nix
|
||
|
||
echo -e "\n\n ✅ Writing correct ips to configuration.nix..."
|
||
sed -i s+HOME_SERVER_IP+$HOME_SERVER_IP+g configuration.nix
|
||
sed -i s+HOME_ROUTER_IP+$HOME_ROUTER_IP+g configuration.nix
|
||
|
||
echo -e "\n\n ✅ Adapting the docker configuration to your hardware..."
|
||
if [[ "$TARGET_GRAPHICS" == "true" && "$TARGET_USB_CORAL" == "true" ]]; then
|
||
sed -i.bak '
|
||
/^[[:space:]]*# ----------------------------------------- #/{
|
||
N;
|
||
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
|
||
N;
|
||
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
|
||
N;
|
||
/----------------------------------------- #/c\
|
||
devices:\
|
||
- /dev/dri:/dev/dri\
|
||
- /dev/bus/usb:/dev/bus/usb
|
||
}
|
||
}
|
||
}' docker/frigate.nix
|
||
elif [[ "$TARGET_GRAPHICS" == "true" && "$TARGET_USB_CORAL" == "false" ]]; then
|
||
sed -i.bak '
|
||
/^[[:space:]]*# ----------------------------------------- #/{
|
||
N;
|
||
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
|
||
N;
|
||
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
|
||
N;
|
||
/----------------------------------------- #/c\
|
||
devices:\
|
||
- /dev/dri:/dev/dri\
|
||
}
|
||
}
|
||
}' docker/frigate.nix
|
||
elif [[ "$TARGET_GRAPHICS" == "false" && "$TARGET_USB_CORAL" == "true" ]]; then
|
||
sed -i.bak '
|
||
/^[[:space:]]*# ----------------------------------------- #/{
|
||
N;
|
||
/DEVICES SECTION WILL APPEAR HERE IF CORAL/{
|
||
N;
|
||
/TPU OR INTEGRATED GRAPHICS ARE PRESENT/{
|
||
N;
|
||
/----------------------------------------- #/c\
|
||
devices:\
|
||
- /dev/bus/usb:/dev/bus/usb
|
||
}
|
||
}
|
||
}' docker/frigate.nix
|
||
fi
|
||
if [[ "$TARGET_ZIGBEE" == "true" ]]; then
|
||
sed -i.bak "
|
||
/^[[:space:]]*# ----------------------------------- #/{
|
||
N;
|
||
/DEVICES SECTION WILL APPEAR HERE IF/{
|
||
N;
|
||
/ZIGBEE USB DEVICE IS PRESENT/{
|
||
N;
|
||
/----------------------------------- #/c\
|
||
devices:\
|
||
- ${TARGET_ZIGBEE_DEVICE_PATH}:/dev/ttyUSB0
|
||
}
|
||
}
|
||
}" docker/hass.nix
|
||
fi
|
||
|
||
echo -e "\n\n ✅ Copying files to the new installation..."
|
||
cp -ravu secrets/ .sops.yaml hardware-configuration.nix extra-files/etc/nixos/
|
||
|
||
echo -e "\n\n ✅ Writing docker configuration files..."
|
||
envsubst < config-files/traefik/headers.yaml > extra-files/mnt/config-storage/traefik/config/conf/headers.yaml
|
||
envsubst < config-files/traefik/nextcloud.yaml > extra-files/mnt/config-storage/traefik/config/conf/nextcloud.yaml
|
||
envsubst < config-files/traefik/tls.yaml > extra-files/mnt/config-storage/traefik/config/conf/tls.yaml
|
||
envsubst < config-files/traefik/traefik.yaml > extra-files/mnt/config-storage/traefik/config/traefik.yaml
|
||
envsubst < config-files/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
|
||
nix shell nixpkgs#mosquitto -c mosquitto_passwd -b extra-files/mnt/config-storage/hass/mqtt/config/password.txt $HOME_ASSISTANT_MQTT_USER $HOME_ASSISTANT_MQTT_PASSWORD
|
||
}
|
||
|
||
deploy() {
|
||
echo -e "\n\n 🔄 Deploying to the remote server..."
|
||
nix run github:nix-community/nixos-anywhere -- \
|
||
--generate-hardware-config nixos-generate-config ./hardware-configuration.nix \
|
||
--flake .#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
|
||
}
|
||
|
||
nixos_deployment() {
|
||
echo -e "\n\n ➡️ On the target host : start the computer and boot into the NixOS iso.\n Launch a console and set up a new user password."
|
||
echo -e " Type 'done' when you have finished."
|
||
read -r SETUP_ANSWER
|
||
if [[ "$SETUP_ANSWER" == "done" ]]; then
|
||
:
|
||
else
|
||
echo "Aborting - you did not type 'done'."
|
||
exit 1
|
||
fi
|
||
|
||
#TARGET SETTINGS
|
||
echo -e "\n\n ➡️ Please provide the IP address of the target host :"
|
||
read -r TARGET_HOST
|
||
echo -e "\n\n ➡️ Please provide the disk you want to install NixOS on (i.e. /dev/vda, /dev/sda, /dev/nvme0n1...) :"
|
||
read -r TARGET_DISK
|
||
echo -e "\n\n ➡️ Please provide the public SSH key of an authorized device :"
|
||
read -r SSH_PUBLIC_KEY
|
||
|
||
# TRAEFIK SETTINGS
|
||
echo -e "\n\n ➡️ Please provide the domain name (FQDN) your home server will use (i.e. yourdomain.com):"
|
||
read -r DOMAIN_NAME
|
||
echo -e "\n\n ➡️ Please provide a valid email address (will be used for ACME, and your services) :"
|
||
read -r EMAIL_ADDRESS
|
||
echo -e "\n\n ➡️ Please provide a cloudflare API token with DNS zone permission :"
|
||
read -r CF_DNS_API_TOKEN
|
||
|
||
#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 (Gmail for example)."
|
||
echo -e " Please provide a valid sender email address :"
|
||
read -r SENDER_EMAIL_ADDRESS
|
||
echo -e "\n\n ➡️ Please provide the password of this email address :"
|
||
read -r SENDER_EMAIL_ADDRESS_PASSWORD
|
||
echo -e "\n\n ➡️ Please provide the smtp endpoint (for gmail : smtp.gmail.com) :"
|
||
read -r SENDER_EMAIL_DOMAIN
|
||
echo -e "\n\n ➡️ Please provide the smtp TLS port (for gmail : 587) :"
|
||
read -r SENDER_EMAIL_PORT
|
||
|
||
#NETWORK SETTINGS
|
||
echo -e "\n\n ➡️ Please provide your home network subnet (i.e. 192.168.1.1/24) :"
|
||
read -r HOME_ROUTER_SUBNET
|
||
echo -e "\n\n ➡️ Please provide the ip address of your router (i.e. 192.168.1.1) :"
|
||
read -r HOME_ROUTER_IP
|
||
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\n range that is not in use. 192.168.1.5 for example.) :"
|
||
read -r HOME_SERVER_IP
|
||
|
||
echo -e "\n\n ➡️ Please provide enter the password of the remote target."
|
||
ssh-copy-id nixos@$TARGET_HOST
|
||
|
||
hardware_detection
|
||
|
||
files_generation
|
||
|
||
deploy
|
||
}
|
||
|
||
nixos_deployment_with_config() {
|
||
echo -e "\n\n ➡️ On the target host : start the computer and boot into the NixOS iso.\n Launch a console and set up a new user password."
|
||
echo -e " Type 'done' when you have finished."
|
||
read -r SETUP_ANSWER
|
||
if [[ "$SETUP_ANSWER" == "done" ]]; then
|
||
:
|
||
else
|
||
echo "Aborting - you did not type 'done'."
|
||
exit 1
|
||
fi
|
||
|
||
echo -e "\n\n ➡️ Please provide the path to a config file :"
|
||
read -erp CONFIG_PATH
|
||
CONFIG_PATH=$(realpath -m "$CONFIG_PATH")
|
||
if [[ ! -f "$CONFIG_PATH" ]]; then
|
||
echo "Error: '$CONFIG_PATH' does not exist or is not a regular file."
|
||
exit 1
|
||
elif [[ ! -r "$CONFIG_PATH" ]]; then
|
||
echo "Error: '$CONFIG_PATH' is not readable."
|
||
exit 1
|
||
fi
|
||
if grep -qE '^[[:space:]]*[^[:space:]#][^=]*=' "$CONFIG_PATH"; then
|
||
source "$CONFIG_PATH"
|
||
else
|
||
echo "Config contains unsupported syntax."
|
||
exit 1
|
||
fi
|
||
|
||
REQUIRED_VARS=(TARGET_HOST TARGET_DISK 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"
|
||
sleep 0.1
|
||
else
|
||
echo "\n ❌ $VAR is missing or empty"
|
||
sleep 0.1
|
||
MISSING=1
|
||
fi
|
||
done
|
||
|
||
if [[ "$MISSING" == "1" ]]; then
|
||
exit 1
|
||
fi
|
||
|
||
files_generation
|
||
|
||
deploy
|
||
}
|
||
|
||
nixos_update() {
|
||
|
||
}
|
||
|
||
echo -e "\n\n Please choose an action (i.e. 1, 2 or 3) :\n"
|
||
echo -e " - [1] 🌐 Deploy NixOS on a remote machine"
|
||
echo -e " - [2] 💽 Deploy NixOS on a remote machine with a file configuration"
|
||
echo -e " - [3] 🛠️ Update a NixOS remote machine"
|
||
read -r ACTION_ANSWER
|
||
|
||
if [[ "$ACTION_ANSWER" == "1" ]]; then
|
||
echo -e "\n ➡️ Proceeding with deployment…"
|
||
nixos_deployment
|
||
elif [[ "$ACTION_ANSWER" == "2" ]]; then
|
||
echo -e "\n ➡️ Proceeding with deployment using a config file…"
|
||
nixos_deployment_with_config
|
||
elif [[ "$ACTION_ANSWER" == "3" ]]; then
|
||
echo -e "\n ➡️ Proceeding with update…"
|
||
nixos_update
|
||
else
|
||
echo "Aborting - you did not type '1, 2 or 3'."
|
||
exit 1
|
||
fi |