Merge branch 'testing' into production
@@ -1,5 +1,4 @@
|
||||
/config/
|
||||
/web/ux/
|
||||
final-nix-config/
|
||||
test*
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -1,5 +1,5 @@
|
||||
/config/
|
||||
/web/ux/
|
||||
web/ux/
|
||||
test*
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -209,7 +209,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||
|
||||
numbus-server
|
||||
Copyright (C) 2025 raphael
|
||||
Copyright (C) 2025 Raphaël Billet
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
numbus-server Copyright (C) 2025 raphael
|
||||
numbus-server Copyright (C) 2025 Raphaël Billet
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||
|
||||
|
||||
@@ -1,54 +1,111 @@
|
||||
# ☁️ Numbus Server: Your Personal Cloud, Simplified 🚀
|
||||
# ☁️ Welcome to Numbus 🚀
|
||||
|
||||
Welcome to the **Numbus Server** project! This repository provides a complete NixOS configuration to deploy a personal home server with a rich set of services in minutes. Our goal is to make self-hosting accessible to everyone, allowing you to take back control of your data with a solution that is easy to manage and highly reliable.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🌐 **Free & Open-Source:** Built with transparency and community collaboration in mind.
|
||||
- 🚀 **Easy Deployment:** Get your server up and running in minutes with a single command.
|
||||
- 🛠️ **Set & Forget:** A highly reliable, low-maintenance solution.
|
||||
- 🔒 **Secure by Design:** Strong security practices are at the core of our configuration.
|
||||
- 📦 **Popular Services:** Access a wide range of popular, pre-configured services.
|
||||
- ⚙️ **Declarative & Reproducible:** Thanks to NixOS, your system configuration is entirely declarative, ensuring reproducibility and easy maintenance.
|
||||
|
||||
## 🛠️ Key Technologies
|
||||
|
||||
- **NixOS:** A declarative Linux distribution that makes system management a breeze.
|
||||
- **Nix Flakes:** For reproducible builds and dependency management.
|
||||
- **Docker & Docker Compose:** To run containerized services with ease.
|
||||
- **Traefik:** A modern reverse proxy for securely exposing services.
|
||||
- **Sops-nix:** For secure and convenient management of secrets.
|
||||
- **NixOS-anywhere:** For seamless initial deployment to any machine.
|
||||
- **Disko:** For declarative and predictable disk partitioning.
|
||||
Welcome to the **Numbus Server** project! This repository provides a complete selection of highly customizable NixOS configurations to deploy **home servers**, **backup servers**, **computers**, **TVs** in an automated manner. Deploy devices in hours instead of days !
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
The entire deployment process is automated with the `deploy.sh` script. This script dynamically adapts the configuration to your hardware, network environment, and secrets.
|
||||
The entire deployment process is automated around a bash script. It helps to dynamically adapt the configuration to your hardware, network environment, and secrets.
|
||||
|
||||
**1. Clone the Repository:**
|
||||
### Requirements :
|
||||
#### For numbus-server :
|
||||
- A NixOS-based machine to deploy from
|
||||
- A NixOS-based live machine to deploy to
|
||||
- An email address with automated mail sending support
|
||||
- A valid, public domain name
|
||||
|
||||
#### For numbus-backup-server :
|
||||
- A NixOS-based machine to deploy from
|
||||
- A NixOS-based live machine to deploy to
|
||||
- An email address with automated mail sending support
|
||||
- A valid domain name
|
||||
|
||||
#### For numbus-computer :
|
||||
- A NixOS-based machine to deploy from
|
||||
- A NixOS-based live machine to deploy to
|
||||
|
||||
#### For numbus-tv :
|
||||
- A NixOS-based machine to deploy from
|
||||
- A NixOS-based live machine to deploy to
|
||||
|
||||
### On your Nix-based machine :
|
||||
|
||||
**1. Clone the Repository :**
|
||||
|
||||
```bash
|
||||
git clone https://git.numbus.eu/raphael/numbus-server.git
|
||||
cd numbus-server
|
||||
git clone https://git.numbus.eu/raphael/numbus.git
|
||||
cd numbus
|
||||
```
|
||||
|
||||
**2. Run the Deployment Script:**
|
||||
**2. (Optional) Fill in the configuration file :**
|
||||
|
||||
This step is globally recommended as it reduces the risks of typos in the credentials. It also is better suited for repeated deployments to multiple machines.
|
||||
|
||||
```bash
|
||||
nano $your_choosed_device.conf
|
||||
```
|
||||
|
||||
Example: if you want to deploy a server
|
||||
|
||||
```bash
|
||||
nano numbus-server.conf
|
||||
```
|
||||
|
||||
**3. Run the Deployment Script :**
|
||||
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
**3. Follow the Prompts:**
|
||||
**4. Follow the script's Prompts :**
|
||||
|
||||
> If you followed **step 2**, then choose non-interactive mode.
|
||||
|
||||
> If you skipped **step 2**, then choose interactive mode.
|
||||
|
||||
The script will guide you through the setup process, including choosing a deployment mode and providing the necessary information. Once completed, the script will:
|
||||
|
||||
- Adapt the configuration to your machine.
|
||||
- Generate SSH and `sops` keys.
|
||||
- Encrypt secrets for secure storage.
|
||||
- Generate configuration files for Docker services.
|
||||
- Deploy the NixOS configuration using `nixos-anywhere`.
|
||||
- Generate configuration files for Podman services.
|
||||
- Deploy the NixOS configuration to the remote using `nixos-anywhere`.
|
||||
- Verify the deployment and guide you through the final setup of the web UIs.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🌐 **Free & Open-Source :** Built with transparency and community collaboration in mind.
|
||||
- 🚀 **Easy Deployment :** Get your server up and running in minutes with a single command.
|
||||
- 🛠️ **Set & Forget :** A highly reliable, low-maintenance solution.
|
||||
- 🔒 **Secure by Design :** Strong security practices are at the core of our configuration.
|
||||
- 📦 **Popular Services :** Access a wide range of popular, pre-configured services.
|
||||
- ⚙️ **Declarative & Reproducible :** Thanks to NixOS, your system configuration is entirely declarative, ensuring reproducibility and easy maintenance.
|
||||
|
||||
## 🛠️ Key Technologies
|
||||
|
||||
#### Global project :
|
||||
- **[NixOS](https://nixos.org):** A declarative Linux distribution that makes system management a breeze.
|
||||
- **[Nix Flakes](https://wiki.nixos.org/wiki/Flakes):** For reproducible builds and dependency management.
|
||||
- **[Sops-nix](https://github.com/Mic92/sops-nix):** For secure and convenient management of secrets.
|
||||
- **[NixOS-anywhere](https://github.com/nix-community/nixos-anywhere):** For seamless initial deployment to any machine.
|
||||
- **[Disko](https://github.com/nix-community/disko):** For declarative and predictable disk partitioning.
|
||||
|
||||
#### Server-centric features :
|
||||
- **[SnapRAID](https://www.snapraid.it):** Keep your data safe.
|
||||
- **[Qemu/KVM](https://www.qemu.org):** Run virtual with near-native performance.
|
||||
- **[Podman](https://podman.io):** Run rootless, containerized services with ease.
|
||||
- **[Traefik](https://traefik.io/traefik):** A modern reverse proxy for securely exposing services.
|
||||
|
||||
#### Desktop-centric features :
|
||||
- **[GNOME](https://www.gnome.org):** A modern, elegant desktop environment.
|
||||
- **[KDE Plasma](https://kde.org):** A full-featured and highly customizable desktop environment.
|
||||
- **[XFCE](https://xfce.org)**: A super lightweight desktop to breathe new life into old computers.
|
||||
- **[Wide offering of free and open-source apps](https://flathub.org/en/apps)**: If you need to get something done, there is an app for it.
|
||||
- **[Windows games compatibility](https://www.protondb.com)**: Most games run on Linux thanks to Proton.
|
||||
|
||||
#### TV-centric features :
|
||||
- **[KDE Plasma Bigscreen](https://plasma-bigscreen.org):** An open-source TV interface for Linux.
|
||||
- **[Web applications](https://flathub.org/en/apps/net.codelogistics.webapps)**: Install websites as apps.
|
||||
|
||||
## 🔧 Deployment Modes
|
||||
|
||||
The `deploy.sh` script offers three modes to suit your needs:
|
||||
|
||||
@@ -1,931 +0,0 @@
|
||||
#!/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
|
||||
|
||||
### --> Default settings
|
||||
export GUM_SPIN_SPINNER="minidot"
|
||||
export GUM_SPIN_SPINNER_BOLD=true
|
||||
export GUM_SPIN_SHOW_ERROR=true
|
||||
export GUM_SPIN_TITLE_BOLD=true
|
||||
|
||||
NECESSARY_VARIABLES_LIST=( LIVE_TARGET_IP LIVE_TARGET_PASSWD \
|
||||
TIMEZONE LANGUAGE LOCALE SERVER_OWNER_NAME SERVER_USER_EMAIL SERVER_ADMIN_EMAIL AUTHORIZED_SSH_PUBLIC_KEY \
|
||||
DOMAIN_NAME CLOUDFLARE_DNS_API_TOKEN \
|
||||
SMTP_SERVER_USERNAME SMTP_SERVER_PASSWORD SMTP_SERVER_HOST SMTP_SERVER_PORT \
|
||||
NETWORK_SUBNET NETWORK_ROUTER_IP HOME_SERVER_IP)
|
||||
|
||||
### Default settings <--
|
||||
|
||||
user_input() {
|
||||
local VAR_NAME="${1}"
|
||||
local HEADER="${2}"
|
||||
local PLACEHOLDER="${3}"
|
||||
local REGEX="${4}"
|
||||
local ERROR_MSG="${5}"
|
||||
local SENSITIVE="${6:-false}"
|
||||
|
||||
while true; do
|
||||
[[ "$SENSITIVE" == "false" ]] && INPUT_VALUE=$(gum input --placeholder "${PLACEHOLDER}" --header "${HEADER}")
|
||||
[[ "$SENSITIVE" == "true" ]] && INPUT_VALUE=$(gum input --password --placeholder "${PLACEHOLDER}" --header "${HEADER}")
|
||||
|
||||
if [[ -z "${INPUT_VALUE}" ]]; then
|
||||
echo "❌ Error: Input cannot be empty. Please provide the necessary information."
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -n "${REGEX}" ]]; then
|
||||
if [[ ! "${INPUT_VALUE}" =~ ${REGEX} ]]; then
|
||||
echo "❌ Error: ${ERROR_MSG}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
export "${VAR_NAME}"="${INPUT_VALUE}"
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
strictly_necessary_information() {
|
||||
export IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
|
||||
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format."
|
||||
user_input "LIVE_TARGET_PASSWD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
|
||||
}
|
||||
|
||||
necessary_information() {
|
||||
# Regex Definitions
|
||||
local SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
|
||||
local DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||
local EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
local PORT_REGEX='^[0-9]{1,5}$'
|
||||
local SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
|
||||
|
||||
echo -e "\n\n➡️ This script needs information about the target you want to install NixOS on\n"
|
||||
# LIVE TARGET SETTINGS
|
||||
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}" "Invalid IP address format."
|
||||
user_input "LIVE_TARGET_PASSWD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
|
||||
|
||||
echo -e "\n\n➡️ Now provide some information about the server you are deploying\n"
|
||||
# SERVER SETTINGS
|
||||
user_input "TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin" "" ""
|
||||
user_input "LANGUAGE" " Please provide the wanted language :" "For example : FR (for french), EN (for english), DE, IT, etc" "" ""
|
||||
user_input "LOCALE" " Please provide your locale :" "For example : fr_FR for France, de_DE for Germany, en_US for USA or en_GB for Great-Britain, etc" "" ""
|
||||
user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve" "" ""
|
||||
user_input "SERVER_USER_EMAIL" " Please provide a valid user email address (to stay informed about your server's health) :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
|
||||
user_input "SERVER_ADMIN_EMAIL" " Please provide a valid admin email address (will be used for ACME, and system failures notifications) :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
|
||||
user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide a list of SSH public keys of authorized devices :" "For example : ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhcYDmjMo5YApLkk/3P3HZCnOSzm0uYewNAbxL8Fci8 user@your-pc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)." "true"
|
||||
|
||||
echo -e "\n\n➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains\n"
|
||||
# TRAEFIK SETTINGS
|
||||
user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}" "Invalid domain name format."
|
||||
user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
|
||||
|
||||
echo -e "\n\n➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)\n"
|
||||
# SMTP SETTINGS
|
||||
user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}" "Invalid email address format."
|
||||
user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
|
||||
user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
|
||||
user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number."
|
||||
|
||||
echo -e "\n\n➡️ This server will connect to your local network and you will configure its IP address\n"
|
||||
# NETWORK SETTINGS
|
||||
user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
|
||||
user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format."
|
||||
user_input "HOME_SERVER_IP" " 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.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
|
||||
}
|
||||
|
||||
necessary_information_config() {
|
||||
echo -e "\n\n➡️ Please choose your configuration file :"
|
||||
local CONFIG_PATH="$(gum file)"
|
||||
|
||||
source "${CONFIG_PATH}"
|
||||
local MISSING=0
|
||||
for VAR in "${NECESSARY_VARIABLES_LIST[@]}"; do
|
||||
if [[ -v "${VAR}" && -n "${!VAR}" ]]; then
|
||||
gum style "✅ "${VAR}" imported successfully from the config file"
|
||||
else
|
||||
gum style "❌ "${VAR}" is missing or empty"
|
||||
MISSING=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${MISSING}" -eq 1 ]]; then
|
||||
echo -e "\n❌ Please check your configuration file to include all necessary variables"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${DEBUG:-false}" == "true" ]]; then
|
||||
echo -e "\n✅ Debugging enabled."
|
||||
export DIR_COPY_FLAGS="ravu"
|
||||
export FILES_COPY_FLAGS="avu"
|
||||
else
|
||||
export DIR_COPY_FLAGS="rau"
|
||||
export FILES_COPY_FLAGS="au"
|
||||
fi
|
||||
}
|
||||
|
||||
setup_ssh() {
|
||||
mkdir -p final-nix-config/
|
||||
mkdir -p final-nix-config/etc/
|
||||
mkdir -p final-nix-config/etc/nixos/
|
||||
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}"
|
||||
}
|
||||
|
||||
hierarchy_preparation() {
|
||||
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"
|
||||
}
|
||||
|
||||
hardware_detection() {
|
||||
### --> Get hardware information
|
||||
local TMPFILE="/tmp/nixos-installation-hardware-detection-temp-file"
|
||||
|
||||
ssh_to_host 'bash -s' << SSHEND
|
||||
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}")
|
||||
else
|
||||
TARGET_GRAPHICS="false"
|
||||
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
|
||||
|
||||
echo "# Hardware detection results on \$(date)" > "${TMPFILE}"
|
||||
for var in \
|
||||
TARGET_GRAPHICS \
|
||||
TARGET_GRAPHICS_BRAND \
|
||||
TARGET_GRAPHICS_RENDERER \
|
||||
TARGET_USB_CORAL \
|
||||
TARGET_PCIE_CORAL \
|
||||
TARGET_ZIGBEE_DEVICE \
|
||||
TARGET_INTERFACE \
|
||||
TARGET_TPM \
|
||||
TARGET_TPM_VERSION; do
|
||||
echo "export \${var}=\${!var}" >> "${TMPFILE}"
|
||||
done
|
||||
|
||||
for var in \
|
||||
DISK_DEVPATH \
|
||||
DISK_NAME \
|
||||
DISK_TYPE \
|
||||
DISK_HEALTH \
|
||||
DISK_ID \
|
||||
DISK_SIZE; do
|
||||
declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}"
|
||||
done
|
||||
SSHEND
|
||||
### Get hardware information <--
|
||||
|
||||
scp -i "final-nix-config/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
|
||||
source "${TMPFILE}" && rm -rf "${TMPFILE}"
|
||||
|
||||
### --> 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() {
|
||||
echo -e "\n\n ➡️ You will now select the services you want installed on your server:"
|
||||
|
||||
local AVAILABLE_DNS_SERVICES=( "pi-hole" "adguard" )
|
||||
|
||||
local AVAILABLE_SERVICES=( "frigate" "gitea" "home-assistant" "immich" "it-tools" \
|
||||
"nextcloud" "passbolt" "clamav" "virtualization" )
|
||||
|
||||
local DNS_SERVICES_DESCRIPTION=( "Pi-Hole : Simple open-source DNS black hole" \
|
||||
"AdGuard " : Feature rich DNS service )
|
||||
|
||||
local SERVICES_DESCRIPTION=( "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" \
|
||||
"ClamAV : An open-source anti-virus"
|
||||
"Virtualization : Run Virtual Machines (KVM/QEMU) with Libvirt" )
|
||||
|
||||
SELECTED_SERVICES=()
|
||||
local SELECTED_SERVICES_DESCRIPTION=$(gum choose --no-limit --header "Homelab services:" "${SERVICES_DESCRIPTION[@]}")
|
||||
|
||||
SELECTED_DNS_SERVICE=""
|
||||
local SELECTED_DNS_SERVICE_DESCRIPTION=$(gum choose --limit 1 --header "Homelab services:" "${DNS_SERVICES_DESCRIPTION[@]}")
|
||||
|
||||
for i in ${!AVAILABLE_SERVICES[@]}; do
|
||||
if printf '%s' "${SELECTED_SERVICES_DESCRIPTION}" | grep -iq "${AVAILABLE_SERVICES[${i}]}"; then
|
||||
SELECTED_SERVICES+=("${AVAILABLE_SERVICES[${i}]}")
|
||||
fi
|
||||
done
|
||||
|
||||
for i in ${!AVAILABLE_DNS_SERVICES[@]}; do
|
||||
if printf '%s' "${SELECTED_DNS_SERVICE_DESCRIPTION}" | grep -iq "${AVAILABLE_DNS_SERVICES[${i}]}"; then
|
||||
SELECTED_DNS_SERVICE="${AVAILABLE_DNS_SERVICES[${i}]}"
|
||||
fi
|
||||
done
|
||||
|
||||
export SELECTED_SERVICES
|
||||
export SELECTED_DNS_SERVICE
|
||||
|
||||
gum confirm "Do you want to edit the default subdomain of your services ?" || { echo -e "\n\n✅ Continuing..."; return 0; }
|
||||
|
||||
for service in ${!SELECTED_SERVICES[@]} $SELECTED_DNS_SERVICE; do
|
||||
local HEADER="Please provide the desired subdomain for ${service}:"
|
||||
local PLACEHOLDER="${service}"
|
||||
SELECTED_SERVICES_SUBDOMAIN+=("$(gum input --placeholder "${PLACEHOLDER}" --header "${HEADER}")")
|
||||
done
|
||||
|
||||
export SELECTED_SERVICES_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}\";" >> ${CONFIGURATION_PATH}
|
||||
echo -e " numbus.services.${SELECTED_DNS_SERVICE}.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_SERVICES[@]}"; do
|
||||
[[ "${SELECTED_SERVICES_SUBDOMAIN+x:-false}" ]] && echo -e " numbus.services.${service}.enable.subdomain = \"${SELECTED_SERVICES_SUBDOMAIN[${i}]}\";" >> ${CONFIGURATION_PATH}
|
||||
echo -e " numbus.services.${service}.enable = true;" >> ${CONFIGURATION_PATH}
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [[ "${SELECTED_SERVICES_SUBDOMAIN+x:-false}" && -n "$SELECTED_DNS_SERVICE" && -n "${SELECTED_SERVICES_SUBDOMAIN[${i}]}" ]]; then
|
||||
echo -e " numbus.services.${SELECTED_DNS_SERVICE}.enable.subdomain = \"${SELECTED_SERVICES_SUBDOMAIN[${i}]}\";" >> ${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]}\`" || echo "* **Boot 2:** *Not configured*" )
|
||||
|
||||
**Data Disks ($CONTENT_DISK_NUMBER):**
|
||||
$( 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:**
|
||||
$(echo "\`${SELECTED_DNS_SERVICE^}\`" && echo "")
|
||||
**Services:**
|
||||
$(for service in ${AVAILABLE_SERVICES[@]}`; do echo "\`${service^}\`" && echo ""; 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 )\`" || echo "**Boot 2:** *Not configured*" )
|
||||
|
||||
**Data Disks ($CONTENT_DISK_NUMBER):**
|
||||
$( [[ $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/data-${j} )\`" && 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}:** \`$( cat final-nix-config/etc/secrets/disks/parity-${j} )\`" && 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 "✅ 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..."
|
||||
|
||||
SELECTED_SERVICES_DNS=()
|
||||
for service in "${SELECTED_SERVICES[@]}"; do
|
||||
[[ "${service}" == "virtualization" ]] && continue
|
||||
[[ "${service}" == "clamav" ]] && continue
|
||||
[[ "${service}" == "nextcloud" ]] && SELECTED_SERVICES_DNS+=( "onlyoffice.${DOMAIN_NAME}" "whiteboard.${DOMAIN_NAME}" )
|
||||
SELECTED_SERVICES_DNS+=( "${service}.${DOMAIN_NAME}" )
|
||||
done
|
||||
SELECTED_SERVICES_DNS+=( "${SELECTED_DNS_SERVICE}.${DOMAIN_NAME}" )
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
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 !!"
|
||||
}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fastfetch --logo nixos --structure ' '
|
||||
|
||||
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")
|
||||
|
||||
if [[ "$ACTION_ANSWER" == "[1] 🌐 Deploy NixOS on a remote machine" ]]; then
|
||||
TARGET_USER="nixos"
|
||||
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; }
|
||||
strictly_necessary_information
|
||||
necessary_information
|
||||
setup_ssh
|
||||
hierarchy_preparation
|
||||
hardware_detection
|
||||
services_selection
|
||||
disks_selection
|
||||
server_config_generation
|
||||
network_config_generation
|
||||
services_config_generation
|
||||
mail_config_generation
|
||||
disk_config_generation
|
||||
keys_generation
|
||||
sum_up
|
||||
cloudflare_dns_setup
|
||||
export_configuration
|
||||
deploy
|
||||
postrun_action
|
||||
|
||||
elif [[ "$ACTION_ANSWER" == "[2] 💽 Deploy NixOS on a remote machine with a file configuration" ]]; then
|
||||
TARGET_USER="nixos"
|
||||
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_information_config
|
||||
setup_ssh
|
||||
hierarchy_preparation
|
||||
hardware_detection
|
||||
disks_selection
|
||||
server_config_generation
|
||||
network_config_generation
|
||||
services_config_generation
|
||||
mail_config_generation
|
||||
disk_config_generation
|
||||
keys_generation
|
||||
sum_up
|
||||
cloudflare_dns_setup
|
||||
export_configuration
|
||||
deploy
|
||||
postrun_action
|
||||
|
||||
elif [[ "$ACTION_ANSWER" == "[3] 🛠️ Update a NixOS remote machine" ]]; then
|
||||
TARGET_USER="numbus-admin"
|
||||
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; }
|
||||
strictly_necessary_information
|
||||
setup_ssh
|
||||
more_information_config
|
||||
folder_tree_generation
|
||||
nix_generation
|
||||
nix_update
|
||||
congrats
|
||||
|
||||
else
|
||||
echo "Aborting - you did not type 1, 2 or 3"
|
||||
exit 1
|
||||
fi
|
||||
|
After Width: | Height: | Size: 255 KiB |
@@ -0,0 +1,45 @@
|
||||
# Numbus documentation
|
||||
|
||||
<img title="Numbus Logo" src="./logo.png" alt="The Numbus logo" width="220">
|
||||
|
||||
## Deployment process overview
|
||||
|
||||
<img title="Overview of the deployment process" src="./deployment_overview.png" alt="A graph showing an overview of the deployment process" width="525">
|
||||
|
||||
## Table of contents
|
||||
|
||||
### [Numbus Server](https://gittea.dev/numbus/numbus-server)
|
||||
|
||||
* [Presentation](https://gittea.dev/numbus/numbus-server)
|
||||
* [Requirements](./numbus-server/requirements.md)
|
||||
* [Configuration](./numbus-server/configuration/index.md)
|
||||
* [Deployment](./numbus-server/deployment/index.md)
|
||||
* [Updates](./numbus-server/updates/index.md)
|
||||
* [Maintenance](./numbus-server/maintenance/index.md)
|
||||
|
||||
### [Numbus Backup Server](https://gittea.dev/numbus/numbus-backup-server)
|
||||
|
||||
* [Presentation](https://gittea.dev/numbus/numbus-backup-server)
|
||||
* [Requirements](./numbus-backup-server/requirements.md)
|
||||
* [Configuration](./numbus-backup-server/configuration/index.md)
|
||||
* [Deployment](./numbus-backup-server/deployment/index.md)
|
||||
* [Updates](./numbus-backup-server/updates/index.md)
|
||||
* [Maintenance](./numbus-backup-server/maintenance/index.md)
|
||||
|
||||
### [Numbus Computer](https://gittea.dev/numbus/numbus-computer)
|
||||
|
||||
* [Presentation](https://gittea.dev/numbus/numbus-computer)
|
||||
* [Requirements](./numbus-computer/requirements.md)
|
||||
* [Configuration](./numbus-computer/configuration/index.md)
|
||||
* [Deployment](./numbus-computer/deployment/index.md)
|
||||
* [Updates](./numbus-computer/updates/index.md)
|
||||
* [Maintenance](./numbus-computer/maintenance/index.md)
|
||||
|
||||
### [Numbus TV](https://gittea.dev/numbus/numbus-tv)
|
||||
|
||||
* [Presentation](https://gittea.dev/numbus/numbus-tv)
|
||||
* [Requirements](./numbus-tv/requirements.md)
|
||||
* [Configuration](./numbus-tv/configuration/index.md)
|
||||
* [Deployment](./numbus-tv/deployment/index.md)
|
||||
* [Updates](./numbus-tv/updates/index.md)
|
||||
* [Maintenance](./numbus-tv/maintenance/index.md)
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,31 @@
|
||||
# Requirements
|
||||
|
||||
### To deploy
|
||||
|
||||
To deploy a numbus-backup-server, you will need :
|
||||
|
||||
* A **live NixOS** bootable USB disk.
|
||||
|
||||
You will make the **target** machine **boot** into the NixOS live environment using this **USB stick**. Download the [NixOS iso](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso) image.
|
||||
|
||||
*On Linux* : Flash it using [Impression (flatpak)](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression) or [BalenaEtcher (AppImage)](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On MacOS* : Flash it using [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On Windows* : Flash it using [Rufus](https://rufus.ie/en/#download) or [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
* **Source** Machine:
|
||||
|
||||
Can be any machine with **Nix installed**, e.g. a **NixOS** machine.
|
||||
|
||||
* **Target** Machine:
|
||||
|
||||
Can be **any computer** (desktop, SFF, tiny/mini/micro, even a laptop). It could be a dedicated server that you bought or just some computer that you decided to repurpose into a backup server.
|
||||
|
||||
* **Network connection** between the source and the target machine.
|
||||
|
||||
---
|
||||
|
||||
### Next step
|
||||
|
||||
[Configuration](./configuration/index.md)
|
||||
@@ -0,0 +1,31 @@
|
||||
# Requirements
|
||||
|
||||
### To deploy
|
||||
|
||||
To deploy a numbus-backup-server, you will need :
|
||||
|
||||
* A **live NixOS** bootable USB disk.
|
||||
|
||||
You will make the **target** machine **boot** into the NixOS live environment using this **USB stick**. Download the [NixOS iso](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso) image.
|
||||
|
||||
*On Linux* : Flash it using [Impression (flatpak)](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression) or [BalenaEtcher (AppImage)](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On MacOS* : Flash it using [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On Windows* : Flash it using [Rufus](https://rufus.ie/en/#download) or [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
* **Source** Machine:
|
||||
|
||||
Can be any machine with **Nix installed**, e.g. a **NixOS** machine.
|
||||
|
||||
* **Target** Machine:
|
||||
|
||||
Can be **any computer** (desktop, SFF, tiny/mini/micro, even a laptop). It could be a dedicated server that you bought or just some computer that you decided to repurpose into a backup server.
|
||||
|
||||
* **Network connection** between the source and the target machine.
|
||||
|
||||
---
|
||||
|
||||
### Next step
|
||||
|
||||
[Configuration](./configuration/index.md)
|
||||
@@ -0,0 +1,19 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## ✏️ Filling the configuration file
|
||||
|
||||
You can deploy the numbus-server without using a configuration file, **but I would strongly advise it** as it diminishes the risk of **typos** when providing credentials. It also creates a file that you can **keep**, **reuse**, **consult** whenever you are **in doubt** about a certain setting.
|
||||
|
||||
> 🚀 Let's fill this configuration file !
|
||||
|
||||
The configuration file is divided into **multiple categories**. Some of them are **optional**, some are **mandatory**. Here is the list of categories :
|
||||
|
||||
| Category | Available variables |
|
||||
| -------- | ------------------ |
|
||||
| [Live target settings](./live_target.md) | 2 |
|
||||
| [Server settings](/.server.md) | 7 |
|
||||
| [Mail settings](./mail.md) | 4 |
|
||||
| [Traefik settings](./automatic_ssl_certs.md) | 1 |
|
||||
| [Network settings](./network.md) | 3 |
|
||||
| [Services selection](./services/index.md) | 5 |
|
||||
| [Script settings](./script.md) | 1 |
|
||||
@@ -0,0 +1,8 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
### 📦 Configuring the target settings
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------- | --------- |
|
||||
| LIVE_TARGET_IP | The IP address of the target machine. Without the CIDR notation. | "192.168.1.28", "10.10.10.45", ... | |
|
||||
| LIVE_TARGET_PASSWD | The password of the target machine that you set using the `passwd` command. | "my-password", ... | |
|
||||
@@ -0,0 +1,24 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 📬 Configuring the mail settings
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| SMTP_SERVER_USERNAME | The email address used to send automated emails (alerts, notifications). | "your-address@your-domain.com" | |
|
||||
| SMTP_SERVER_PASSWORD | The password or app-specific password for the email account. | "your-secure-password" | |
|
||||
| SMTP_SERVER_HOST | The address of the SMTP server. | "smtp.yourdomain.com" | "smtp.gmail.com" |
|
||||
| SMTP_SERVER_PORT | The port number for the SMTP server. | "587" or "465" | "587" |
|
||||
|
||||
## ❓ I don't already have an SMTP capable email
|
||||
|
||||
If your provider doesn't support **standard SMTP**, the easiest solution is to create a **free Gmail account** dedicated to your server.
|
||||
|
||||
Gmail allows you to generate an **App Password** which works perfectly for automated alerts.
|
||||
|
||||
1. Navigate to the [Google account creation page](https://accounts.google.com/signup). Follow the prompts to create your account. Feel free **not to provide** real information about yourself. Giving your phone number and home address is **not needed**.
|
||||
|
||||
2. Once your account is **created** navigate to the [Google account settings](https://myaccount.google.com).
|
||||
|
||||
3. In the security page, enable the **two factor authentication (2FA)**. The best way to enable 2FA without providing too much information is using **TOTP codes** (under Authenticator label). You can use the open-source [Ente Auth](https://ente.io/auth/) app to get your **TOTP codes**.
|
||||
|
||||
4. Finally, once **2FA** is enabled, you can generate an **app password**. Navigate to the [app passwords page](https://myaccount.google.com/apppasswords). Generate an app password and call it "numbus-server" to **remember** what it is used for.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 🛜 Configuring the server's network settings
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| NETWORK_SUBNET | The local network subnet in CIDR notation. | "192.168.1.0/24", "10.10.10.0/24", ... | "192.168.1.0/24" |
|
||||
| NETWORK_ROUTER_IP | The IP address of your internet router/gateway. | "192.168.1.1", "192.168.1.254", ... | "192.168.1.1" |
|
||||
| HOME_SERVER_IP | The static IP address to assign to this server (must be outside the router's DHCP range). | "192.168.1.5", "192.168.0.200", ... | "192.168.1.254" |
|
||||
@@ -0,0 +1,7 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 📜 Configuring the script's settings
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| VERBOSE | Enables verbose logging for the deployment script (useful for debugging). | "true" or "false" | "false" |
|
||||
@@ -0,0 +1,13 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## ⚙️ Configuring the server settings
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| SERVER_LANGUAGE | The desired language for the server's system and services. | "FR", "DE", "UK", ... | "FR" |
|
||||
| SERVER_LOCALE | Defines the server's regional settings, including date, time, and number formatting. | "fr_FR", "de_DE", "en_UK", ... | "fr_FR" |
|
||||
| SERVER_TIMEZONE | Sets the server's timezone to ensure accurate timekeeping for logs and services. | "Europe/Paris", "Europe/Berlin", "Europe/London", ... | "Europe/Paris" |
|
||||
| SERVER_OWNER_NAME | The name of the server owner. Will be used for emails personnalization and server identification (if you have multiple servers for example). | "Alexandre", "Jane", ... | "Numbus" |
|
||||
| SERVER_USER_EMAIL | This email will be used to send friendly alerts in case of problems. | "your-user@your-domain.com" | |
|
||||
| SERVER_ADMIN_EMAIL | This email will be used to send complete alerts, including some logs, in case of problems. | "your-admin@your-domain.com" | |
|
||||
| SERVER_AUTHORIZED_SSH_PUBKEYS | The public SSH key(s) that will be authorized to access the server. You can add multiple keys in the parenthesis, space-separated. | ( "ssh-ed25519 AAAAoefzefpoipoeCEZJCPEACPAcjapjcpajepcjAPJECJPEJAPJAZ yours@your-domain.com" ) | |
|
||||
@@ -0,0 +1,7 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 🚦 Configuring Traefik
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| CLOUDFLARE_DNS_API_TOKEN | The Cloudflare API token with "Zone.DNS" permissions, used for SSL certificate generation. | "bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" | null. Not setting this variable will cause the script to fail. |
|
||||
@@ -0,0 +1,13 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## ⛏️ Configuring Crafty Controller
|
||||
|
||||
To access your Minecraft server, **ports** need to be **open on the firewall**. In order to **minimize** the number of open ports and thus reduce **risks**, the numbus-server will only open a necessary number of ports that matches the number of minecraft. **Most people** should set the number of servers to **1** of their favorite version.
|
||||
|
||||
These option **only configure networking** and **won't** create the Minecraft server for you. You will have to do it in Crafty's interface.
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| DYNMAP_ENABLED | Enables the Dynmap feature for Minecraft servers. | "true" or "false" | "false" |
|
||||
| WANTED_NUMBER_OF_JAVA_MINECRAFT_SERVERS | The number of Java Edition Minecraft servers you plan on creating. | "0", "1", ..., "100" | "0" |
|
||||
| WANTED_NUMBER_OF_BEDROCK_MINECRAFT_SERVERS | The number of Bedrock Edition Minecraft servers you plan on creating. | "0", "1", ..., "100" | "0" |
|
||||
@@ -0,0 +1,34 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 🌍 Using custom subdomains
|
||||
|
||||
**By default**, the name of the service will be the **subdomain**. For example, nextcloud will get the "nextcloud" subdomain. You **should change** those subdomains :
|
||||
- If you are using the **same domain name** for **multiple servers** (avoids conflicts)
|
||||
- If you just **want** to change them
|
||||
|
||||
<br>
|
||||
|
||||
> To change the subdomain of a service, just create a variable in the **.conf** configuration file like so : **<THE_SERVICE>_SUBDOMAIN="your_custom_subdomain".** If you are you are unsure of the correct service name, check the list below.
|
||||
|
||||
## Variables list
|
||||
|
||||
| Variable | Default |
|
||||
| --------- | ------ |
|
||||
| PI_HOLE_SUBDOMAIN | pi-hole |
|
||||
| ADGUARD_SUBDOMAIN | adguard |
|
||||
| CRAFFY_SUBDOMAIN | crafty |
|
||||
| FRIGATE_SUBDOMAIN | frigate |
|
||||
| GITEA_SUBDOMAIN | gitea |
|
||||
| HOME_ASSISTANT_SUBDOMAIN | home-assistant |
|
||||
| HOMEPAGE_SUBDOMAIN | homepage |
|
||||
| IMMICH_SUBDOMAIN | immich |
|
||||
| IT_TOOLS_SUBDOMAIN | it-tools |
|
||||
| JELLYFIN_SUBDOMAIN | jellyfin |
|
||||
| N8N_SUBDOMAIN | n8n |
|
||||
| NETBOOTXYZ_SUBDOMAIN | netbootxyz |
|
||||
| NEXTCLOUD_SUBDOMAIN | nextcloud |
|
||||
| NTFY_SUBDOMAIN | ntfy |
|
||||
| ODOO_SUBDOMAIN | odoo |
|
||||
| PASSBOLT_SUBDOMAIN | passbolt |
|
||||
| UPTIME_KUMA_SUBDOMAIN | uptime-kuma |
|
||||
| VSCODIUM_SUBDOMAIN | vscodium |
|
||||
@@ -0,0 +1,43 @@
|
||||
# Numbus Server documentation
|
||||
|
||||
## 🛠️ Configuring the server's services
|
||||
|
||||
| Variable | Description | Values | Default |
|
||||
| -------- | ----------- | ------ | --------- |
|
||||
| DOMAIN_NAME | The domain name that will be used to access the different services. | your-domain.com | |
|
||||
| SELECTED_DNS_SERVICE | The DNS service to install (AdBlocking). | ( "pi-hole" ), ( "adguard" ) | ( "pi-hole" ) |
|
||||
| SELECTED_WEB_APPLICATIONS | The list of web applications to install. | ( "nextcloud" ), ( "homepage" jellyfin" "it-tools" "netbootxyz" ), ... [see the full list below](./index.md#web-applications-list) | |
|
||||
| SELECTED_SYSTEM_SERVICES | The list of system services to install. | ( "clamav" ), ( "virtualization" "clamav" ), ... [see the full list below](./index.md#system-services-list) | |
|
||||
|
||||
|
||||
## Web applications list
|
||||
|
||||
This is the list of **all the available apps** that can be enabled on the numbus-server. You can choose as many as you want, just **keep in mind** that enabling more apps will be more resource **intensive** and consume **more power**.
|
||||
|
||||
| Name | Description | Additional settings |
|
||||
| -------- | ----------- | ------ |
|
||||
| pi-hole | Simple, fully open network-wide Ad Blocker. | [subdomain](../custom_subdomain.md) |
|
||||
| adguard | Feature-rich network-wide Ad Blocker. | [subdomain](../custom_subdomain.md) |
|
||||
| crafty | Minecraft server(s) manager. | [subdomain](../custom_subdomain.md), [settings](./crafty.md) |
|
||||
| frigate | NVR with real-time local object detection. | [subdomain](../custom_subdomain.md) |
|
||||
| gitea | Painless self-hosted Git service. | [subdomain](../custom_subdomain.md) |
|
||||
| home-assistant | Open source home automation. | [subdomain](../custom_subdomain.md) |
|
||||
| homepage | A modern, secure, highly customizable dashboard. | [subdomain](../custom_subdomain.md) |
|
||||
| immich | High performance self-hosted photo/video management. | [subdomain](../custom_subdomain.md) |
|
||||
| it-tools | Collection of online tools for developers. | [subdomain](../custom_subdomain.md) |
|
||||
| jellyfin | The Free Software Media System. | [subdomain](../custom_subdomain.md) |
|
||||
| n8n | Workflow automation tool. | [subdomain](../custom_subdomain.md) |
|
||||
| netbootxyz | Network boot operating system installers. | [subdomain](../custom_subdomain.md) |
|
||||
| nextcloud | Self-hosted collaboration platform. | [subdomain](../custom_subdomain.md) |
|
||||
| ntfy | Send push notifications via HTTP. | [subdomain](../custom_subdomain.md) |
|
||||
| odoo | Open Source ERP and CRM. | [subdomain](../custom_subdomain.md) |
|
||||
| passbolt | Open source password manager. | [subdomain](../custom_subdomain.md) |
|
||||
| uptime-kuma | Self-hosted monitoring tool. | [subdomain](../custom_subdomain.md) |
|
||||
| vscodium | VS Code in your browser. | [subdomain](../custom_subdomain.md) |
|
||||
|
||||
## System services list
|
||||
|
||||
| Name | Description | Additional settings |
|
||||
| -------- | ----------- | ------ |
|
||||
| clamav | Open-source anti-virus software. | |
|
||||
| virtualization | Run Virtual Machines (KVM/QEMU). | |
|
||||
@@ -0,0 +1,31 @@
|
||||
# Requirements
|
||||
|
||||
### To deploy
|
||||
|
||||
To deploy a numbus-backup-server, you will need :
|
||||
|
||||
* A **live NixOS** bootable USB disk.
|
||||
|
||||
You will make the **target** machine **boot** into the NixOS live environment using this **USB stick**. Download the [NixOS iso](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso) image.
|
||||
|
||||
*On Linux* : Flash it using [Impression (flatpak)](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression) or [BalenaEtcher (AppImage)](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On MacOS* : Flash it using [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On Windows* : Flash it using [Rufus](https://rufus.ie/en/#download) or [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
* **Source** Machine:
|
||||
|
||||
Can be any machine with **Nix installed**, e.g. a **NixOS** machine.
|
||||
|
||||
* **Target** Machine:
|
||||
|
||||
Can be **any computer** (desktop, SFF, tiny/mini/micro, even a laptop). It could be a dedicated server that you bought or just some computer that you decided to repurpose into a backup server.
|
||||
|
||||
* **Network connection** between the source and the target machine.
|
||||
|
||||
---
|
||||
|
||||
### Next step
|
||||
|
||||
[Configuration](./configuration/index.md)
|
||||
@@ -0,0 +1,31 @@
|
||||
# Requirements
|
||||
|
||||
### To deploy
|
||||
|
||||
To deploy a numbus-backup-server, you will need :
|
||||
|
||||
* A **live NixOS** bootable USB disk.
|
||||
|
||||
You will make the **target** machine **boot** into the NixOS live environment using this **USB stick**. Download the [NixOS iso](https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso) image.
|
||||
|
||||
*On Linux* : Flash it using [Impression (flatpak)](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression) or [BalenaEtcher (AppImage)](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On MacOS* : Flash it using [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
*On Windows* : Flash it using [Rufus](https://rufus.ie/en/#download) or [BalenaEtcher](https://etcher.balena.io/#download-etcher).
|
||||
|
||||
* **Source** Machine:
|
||||
|
||||
Can be any machine with **Nix installed**, e.g. a **NixOS** machine.
|
||||
|
||||
* **Target** Machine:
|
||||
|
||||
Can be **any computer** (desktop, SFF, tiny/mini/micro, even a laptop). It could be a dedicated server that you bought or just some computer that you decided to repurpose into a backup server.
|
||||
|
||||
* **Network connection** between the source and the target machine.
|
||||
|
||||
---
|
||||
|
||||
### Next step
|
||||
|
||||
[Configuration](./configuration/index.md)
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
description = "Numbus - Simplified NixOS deployments";
|
||||
|
||||
inputs = {
|
||||
# Nix unstable packages
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
# Nix stable packages
|
||||
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-25.11";
|
||||
# Numbus
|
||||
numbus.url = "https://gittea.dev/numbus/numbus";
|
||||
# Disko
|
||||
disko.url = "github:nix-community/disko";
|
||||
disko.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Sops-nix
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Flatpaks
|
||||
nix-flatpak.url = "github:gmodena/nix-flatpak";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, ... }@inputs:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Helper for defining systems
|
||||
mkNumbus = { deviceModule, extraModules ? [], nixpkgsRef, deviceType }: nixpkgsRef.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs deviceType; };
|
||||
modules = [
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
self.nixosModules.common
|
||||
deviceModule
|
||||
] ++ extraModules;
|
||||
};
|
||||
in {
|
||||
nixosModules = {
|
||||
common = ./modules/common/default.nix;
|
||||
server = ./modules/server/default.nix;
|
||||
backup = ./modules/backup/default.nix;
|
||||
console = ./modules/console/default.nix;
|
||||
computer = ./modules/computer/default.nix;
|
||||
tv = ./modules/tv/default.nix;
|
||||
};
|
||||
|
||||
nixosConfigurations = {
|
||||
numbus-server = mkNumbus {
|
||||
deviceModule = self.nixosModules.server;
|
||||
nixpkgsRef = inputs.nixpkgs-stable;
|
||||
deviceType = "server";
|
||||
};
|
||||
|
||||
numbus-backup = mkNumbus {
|
||||
deviceModule = self.nixosModules.backup;
|
||||
nixpkgsRef = inputs.nixpkgs-stable;
|
||||
deviceType = "backup";
|
||||
};
|
||||
|
||||
numbus-computer = mkNumbus {
|
||||
deviceModule = self.nixosModules.computer;
|
||||
extraModules = [ inputs.nix-flatpak.nixosModules.nix-flatpak ];
|
||||
nixpkgsRef = inputs.nixpkgs;
|
||||
deviceType = "computer";
|
||||
};
|
||||
|
||||
numbus-console = mkNumbus {
|
||||
deviceModule = self.nixosModules.console;
|
||||
nixpkgsRef = inputs.nixpkgs;
|
||||
deviceType = "console";
|
||||
};
|
||||
|
||||
numbus-tv = mkNumbus {
|
||||
deviceModule = self.nixosModules.tv;
|
||||
nixpkgsRef = inputs.nixpkgs;
|
||||
extraModules = [ inputs.nix-flatpak.nixosModules.nix-flatpak ];
|
||||
deviceType = "tv";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./hardware/default.nix
|
||||
./mail/default.nix
|
||||
./misc/default.nix
|
||||
./networking/default.nix
|
||||
./packages/default.nix
|
||||
./services/default.nix
|
||||
./global.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
description = "Numbus Server Module";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }: {
|
||||
nixosModules.numbus = import ./modules/default.nix;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
options.numbus = {
|
||||
owner = mkOption {
|
||||
type = types.str;
|
||||
example = "Alex";
|
||||
default = "Numbus";
|
||||
description = "The name of the person who owns this server";
|
||||
};
|
||||
language = mkOption {
|
||||
type = types.str;
|
||||
example = "FR";
|
||||
default = "FR";
|
||||
description = "The language for this server";
|
||||
};
|
||||
locale = mkOption {
|
||||
type = types.str;
|
||||
example = "fr_FR";
|
||||
default = "fr_FR";
|
||||
description = "The default locale for this server";
|
||||
};
|
||||
|
||||
services = {
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
example = "numbus.eu";
|
||||
description = "The root domain name (i.e. example.com) that your services will use";
|
||||
};
|
||||
dns = mkOption {
|
||||
type = types.enum [ "pi-hole" "adguard" ];
|
||||
default = "pi-hole";
|
||||
example = "pi-hole";
|
||||
description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on";
|
||||
};
|
||||
};
|
||||
|
||||
email = {
|
||||
administratorEmail = mkOption {
|
||||
type = types.str;
|
||||
example = "admin@your-domain.com";
|
||||
description = "The email that will be used to send critical notifications such as hardware failures, services errors, ACME updates, etc";
|
||||
};
|
||||
userEmail = mkOption {
|
||||
type = types.str;
|
||||
example = "user@your-domain.com";
|
||||
description = "The email that will be used by services to send notifications";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./firewall.nix
|
||||
./networking.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
networking.nftables.enable = true;
|
||||
networking.firewall = {
|
||||
enable = true;
|
||||
allowPing = true;
|
||||
allowedTCPPorts = [ 53 80 443 ];
|
||||
allowedUDPPorts = [ 53 443 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.networking;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.networking = {
|
||||
ipAddress = mkOption {
|
||||
description = "The IP address that this server will use";
|
||||
type = types.str;
|
||||
example = "192.168.1.100";
|
||||
};
|
||||
interface = mkOption {
|
||||
description = "The interface that this server will use to connect to the network";
|
||||
type = types.str;
|
||||
example = "enp1s0";
|
||||
};
|
||||
routerIpAddress = mkOption {
|
||||
description = "The IP address of the router of your network";
|
||||
type = types.str;
|
||||
example = "192.168.1.1";
|
||||
};
|
||||
networkSubnet = mkOption {
|
||||
description = "The subnet of your network";
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "192.168.1.0/24";
|
||||
};
|
||||
dnsServers = mkOption {
|
||||
description = "The list of DNS servers that this server will use";
|
||||
type = types.listOf types.str;
|
||||
default = [ "${cfg.ipAddress}" "9.9.9.9" ];
|
||||
example = [ "${cfg.ipAddress}" "9.9.9.9" ];
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
networking.hostName = "numbus-server";
|
||||
networking.networkmanager.enable = false;
|
||||
|
||||
# Allow rootless containers to bind to port 53 and up
|
||||
boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53;
|
||||
|
||||
networking.bridges.br0.interfaces = [ "${cfg.interface}" ];
|
||||
networking.interfaces."${cfg.interface}".useDHCP = false;
|
||||
networking.interfaces.br0.useDHCP = false;
|
||||
networking.nameservers = cfg.dnsServers;
|
||||
networking.interfaces.br0.ipv4.addresses = [{
|
||||
address = "${cfg.ipAddress}";
|
||||
prefixLength = 24;
|
||||
}];
|
||||
networking.defaultGateway = {
|
||||
address = "${cfg.routerIpAddress}";
|
||||
interface = "br0";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./packages.nix
|
||||
./podman.nix
|
||||
./ssh.nix
|
||||
./terminal.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
git
|
||||
ncdu
|
||||
fastfetch
|
||||
tpm2-tss
|
||||
sops
|
||||
age
|
||||
powertop
|
||||
pciutils
|
||||
hdparm
|
||||
hd-idle
|
||||
hddtemp
|
||||
smartmontools
|
||||
cpufrequtils
|
||||
intel-gpu-tools
|
||||
snapraid
|
||||
mergerfs
|
||||
mergerfs-tools
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
virtualisation.podman.enable = true;
|
||||
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
|
||||
|
||||
virtualisation.containers.containersConf.settings = {
|
||||
network.default_rootless_network_cmd = "slirp4netns";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
podman-compose
|
||||
podman-tui
|
||||
slirp4netns
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
services.openssh.enable = true;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
fish
|
||||
fishPlugins.fzf-fish
|
||||
fishPlugins.grc
|
||||
grc
|
||||
fzf
|
||||
];
|
||||
|
||||
programs.fish = {
|
||||
enable = true;
|
||||
interactiveShellInit = ''
|
||||
set fish_greeting # Disable greeting
|
||||
fastfetch
|
||||
echo -e "\n\nWelcome to your Numbus-Server !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support if you can't get your server running\n- Have a nice day and enjoy !"
|
||||
'';
|
||||
shellAliases = {
|
||||
nixup = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch --upgrade && cd -";
|
||||
nixwitch = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch && cd -";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "authelia";
|
||||
# Version tagging
|
||||
autheliaVersion = "v4.39.16";
|
||||
databaseVersion = "18.3";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.authelia;
|
||||
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
|
||||
domainParts = splitString "." config.numbus-server.services.domain;
|
||||
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
|
||||
# Generate dynamic access control rules based on groups and allowedApps
|
||||
mkGroupRule = groupName: appName:
|
||||
let
|
||||
app = config.numbus-server.service.${appName} or {};
|
||||
in
|
||||
if app ? subdomain && app ? domain then ''
|
||||
- domain: "${app.subdomain}.${app.domain}"
|
||||
policy: two_factor
|
||||
subject: "group:${groupName}"''
|
||||
else "";
|
||||
allGroupRules = concatStringsSep "\n" (filter (s: s != "") (flatten (mapAttrsToList (groupName: groupCfg:
|
||||
map (appName: mkGroupRule groupName appName) (groupCfg.allowedApps or [])
|
||||
) (config.numbus-server.groups or {}))));
|
||||
|
||||
defaultRedirectionUrl =
|
||||
if config.numbus-server.services.homepage.enable then
|
||||
"https://${config.numbus-server.services.homepage.subdomain}.${config.numbus-server.services.domain}"
|
||||
else if config.numbus-server.services.dashy.enable then
|
||||
"https://${config.numbus-server.services.dashy.subdomain}.${config.numbus-server.services.domain}"
|
||||
else null;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = name;
|
||||
description = "Authelia, your own unified login provider";
|
||||
defaultPort = "9091";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"authelia/db_name"
|
||||
"authelia/db_username"
|
||||
"authelia/db_password"
|
||||
"authelia/jwt_secret"
|
||||
"authelia/session_secret"
|
||||
"authelia/storage_secret"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
authelia-server:
|
||||
image: ghcr.io/authelia/authelia:${autheliaVersion}
|
||||
container_name: authelia-server
|
||||
hostname: authelia-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
authelia:
|
||||
ipv4_address: 10.89.251.253
|
||||
ports:
|
||||
- "${cfg.port}:9091/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/server:/config
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
authelia-database:
|
||||
container_name: authelia-database
|
||||
hostname: authelia-database
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
authelia:
|
||||
ipv4_address: 10.89.251.252
|
||||
environment:
|
||||
POSTGRES_DB: ${config.sops.placeholder."authelia/db_name"}
|
||||
POSTGRES_USER: ${config.sops.placeholder."authelia/db_username"}
|
||||
POSTGRES_PASSWORD: ${config.sops.placeholder."authelia/db_password"}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
authelia:
|
||||
driver: bridge
|
||||
name: authelia
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.251.0/24"
|
||||
gateway: "10.89.251.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."authelia-config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
authelia:
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: "${config.sops.placeholder."authelia/jwt_secret"}"
|
||||
jwt_lifespan: "5 minutes"
|
||||
jwt_algorithm: "HS256"
|
||||
storage:
|
||||
encryption_key: "${config.sops.placeholder."authelia/storage_secret"}"
|
||||
postgres:
|
||||
address: "tcp://authelia-database:5432"
|
||||
database: "${config.sops.placeholder."authelia/db_name"}"
|
||||
username: "${config.sops.placeholder."authelia/db_username"}"
|
||||
password: "${config.sops.placeholder."authelia/db_password"}"
|
||||
session:
|
||||
secret: "${config.sops.placeholder."authelia/session_secret"}"
|
||||
cookies:
|
||||
- domain: "${config.numbus-server.services.domain}"
|
||||
authelia_url: "https://${cfg.subdomain}.${config.numbus-server.services.domain}"
|
||||
${optionalString (defaultRedirectionUrl != null) "default_redirection_url: \"${defaultRedirectionUrl}\""}
|
||||
authentication_backend:
|
||||
ldap:
|
||||
implementation: "lldap"
|
||||
address: "ldap://host.containers.internal:3890"
|
||||
base_dn: "${baseDN}"
|
||||
user: "UID=authelia,OU=people,${baseDN}"
|
||||
password: "${config.sops.placeholder."lldap/"}"
|
||||
notifier:
|
||||
smtp:
|
||||
address: submission://${config.numbus-server.mail.smtpHost}:${config.numbus-server.mail.smtpPort}
|
||||
username: ${config.numbus-server.mail.smtpUsername}
|
||||
password: ${config.sops.placeholder.smtpPassword}
|
||||
sender: ${config.numbus-server.mail.fromAddress}
|
||||
tls:
|
||||
server_name: ${config.numbus-server.mail.smtpHost}
|
||||
minimum_version: TLS1.2
|
||||
skip_verify: false
|
||||
access_control:
|
||||
default_policy: 'deny'
|
||||
rules:
|
||||
- domain: "*.${config.numbus-server.service.domain}"
|
||||
policy: two_factor
|
||||
subject: "group:admin"
|
||||
${allGroupRules}
|
||||
'';
|
||||
path = "/etc/authelia/authelia.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "crafty";
|
||||
# Version tagging
|
||||
craftyVersion = "v4.10.1";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.crafty;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Crafty controller, one place to manage your minecraft servers";
|
||||
defaultPort = "8443";
|
||||
scheme = "https";
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/log"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${optimizedDir}/import"
|
||||
"100999:100 ${optimizedDir}/backups"
|
||||
"100999:100 ${optimizedDir}/servers"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
crafty:
|
||||
image: registry.gitlab.com/crafty-controller/crafty-4:${craftyVersion}
|
||||
container_name: crafty
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
crafty:
|
||||
ipv4_address: 10.89.250.253
|
||||
ports:
|
||||
- "${cfg.port}:8443/tcp"
|
||||
- "19132:19132/udp"
|
||||
- "25500-25600:25500-25600"
|
||||
volumes:
|
||||
- ${optimizedDir}/backups:/crafty/backups
|
||||
- ${optimizedDir}/servers:/crafty/servers
|
||||
- ${optimizedDir}/import:/crafty/import
|
||||
- ${cfg.configDir}/logs:/crafty/logs
|
||||
- ${cfg.configDir}/config:/crafty/app/config
|
||||
environment:
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
crafty:
|
||||
driver: bridge
|
||||
name: crafty
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.250.0/24"
|
||||
gateway: "10.89.250.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "dashy";
|
||||
# Version tagging
|
||||
dashyVersion = "v3.2.3";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.dashy;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Dashy, the ultimate dashboard for your homelab";
|
||||
defaultPort = "8999";
|
||||
configDirEnabled = false;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
dashy:
|
||||
image: lissy93/dashy:${dashyVersion}
|
||||
container_name: dashy
|
||||
hostname: dashy
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
dashy:
|
||||
ipv4_address: 10.89.235.253
|
||||
ports:
|
||||
- ${cfg.port}:8080
|
||||
volumes:
|
||||
- ${config.sops."dashy/config".path}:/app/user-data/conf.yml
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- NODE_ENV=production
|
||||
healthcheck:
|
||||
test: ['CMD', 'node', '/app/services/healthcheck']
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
dashy:
|
||||
driver: bridge
|
||||
name: dashy
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.235.0/24"
|
||||
gateway: "10.89.235.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."dashy/config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0440" ;
|
||||
content = ''
|
||||
pageInfo:
|
||||
title: My Homelab
|
||||
sections:
|
||||
- name: Example Section
|
||||
icon: far fa-rocket
|
||||
items:
|
||||
- title: GitHub
|
||||
description: Dashy source code and docs
|
||||
icon: fab fa-github
|
||||
url: https://github.com/Lissy93/dashy
|
||||
- title: Issues
|
||||
description: View open issues, or raise a new one
|
||||
icon: fas fa-bug
|
||||
url: https://github.com/Lissy93/dashy/issues
|
||||
- name: Local Services
|
||||
items:
|
||||
- title: Firewall
|
||||
icon: favicon
|
||||
url: http://192.168.1.1/
|
||||
- title: Game Server
|
||||
icon: https://i.ibb.co/710B3Yc/space-invader-x256.png
|
||||
url: http://192.168.130.1/
|
||||
'';
|
||||
path = "/etc/dashy/dashy.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# Good
|
||||
./gitea.nix
|
||||
./immich.nix
|
||||
./nextcloud.nix
|
||||
./passbolt.nix
|
||||
./traefik.nix
|
||||
# Testing needed
|
||||
./authelia.nix
|
||||
./crafty.nix
|
||||
./dashy.nix
|
||||
./frigate.nix
|
||||
./home-assistant.nix
|
||||
./homepage.nix
|
||||
./it-tools.nix
|
||||
./jellyfin.nix
|
||||
./lldap.nix
|
||||
./n8n.nix
|
||||
./netbird.nix
|
||||
./netbootxyz.nix
|
||||
./ntfy.nix
|
||||
./odoo.nix
|
||||
./uptime-kuma.nix
|
||||
./vscodium.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "frigate";
|
||||
# Version tagging
|
||||
frigateVersion = "0.16.4";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.frigate;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "home-assistant";
|
||||
description = "Frigate, your fully-local NVR (Network Video Recorder)";
|
||||
defaultPort = "8971";
|
||||
scheme = "https";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"authelia.service"
|
||||
"home-assistant.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"1000:100 ${cfg.configDir}"
|
||||
"1000:100 ${cfg.dataDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
frigate:
|
||||
image: ghcr.io/blakeblackshear/frigate:${frigateVersion}
|
||||
container_name: frigate
|
||||
hostname: frigate
|
||||
shm_size: "256mb"
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.253
|
||||
ports:
|
||||
- "${cfg.port}:8971/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/config
|
||||
- ${cfg.dataDir}:/media/frigate
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- type: tmpfs
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
environment:
|
||||
- FRIGATE_MQTT_USER=${config.sops.placeholder."home-assistant/mqtt_username"}
|
||||
- FRIGATE_MQTT_PASSWORD=${config.sops.placeholder."home-assistant/mqtt_password"}
|
||||
${lib.optionalString (cfg.devices != []) ''
|
||||
devices:
|
||||
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
|
||||
''}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
stop_grace_period: 30s
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
home-assistant:
|
||||
external: true
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ];
|
||||
description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "gitea";
|
||||
# Version tagging
|
||||
giteaVersion = "1.25.4-rootless";
|
||||
databaseVersion = "18-alpine";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.gitea;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "Gitea, your own self-hosted git platform";
|
||||
defaultPort = "3000";
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}/data"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
secrets = [
|
||||
"gitea/db_name"
|
||||
"gitea/db_username"
|
||||
"gitea/db_password"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
gitea-database:
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
container_name: gitea-database
|
||||
hostname: gitea-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
gitea:
|
||||
ipv4_address: 10.89.240.253
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=${config.sops.placeholder."gitea/db_name"}
|
||||
- POSTGRES_USER=${config.sops.placeholder."gitea/db_username"}
|
||||
- POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-server:
|
||||
image: docker.gitea.com/gitea:${giteaVersion}
|
||||
container_name: gitea-server
|
||||
hostname: gitea-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
gitea:
|
||||
ipv4_address: 10.89.240.252
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}/data:/var/lib/gitea
|
||||
- ${cfg.configDir}/config:/etc/gitea
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-database:5432
|
||||
- GITEA__database__NAME=${config.sops.placeholder."gitea/db_name"}
|
||||
- GITEA__database__USER=${config.sops.placeholder."gitea/db_username"}
|
||||
- GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"}
|
||||
- GITEA__server__SSH_PORT=2424
|
||||
- GITEA__server__ROOT_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
depends_on:
|
||||
- gitea-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
gitea:
|
||||
driver: bridge
|
||||
name: gitea
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.240.0/24"
|
||||
gateway: "10.89.240.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "home-assistant";
|
||||
# Version tagging
|
||||
homeAssistantVersion = "2026.2.3";
|
||||
mqttVersion = "2.1-alpine";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.home-assistant;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Home Assistant, libre house control and much more";
|
||||
defaultPort = "8123";
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"1000:100 ${cfg.configDir}"
|
||||
"1000:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/mqtt"
|
||||
];
|
||||
secrets = [
|
||||
"home-assistant/mqtt_user"
|
||||
"home-assistant/mqtt_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
home-assistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:${homeAssistantVersion}
|
||||
container_name: home-assistant
|
||||
hostname: home-assistant
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.252
|
||||
ports:
|
||||
- "${cfg.port}:8123/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /run/dbus:/run/dbus:ro
|
||||
${lib.optionalString (cfg.devices != []) ''
|
||||
devices:
|
||||
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
|
||||
''}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
home-assistant-mqtt:
|
||||
image: docker.io/library/eclipse-mosquitto:${mqttVersion}
|
||||
container_name: home-assistant-mqtt
|
||||
hostname: home-assistant-mqtt
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/mqtt:/mosquitto
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
home-assistant:
|
||||
driver: bridge
|
||||
name: home-assistant
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.230.0/24"
|
||||
gateway: "10.89.230.254"
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "/dev/serial/by-id/Sonoff_Zigbee_3.0-id-port0:/dev/ttyUSB0" ];
|
||||
description = "List of devices to map into the container. /dev/ttyUSB0 is used for Zigbee dongles";
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = {
|
||||
systemd.services."${name}-quirk" = {
|
||||
description = "Podman container quirk : ${name}";
|
||||
after = [ "${name}.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.systemd ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
mkdir -p /var/lib/numbus-server/${name}
|
||||
if [[ -e ${cfg.configDir}/config/configuration.yaml ]]; then
|
||||
if grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
|
||||
exit 0
|
||||
elif grep -qF "use_x_forwarded_for" ${cfg.configDir}/config/configuration.yaml && ! grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
|
||||
tmp=$(mktemp)
|
||||
head -n -6 ${cfg.configDir}/config/configuration.yaml > "$tmp"
|
||||
mv "$tmp" ${cfg.configDir}/config/configuration.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
until [[ -e ${cfg.configDir}/config/configuration.yaml ]]; do
|
||||
sleep 15
|
||||
done
|
||||
cat << 'EOF' >> ${cfg.configDir}/config/configuration.yaml
|
||||
|
||||
http:
|
||||
use_x_forwarded_for: true
|
||||
trusted_proxies: 10.89.230.1
|
||||
|
||||
zha:
|
||||
EOF
|
||||
|
||||
systemctl restart ${name}.service
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."mqtt-quirk" = {
|
||||
description = "Podman container quirk : Home-assistant MQTT";
|
||||
after = [ "sops-install-secrets.service" ];
|
||||
before = [ "${name}.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.mosquitto ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
if [[ -e ${cfg.configDir}/mqtt/mosquitto.conf && ${cfg.configDir}/mqtt/password.txt ]]; then
|
||||
if grep -qF "listener 1883" ${cfg.configDir}/mqtt/mosquitto.conf; then
|
||||
exit 0
|
||||
else
|
||||
rm ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
rm ${cfg.configDir}/mqtt/password.txt
|
||||
touch ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
touch ${cfg.configDir}/mqtt/password.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
cat << EOF >> ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
persistence true
|
||||
persistence_location /mosquitto/data/
|
||||
log_dest file /mosquitto/log/mosquitto.log
|
||||
listener 1883
|
||||
## Authentication ##
|
||||
allow_anonymous false
|
||||
password_file /mosquitto/password.txt
|
||||
EOF
|
||||
|
||||
HOME_ASSISTANT_MQTT_USER=$(cat /run/secrets/home-assistant/mqtt_user)
|
||||
HOME_ASSISTANT_MQTT_PASSWORD=$(cat /run/secrets/home-assistant/mqtt_password)
|
||||
|
||||
mosquitto_passwd -b ${cfg.configDir}/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD"
|
||||
chmod 0400 ${cfg.configDir}/mqtt/password.txt
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "homepage";
|
||||
# Version tagging
|
||||
homepageVersion = "v1.10.1";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.homepage;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Homepage, a modern and highly customizable application dashboard";
|
||||
defaultPort = "3003";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/images"
|
||||
"100999:100 ${cfg.configDir}/icons"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
homepage:
|
||||
image: ghcr.io/gethomepage/homepage:${homepageVersion}
|
||||
container_name: homepage
|
||||
hostname: homepage
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
homepage:
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/app/config
|
||||
- ${cfg.configDir}/images:/app/public/images
|
||||
- ${cfg.configDir}/icons:/app/public/icons
|
||||
environment:
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
HOMEPAGE_ALLOWED_HOSTS: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
homepage:
|
||||
driver: bridge
|
||||
name: homepage
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.220.0/24"
|
||||
gateway: "10.89.220.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container configuration
|
||||
name = "immich";
|
||||
# Version tagging
|
||||
immichVersion = "v2.5.6";
|
||||
redisVersion = "9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63";
|
||||
databaseVersion = "14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.immich;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Immich, Google Photos but better";
|
||||
defaultPort = "2283";
|
||||
middlewares = [
|
||||
"immichSecureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/redis"
|
||||
"100999:100 ${cfg.configDir}/model-cache"
|
||||
"100999:100 ${cfg.configDir}/machine-learning-cache"
|
||||
"100999:100 ${cfg.configDir}/machine-learning-config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
"100999:100 ${cfg.dataDir}"
|
||||
];
|
||||
secrets = [
|
||||
"immich/redis_hostname"
|
||||
"immich/db_hostname"
|
||||
"immich/db_name"
|
||||
"immich/db_username"
|
||||
"immich/db_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich-server
|
||||
hostname: immich-server
|
||||
image: ghcr.io/immich-app/immich-server:${immichVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.253
|
||||
ports:
|
||||
- "${cfg.port}:2283/tcp"
|
||||
volumes:
|
||||
- $UPLOAD_LOCATION:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
TZ: $TZ
|
||||
REDIS_HOSTNAME: ${config.sops.placeholder."immich/redis_hostname"}
|
||||
DB_HOSTNAME: ${config.sops.placeholder."immich/db_hostname"}
|
||||
DB_DATABASE_NAME: ${config.sops.placeholder."immich/db_name"}
|
||||
DB_USERNAME: ${config.sops.placeholder."immich/db_username"}
|
||||
DB_PASSWORD: ${config.sops.placeholder."immich/db_password"}
|
||||
IMMICH_TRUSTED_PROXIES: 10.89.210.1
|
||||
depends_on:
|
||||
- immich-redis
|
||||
- immich-database
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich-machine-learning
|
||||
hostname: immich-machine-learning
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${immichVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/model-cache:/cache
|
||||
- ${cfg.configDir}/machine-learning-config:/usr/src/.config
|
||||
- ${cfg.configDir}/machine-learning-cache:/usr/src/.cache/
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-redis:
|
||||
container_name: immich-redis
|
||||
hostname: immich-redis
|
||||
image: docker.io/valkey/valkey:${redisVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.251
|
||||
volumes:
|
||||
- ${cfg.configDir}/redis:/data
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-database:
|
||||
container_name: immich-database
|
||||
hostname: immich-database
|
||||
image: ghcr.io/immich-app/postgres:${databaseVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.250
|
||||
environment:
|
||||
POSTGRES_DB: ${config.sops.placeholder."immich/db_name"}
|
||||
POSTGRES_USER: ${config.sops.placeholder."immich/db_username"}
|
||||
POSTGRES_PASSWORD: ${config.sops.placeholder."immich/db_password"}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
volumes:
|
||||
- $DB_DATA_LOCATION:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
immich:
|
||||
driver: bridge
|
||||
name: immich
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.210.0/24"
|
||||
gateway: "10.89.210.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."immich/env" = {
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
DB_DATA_LOCATION=${cfg.configDir}/database
|
||||
UPLOAD_LOCATION=${cfg.dataDir}
|
||||
'';
|
||||
path = "/etc/podman/immich/.env";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/immich-secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
immichSecureHeaders:
|
||||
headers:
|
||||
FrameDeny: true
|
||||
AccessControlAllowMethods: 'GET,POST,PUT,DELETE,OPTIONS'
|
||||
AccessControlAllowOriginList:
|
||||
- https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
ContentSecurityPolicy: "default-src 'self'; base-uri 'self'; img-src 'self' https://static.immich.cloud https://tiles.immich.cloud data: blob:; connect-src 'self' https://${cfg.subdomain}.${config.numbus-server.services.domain} wss://${cfg.subdomain}.${config.numbus-server.services.domain} https://static.immich.cloud https://tiles.immich.cloud; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: https://${cfg.subdomain}.${config.numbus-server.services.domain}; frame-ancestors 'self';"
|
||||
CustomFrameOptionsValue: SAMEORIGIN
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: vibrate 'self'
|
||||
STSSeconds: 315360000
|
||||
'';
|
||||
path = "/etc/traefik/rules/immich-secureHeaders.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "it-tools";
|
||||
# Version tagging
|
||||
it-toolsVersion = "2024.10.22-7ca5933";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.it-tools;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "IT-tools, useful tools when doing IT";
|
||||
pod = "false";
|
||||
defaultPort = "8880";
|
||||
configDirEnabled = false;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
it-tools:
|
||||
image: docker.io/corentinth/it-tools:${it-toolsVersion}
|
||||
container_name: it-tools
|
||||
hostname: it-tools
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
it-tools:
|
||||
ipv4_address: 10.89.200.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
it-tools:
|
||||
driver: bridge
|
||||
name: it-tools
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.200.0/24"
|
||||
gateway: "10.89.200.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "jellyfin";
|
||||
# Version tagging
|
||||
jellyfinVersion = "10.11.6";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.jellyfin;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Jellyfin : A self-hosted media server to stream your movies and music";
|
||||
defaultPort = "8096";
|
||||
scheme = "https"; #TODO CHECK
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.dataDir}"
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.dataDir}/media"
|
||||
"100999:100 ${cfg.dataDir}/fonts"
|
||||
"100999:100 ${cfg.configDir}/cache"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
jellyfin:
|
||||
image: docker.io/jellyfin/jellyfin:${jellyfinVersion}
|
||||
container_name: jellyfin
|
||||
hostname: jellyfin
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
jellyfin:
|
||||
ipv4_address: 10.89.190.253
|
||||
ports:
|
||||
- "${cfg.port}:8096/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${cfg.configDir}/cache:/cache
|
||||
- type: bind
|
||||
source: ${cfg.dataDir}/media
|
||||
target: /media
|
||||
- type: bind
|
||||
source: ${cfg.dataDir}/fonts
|
||||
target: /usr/local/share/fonts/custom
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
jellyfin:
|
||||
driver: bridge
|
||||
name: jellyfin
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.190.0/24"
|
||||
gateway: "10.89.190.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "lldap";
|
||||
# Version tagging
|
||||
lldapVersion = "v0.6.2";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.lldap;
|
||||
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
|
||||
domainParts = splitString "." config.numbus-server.services.domain;
|
||||
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "LLDAP, unified user management";
|
||||
defaultPort = "17170";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"network-online.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"lldap/jwt_secret"
|
||||
"lldap/key_seed"
|
||||
"lldap/admin_password"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
lldap:
|
||||
image: lldap/lldap:${lldapVersion}
|
||||
container_name: lldap
|
||||
hostname: lldap
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
lldap:
|
||||
ipv4_address: 10.89.185.253
|
||||
ports:
|
||||
- "3890:3890"
|
||||
- "${cfg.port}:17170"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/data
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- TZ=${config.time.timeZone}
|
||||
- LLDAP_LDAP_BASE_DN=${baseDN}
|
||||
- LLDAP_JWT_SECRET="${config.sops.placeholder."lldap/jwt_secret"}"
|
||||
- LLDAP_KEY_SEED="${config.sops.placeholder."lldap/key_seed"}"
|
||||
- LLDAP_LDAP_USER_PASS="${config.sops.placeholder."lldap/admin_password"}"
|
||||
- LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
|
||||
- LLDAP_SMTP_OPTIONS__SERVER=${config.numbus-server.mail.smtpServer}
|
||||
- LLDAP_SMTP_OPTIONS__PORT=${config.numbus-server.mail.smtpPort}
|
||||
- LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=${config.numbus-server.mail.smtpEncryption}
|
||||
- LLDAP_SMTP_OPTIONS__USER=${config.numbus-server.mail.smtpUsername}
|
||||
- LLDAP_SMTP_OPTIONS__PASSWORD=${config.sops.placeholder."mail/smtpPassword"}
|
||||
- LLDAP_SMTP_OPTIONS__FROM=no-reply <${config.numbus-server.mail.fromAddress}>
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
lldap:
|
||||
driver: bridge
|
||||
name: lldap
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.185.0/24"
|
||||
gateway: "10.89.185.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "n8n";
|
||||
# Version tagging
|
||||
n8nVersion = "2.11.4";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.n8n;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "n8n, the ultimate automation platform";
|
||||
defaultPort = "5678";
|
||||
scheme = "https";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:${n8nVersion}
|
||||
container_name: n8n
|
||||
hostname: n8n
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
n8n:
|
||||
ipv4_address: 10.89.180.253
|
||||
ports:
|
||||
- "${cfg.port}:5678"
|
||||
volumes:
|
||||
- ${optimizedDir}:/home/node/.n8n
|
||||
environment:
|
||||
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||||
- N8N_HOST=${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- N8N_PORT=5678
|
||||
- N8N_PROTOCOL=https
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- NODE_ENV=production
|
||||
- WEBHOOK_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}/
|
||||
- GENERIC_TIMEZONE=${time.timeZone}
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
n8n:
|
||||
driver: bridge
|
||||
name: n8n
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.180.0/24"
|
||||
gateway: "10.89.180.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "netbird";
|
||||
# Version tagging
|
||||
netbirdDashboardVersion = "";
|
||||
netbirdServerVersion = "";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.netbird;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "NetBird, an all-in-one ZTNA remote access platform";
|
||||
defaultPort = "8888";
|
||||
reverseProxied = false;
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"netbird/auth_key"
|
||||
"netbird/encryption_key"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
netbird-dashboard:
|
||||
image: netbirdio/dashboard:${netbirdDashboardVersion}
|
||||
container_name: netbird-dashboard
|
||||
hostname: netbird-dashboard
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbird:
|
||||
ipv4_address: 10.89.175.253
|
||||
ports:
|
||||
- "${defaultPort}:8080/tcp"
|
||||
environment:
|
||||
# Endpoints
|
||||
- NETBIRD_MGMT_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
# OIDC - using embedded IdP
|
||||
- AUTH_AUDIENCE=netbird-dashboard
|
||||
- AUTH_CLIENT_ID=netbird-dashboard
|
||||
- AUTH_CLIENT_SECRET=
|
||||
- AUTH_AUTHORITY=https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2
|
||||
- USE_AUTH0=false
|
||||
- AUTH_SUPPORTED_SCOPES=openid profile email groups
|
||||
- AUTH_REDIRECT_URI=/nb-auth
|
||||
- AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||
# SSL
|
||||
- NGINX_SSL_PORT=443
|
||||
- LETSENCRYPT_DOMAIN=none
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
netbird-server:
|
||||
image: netbirdio/netbird-server:${netbirdServerVersion}
|
||||
container_name: netbird-server
|
||||
hostname: netbird-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbird:
|
||||
ipv4_address: 10.89.175.252
|
||||
ports:
|
||||
- "8889:8081/tcp"
|
||||
- "3478:3478/udp"
|
||||
volumes:
|
||||
- ${config.sops.templates."netbird-config".path}:/etc/netbird/config.yaml
|
||||
- ${cfg.configDir}:/var/lib/netbird
|
||||
command: ["--config", "/etc/netbird/config.yaml"]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
netbird:
|
||||
driver: bridge
|
||||
name: netbird
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.175.0/24"
|
||||
gateway: "10.89.175.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."netbird-config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
server:
|
||||
listenAddress: ":80"
|
||||
exposedAddress: "https://${cfg.subdomain}.${config.numbus-server.services.domain}:443"
|
||||
stunPorts:
|
||||
- 3478
|
||||
metricsPort: 9090
|
||||
healthcheckAddress: ":9000"
|
||||
logLevel: "info"
|
||||
logFile: "console"
|
||||
authSecret: "${config.sops.placeholder."netbird/auth_key"}"
|
||||
dataDir: "/var/lib/netbird"
|
||||
|
||||
auth:
|
||||
issuer: "https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2"
|
||||
signKeyRefreshEnabled: true
|
||||
dashboardRedirectURIs:
|
||||
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-auth"
|
||||
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-silent-auth"
|
||||
cliRedirectURIs:
|
||||
- "http://localhost:53000/"
|
||||
|
||||
reverseProxy:
|
||||
trustedHTTPProxies:
|
||||
- "10.89.175.1/32"
|
||||
|
||||
store:
|
||||
engine: "sqlite"
|
||||
encryptionKey: "${config.sops.placeholder."netbird/encryption_key"}"
|
||||
'';
|
||||
path = "/etc/netbird/netbird.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/${name}" = {
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
${name}-dashboard:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
priority: 1
|
||||
${name}-grpc:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: ${name}-server-h2c
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
${name}-backend:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: ${name}-server
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
|
||||
services:${cfg.port}
|
||||
${name}-dashboard:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:${cfg.port}"
|
||||
${name}-server:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:8889"
|
||||
${name}-server-h2c:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "h2c://host.containers.internal:3478"
|
||||
'';
|
||||
path = "/etc/traefik/rules/${name}";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "netbootxyz";
|
||||
# Version tagging
|
||||
netbootxyzVersion = "3.0.0";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.netbootxyz;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Netboot.xyz, forget about flashing isos on USB sticks with PXE boot";
|
||||
pod = "false";
|
||||
defaultPort = "3004";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${optimizedDir}/assets"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
netbootxyz:
|
||||
image: ghcr.io/netbootxyz/netbootxyz:${netbootxyzVersion}
|
||||
container_name: netbootxyz
|
||||
hostname: netbootxyz
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbootxyz:
|
||||
ipv4_address: 10.89.170.253
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
- "69:69/udp"
|
||||
- "8008:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${optimizedDir}/assets:/assets
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${time.timeZone}
|
||||
- PORT_RANGE=30000:30010
|
||||
- SUBFOLDER=/
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
netbootxyz:
|
||||
driver: bridge
|
||||
name: netbootxyz
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.170.0/24"
|
||||
gateway: "10.89.170.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
nextcloudVersion = "33.0.0";
|
||||
redisVersion = "8.6-alpine";
|
||||
databaseVersion = "11.8";
|
||||
onlyofficeVersion = "9.2";
|
||||
whiteboardVersion = "v1.5.6";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.nextcloud;
|
||||
# Container config
|
||||
name = "nextcloud";
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Nextcloud, your own online office suite";
|
||||
defaultPort = "1100";
|
||||
middlewares = [
|
||||
"nextcloudSecureHeaders"
|
||||
];
|
||||
secrets = [
|
||||
"nextcloud/db_name"
|
||||
"nextcloud/db_username"
|
||||
"nextcloud/db_password"
|
||||
"nextcloud/redis_password"
|
||||
"nextcloud/onlyoffice_secret"
|
||||
"nextcloud/whiteboard_secret"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100032:100 ${cfg.dataDir}"
|
||||
"100032:100 ${cfg.configDir}"
|
||||
"100032:100 ${cfg.configDir}/web"
|
||||
"100999:100 ${cfg.configDir}/redis"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/log"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/cache"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/data"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/database"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
nextcloud-server:
|
||||
image: docker.io/library/nextcloud:${nextcloudVersion}
|
||||
container_name: nextcloud-server
|
||||
hostname: nextcloud-server
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/web:/var/www/html
|
||||
- ${cfg.dataDir}:/mnt/ncdata
|
||||
environment:
|
||||
MYSQL_HOST: nextcloud-database:3306
|
||||
MYSQL_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
|
||||
MYSQL_USER: ${config.sops.placeholder."nextcloud/db_username"}
|
||||
MYSQL_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
|
||||
REDIS_HOST_PASSWORD: ${config.sops.placeholder."nextcloud/redis_password"}
|
||||
REDIS_HOST: nextcloud-redis
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
NEXTCLOUD_DATA_DIR: /mnt/ncdata
|
||||
SMTP_SECURE: tls
|
||||
SMTP_HOST: ${config.numbus-server.mail.smtpServer}
|
||||
SMTP_PORT: ${toString config.numbus-server.mail.smtpPort}
|
||||
SMTP_NAME: ${config.numbus-server.mail.smtpUsername}
|
||||
SMTP_PASSWORD: ${config.sops.placeholder.smtpPassword}
|
||||
MAIL_FROM_ADDRESS: no-reply
|
||||
MAIL_DOMAIN: ${config.numbus-server.services.domain}
|
||||
APACHE_DISABLE_REWRITE_IP: 1
|
||||
OVERWRITEPROTOCOL: https
|
||||
TRUSTED_PROXIES: 10.89.160.1
|
||||
NC_default_phone_region: "${config.numbus-server.language}"
|
||||
NC_default_language: "${config.numbus-server.language}"
|
||||
NC_default_locale: "${config.numbus-server.locale}"
|
||||
NC_default_timezone: "${config.time.timeZone}"
|
||||
NC_maintenance_window_start: "1"
|
||||
depends_on:
|
||||
- nextcloud-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-redis:
|
||||
image: docker.io/library/redis:${redisVersion}
|
||||
container_name: nextcloud-redis
|
||||
hostname: nextcloud-redis
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/redis:/data
|
||||
command: redis-server --requirepass ${config.sops.placeholder."nextcloud/redis_password"} --save 60 1 --loglevel warning
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-database:
|
||||
image: docker.io/library/mariadb:${databaseVersion}
|
||||
container_name: nextcloud-database
|
||||
hostname: nextcloud-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.251
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/mysql
|
||||
environment:
|
||||
MARIADB_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
|
||||
MARIADB_USER: ${config.sops.placeholder."nextcloud/db_username"}
|
||||
MARIADB_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
|
||||
MARIADB_RANDOM_ROOT_PASSWORD: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
command:
|
||||
- "--transaction-isolation=READ-COMMITTED"
|
||||
- "--binlog-format=ROW"
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-onlyoffice:
|
||||
image: docker.io/onlyoffice/documentserver:${onlyofficeVersion}
|
||||
container_name: nextcloud-onlyoffice
|
||||
hostname: nextcloud-onlyoffice
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.250
|
||||
ports:
|
||||
- "9980:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/onlyoffice/log:/var/log/onlyoffice
|
||||
- ${cfg.configDir}/onlyoffice/cache:/var/lib/onlyoffice
|
||||
- ${cfg.configDir}/onlyoffice/data:/var/www/onlyoffice/Data
|
||||
- ${cfg.configDir}/onlyoffice/database:/var/lib/postgresql
|
||||
environment:
|
||||
- JWT_SECRET=${config.sops.placeholder."nextcloud/onlyoffice_secret"}
|
||||
- REDIS_SERVER_PASS=${config.sops.placeholder."nextcloud/redis_password"}
|
||||
- REDIS_SERVER_HOST=nextcloud-redis
|
||||
- REDIS_SERVER_PORT=6379
|
||||
- ADMINPANEL_ENABLED=false
|
||||
- EXAMPLE_ENABLED=false
|
||||
- METRICS_ENABLED=false
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-whiteboard:
|
||||
image: ghcr.io/nextcloud-releases/whiteboard:${whiteboardVersion}
|
||||
container_name: nextcloud-whiteboard
|
||||
hostname: nextcloud-whiteboard
|
||||
user: '1000:1000'
|
||||
ports:
|
||||
- "3002:3002/tcp"
|
||||
environment:
|
||||
NEXTCLOUD_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
JWT_SECRET_KEY: ${config.sops.placeholder."nextcloud/whiteboard_secret"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
nextcloud:
|
||||
driver: bridge
|
||||
name: nextcloud
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.160.0/24"
|
||||
gateway: "10.89.160.254"
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
onlyoffice = {
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = "onlyoffice";
|
||||
example = "onlyoffice";
|
||||
description = "The subdomain that onlyoffice for nextcloud will use";
|
||||
};
|
||||
};
|
||||
whiteboard = {
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = "whiteboard";
|
||||
example = "whiteboard";
|
||||
description = "The subdomain that whiteboard for nextcloud will use";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."traefik/rules/nextcloud-onlyoffice" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
nextcloud-onlyoffice:
|
||||
rule: "Host(`${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: nextcloud-onlyoffice
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
services:
|
||||
nextcloud-onlyoffice:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:9980"
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-onlyoffice.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/nextcloud-whiteboard" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
nextcloud-whiteboard:
|
||||
rule: "Host(`${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: nextcloud-whiteboard
|
||||
middlewares:
|
||||
- "secureHeaders"
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
services:
|
||||
nextcloud-whiteboard:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:3002"
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-whiteboard.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/nextcloud-secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
nextcloudSecureHeaders:
|
||||
headers:
|
||||
FrameDeny: false
|
||||
CustomFrameOptionsValue: "SAMEORIGIN"
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSSeconds: 315360000
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
AccessControlAllowMethods: "GET,OPTIONS,PUT"
|
||||
AccessControlAllowOriginList:
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: "vibrate=()"
|
||||
ContentSecurityPolicy: >-
|
||||
default-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
script-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
connect-src 'self';
|
||||
img-src 'self' data:;
|
||||
font-src 'self' data:;
|
||||
frame-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
frame-ancestors https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-secureHeaders";
|
||||
};
|
||||
|
||||
systemd.services."${name}-quirk" = {
|
||||
description = "Podman container quirk : ${name}";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "${name}.service" "${name}-secrets.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.sudo pkgs.podman pkgs.systemd pkgs.gnugrep ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
OCC="sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ"
|
||||
|
||||
[[ ! -e /var/lib/numbus-server/${name}/.env ]] && systemctl start ${name}-secrets.service
|
||||
until [[ -e /var/lib/numbus-server/${name}/.env ]]; do
|
||||
echo "Waiting for secrets generation..."
|
||||
sleep 5
|
||||
done
|
||||
source /var/lib/numbus-server/${name}/.env
|
||||
|
||||
until $OCC status | grep -iq "installed: true" >/dev/null 2>&1; do
|
||||
echo "Waiting for Nextcloud to be up and running..."
|
||||
sleep 60
|
||||
done
|
||||
|
||||
$OCC db:add-missing-indices
|
||||
$OCC maintenance:repair --include-expensive
|
||||
|
||||
INSTALL_APPS_LIST=( "calendar" "contacts" "mail" "notes" "onlyoffice" "cookbook" "whiteboard" )
|
||||
DISABLE_APPS_LIST=( "activity" "federation" "webhook_listeners" "photos" "recommendations" "sharebymail" "teams" "support" "richdocumentscode" )
|
||||
|
||||
for app in ''${INSTALL_APPS_LIST[@]}; do
|
||||
if ! $OCC --no-warnings app:list | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:install "$app"
|
||||
fi
|
||||
if $OCC --no-warnings app:list --disabled | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:enable "$app"
|
||||
fi
|
||||
done
|
||||
for app in ''${DISABLE_APPS_LIST[@]}; do
|
||||
if $OCC --no-warnings app:list --enabled | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:disable "$app"
|
||||
fi
|
||||
done
|
||||
$OCC --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
|
||||
$OCC --no-warnings config:system:set onlyoffice DocumentServerUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
|
||||
$OCC --no-warnings config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_PASSWORD"
|
||||
$OCC --no-warnings config:app:set whiteboard collabBackendUrl --value="https://${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}"
|
||||
$OCC --no-warnings config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_PASSWORD"
|
||||
|
||||
if [[ ! -f /var/lib/numbus-server/${name}/croned.true ]]; then
|
||||
$OCC background:cron
|
||||
sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php
|
||||
touch /var/lib/numbus-server/${name}/croned.true
|
||||
fi
|
||||
|
||||
if [[ ! -f /var/lib/numbus-server/${name}/scanned.true ]]; then
|
||||
$OCC files:scan --all
|
||||
$OCC files:repair-tree
|
||||
touch /var/lib/numbus-server/${name}/scanned.true
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services."${name}-cron" = {
|
||||
description = "Podman container crontab : ${name}";
|
||||
after = [ "${name}.service" "${name}-quirk.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
path = [ pkgs.sudo pkgs.podman ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecCondition = ''${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ status'';
|
||||
ExecStart = "${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers."${name}-cron" = {
|
||||
description = "Timer for Nextcloud cron";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "5m";
|
||||
OnUnitActiveSec = "5m";
|
||||
Unit = "${name}-cron.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "ntfy";
|
||||
# Version tagging
|
||||
ntfyVersion = "v2.18.0";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.ntfy;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Ntfy, get notified easily";
|
||||
defaultPort = "8099";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/cache"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
ntfy:
|
||||
image: docker.io/binwiederhier/ntfy
|
||||
container_name: ntfy
|
||||
hostname: ntfy
|
||||
user: "1000:1000"
|
||||
networks:
|
||||
ntfy:
|
||||
ipv4_address: 10.89.150.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
command:
|
||||
- serve
|
||||
volumes:
|
||||
- ${cfg.config}/cache:/var/cache/ntfy
|
||||
- ${cfg.config}/config:/etc/ntfy
|
||||
environment:
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
ntfy:
|
||||
driver: bridge
|
||||
name: ntfy
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.150.0/24"
|
||||
gateway: "10.89.150.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "odoo";
|
||||
# Version tagging
|
||||
odooVersion = "10.11.6";
|
||||
databaseVersion = "15.17";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.odoo;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Odoo : An open ERP (Enterprise resource planning) solution";
|
||||
defaultPort = "8069";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${optimizedDir}/odoo"
|
||||
"100999:100 ${cfg.configDir}/addons"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
odoo-database:
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
container_name: odoo-database
|
||||
hostname: odoo-database
|
||||
user: '1000:1000'
|
||||
shm_size: 128mb
|
||||
networks:
|
||||
odoo:
|
||||
ipv4_address: 10.89.190.253
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=${config.sops.placeholder."odoo/db_name"}
|
||||
- POSTGRES_PASSWORD=${config.sops.placeholder."odoo/db_password"}
|
||||
- POSTGRES_USER=${config.sops.placeholder."odoo/db_username"}
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
odoo-server:
|
||||
image: docker.io/library/odoo:${odooVersion}
|
||||
container_name: odoo-server
|
||||
hostname: odoo-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
odoo:
|
||||
ipv4_address: 10.89.190.252
|
||||
ports:
|
||||
- "${cfg.port}:8069/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}/odoo:/var/lib/odoo
|
||||
- ${cfg.configDir}/config:/etc/odoo
|
||||
- ${cfg.configDir}/addons:/mnt/extra-addons
|
||||
environment:
|
||||
- HOST=odoo-database
|
||||
- USER=${config.sops.placeholder."odoo/db_username"}
|
||||
- PASSWORD=${config.sops.placeholder."odoo/db_password"}
|
||||
depends_on:
|
||||
- odoo-database
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
odoo:
|
||||
driver: bridge
|
||||
name: odoo
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.190.0/24"
|
||||
gateway: "10.89.190.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."odoo/db_name" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
sops.secrets."odoo/db_username" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
sops.secrets."odoo/db_password" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "passbolt";
|
||||
# Version tagging
|
||||
passboltVersion = "5.9.0-1-ce-non-root";
|
||||
databaseVersion = "12.2";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.passbolt;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Passbolt, your password manager";
|
||||
defaultPort = "4433";
|
||||
scheme = "https";
|
||||
dataDirEnabled = false;
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [
|
||||
"100032:100 ${cfg.configDir}"
|
||||
"100032:100 ${cfg.configDir}/gpg"
|
||||
"100032:100 ${cfg.configDir}/jwt"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
secrets = [
|
||||
"passbolt/db_name"
|
||||
"passbolt/db_username"
|
||||
"passbolt/db_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
passbolt-server:
|
||||
image: docker.io/passbolt/passbolt:${passboltVersion}
|
||||
container_name: passbolt-server
|
||||
hostname: passbolt-server
|
||||
user: '33:33'
|
||||
networks:
|
||||
passbolt:
|
||||
ports:
|
||||
- "${cfg.port}:4433/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/gpg:/etc/passbolt/gpg
|
||||
- ${cfg.configDir}/jwt:/etc/passbolt/jwt
|
||||
environment:
|
||||
APP_DEFAULT_TIMEZONE: ${config.time.timeZone}
|
||||
APP_FULL_BASE_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
DATASOURCES_DEFAULT_HOST: "passbolt-database"
|
||||
DATASOURCES_DEFAULT_USERNAME: ${config.sops.placeholder."passbolt/db_username"}
|
||||
DATASOURCES_DEFAULT_PASSWORD: ${config.sops.placeholder."passbolt/db_password"}
|
||||
DATASOURCES_DEFAULT_DATABASE: ${config.sops.placeholder."passbolt/db_name"}
|
||||
EMAIL_DEFAULT_FROM_NAME: "Passbolt"
|
||||
EMAIL_TRANSPORT_DEFAULT_HOST: ${config.numbus-server.mail.smtpServer}
|
||||
EMAIL_TRANSPORT_DEFAULT_PORT: ${toString config.numbus-server.mail.smtpPort}
|
||||
EMAIL_TRANSPORT_DEFAULT_USERNAME: ${config.numbus-server.mail.smtpUsername}
|
||||
EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${config.sops.placeholder."mail/smtpPassword"}
|
||||
EMAIL_TRANSPORT_DEFAULT_TLS: true
|
||||
EMAIL_DEFAULT_FROM: passbolt-noreply@${config.numbus-server.services.domain}
|
||||
PASSBOLT_SSL_FORCE: true
|
||||
command:
|
||||
[
|
||||
"/usr/bin/wait-for.sh",
|
||||
"-t",
|
||||
"0",
|
||||
"passbolt-database:3306",
|
||||
"--",
|
||||
"/docker-entrypoint.sh"
|
||||
]
|
||||
depends_on:
|
||||
- passbolt-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
passbolt-database:
|
||||
image: docker.io/library/mariadb:${databaseVersion}
|
||||
container_name: passbolt-database
|
||||
hostname: passbolt-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
passbolt:
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: "true"
|
||||
MYSQL_DATABASE: ${config.sops."passbolt/db_name"}
|
||||
MYSQL_USER: ${config.sops."passbolt/db_username"}
|
||||
MYSQL_PASSWORD: ${config.sops."passbolt/db_password"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
passbolt:
|
||||
name: passbolt
|
||||
driver: bridge
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "traefik";
|
||||
# Version tagging
|
||||
traefikVersion = "v3.6.8";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.traefik;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Traefik reverse proxy, one to rule them all";
|
||||
defaultPort = "7780";
|
||||
pod = "false";
|
||||
startDelay = 10;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"network-online.target"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/certs"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
traefik:
|
||||
image: docker.io/library/traefik:${traefikVersion}
|
||||
container_name: traefik
|
||||
hostname: traefik
|
||||
user: '1000:1000'
|
||||
network_mode: pasta
|
||||
ports:
|
||||
- "${cfg.port}:8080/tcp"
|
||||
- "443:443/tcp"
|
||||
volumes:
|
||||
- ${config.sops.templates."traefik/config".path}:/etc/traefik/traefik.yaml:ro
|
||||
- ${cfg.configDir}/certs:/var/traefik/certs
|
||||
- /etc/traefik/rules:/etc/traefik/rules:ro
|
||||
environment:
|
||||
- CF_DNS_API_TOKEN=${config.sops.placeholder."traefik/cloudflare_api_token"}
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."traefik/cloudflare_api_token" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/traefik.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.templates."traefik/config"= {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
global:
|
||||
checkNewVersion: false
|
||||
sendAnonymousUsage: false
|
||||
log:
|
||||
level: ${cfg.logLevel}
|
||||
accesslog: {}
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: false
|
||||
entryPoints:
|
||||
web:
|
||||
address: :80
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: :443
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
- "127.0.0.1/32"
|
||||
- "10.0.0.0/8"
|
||||
- "192.168.0.0/16"
|
||||
- "172.16.0.0/12"
|
||||
certificatesResolvers:
|
||||
cloudflare:
|
||||
acme:
|
||||
email: ${config.numbus-server.mail.adminAddress}
|
||||
storage: /var/traefik/certs/cloudflare-acme.json
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
dnsChallenge:
|
||||
provider: cloudflare
|
||||
resolvers:
|
||||
- "1.1.1.1:53"
|
||||
- "9.9.9.9:53"
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
providers:
|
||||
file:
|
||||
directory: "/etc/traefik/rules"
|
||||
watch: true
|
||||
'';
|
||||
path = "/etc/traefik/traefik.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
secureHeaders:
|
||||
headers:
|
||||
FrameDeny: true
|
||||
AccessControlAllowMethods: 'GET,OPTIONS,PUT'
|
||||
AccessControlAllowOriginList:
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
ContentSecurityPolicy: default-src 'self' 'unsafe-inline'
|
||||
CustomFrameOptionsValue: SAMEORIGIN
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: vibrate 'self'
|
||||
STSSeconds: 315360000
|
||||
'';
|
||||
path = "/etc/traefik/rules/secureHeaders.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/secureTLS" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
tls:
|
||||
options:
|
||||
secureTLS:
|
||||
minVersion: VersionTLS12
|
||||
sniStrict: true
|
||||
curvePreferences:
|
||||
- CurveP521
|
||||
- CurveP384
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
'';
|
||||
path = "/etc/traefik/rules/secureTLS.yaml";
|
||||
};
|
||||
};
|
||||
|
||||
extraOptions = {
|
||||
enable.default = true;
|
||||
logLevel = mkOption {
|
||||
type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ];
|
||||
default = "ERROR";
|
||||
example = "ERROR";
|
||||
description = "The level of detail Traefik should print in the logs.";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "uptimeKuma";
|
||||
# Version tagging
|
||||
uptimeKumaVersion = "2.2.0-rootless";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.uptimeKuma;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Uptime-Kuma, \"don't let your server down !\" monitoring tools";
|
||||
defaultPort = "3001";
|
||||
scheme = "http";
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [ "100999:100 ${optimizedDir}" ];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
uptimekuma:
|
||||
image: docker.io/louislam/uptime-kuma:${uptimeKumaVersion}
|
||||
container_name: uptime-kuma
|
||||
hostname: uptime-kuma
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
uptime-kuma:
|
||||
ipv4_address: 10.89.100.253
|
||||
ports:
|
||||
- "${cfg.port}:3001/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}:/app/data
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
uptime-kuma:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.100.0/24"
|
||||
gateway: "10.89.100.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "vscodium";
|
||||
# Version tagging
|
||||
vscodiumVersion = "1.110.11607-ls15";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.vscodium;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "VScodium, an open-source version of VScode in your web browser";
|
||||
defaultPort = "8000";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}/workspace"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
vscodium:
|
||||
image: lscr.io/linuxserver/vscodium-web:${vscodiumVersion}
|
||||
container_name: vscodium
|
||||
hostname: vscodium
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
vscodium:
|
||||
ipv4_address: 10.89.50.253
|
||||
ports:
|
||||
- "${defaultPort}:8000"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${optimizedDir}/workspace:/workspace
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${time.timeZone}
|
||||
- CONNECTION_TOKEN=${config.sops.placeholder."vscodium/connection_token"}
|
||||
shm_size: "1gb"
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
vscodium:
|
||||
name: vscodium
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.50.0/24"
|
||||
gateway: "10.89.50.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."vscodium/connection_token" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/vscodium.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
adguardVersion = "latest";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.adguard;
|
||||
# Container config
|
||||
name = "adguard";
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "AdGuard, feature-rich DNS service";
|
||||
defaultPort = "3000";
|
||||
scheme = "http";
|
||||
dataDirEnabled = false;
|
||||
startDelay = 10;
|
||||
dependencies = [
|
||||
"network.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
adguardhome:
|
||||
image: adguard/adguardhome:${adguardVersion}
|
||||
container_name: adguard
|
||||
hostname: adguard
|
||||
network_mode: pasta
|
||||
user: '1000:1000'
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/work:/opt/adguardhome/work
|
||||
- ${cfg.configDir}/config:/opt/adguardhome/conf
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./adguard.nix
|
||||
# Tested
|
||||
./pi-hole.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
piholeVersion = "2026.02.0";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.pi-hole;
|
||||
# Container config
|
||||
name = "pi-hole";
|
||||
# DNS config
|
||||
dnsConfig = ''
|
||||
|
||||
'';
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Pi-Hole, the ads black hole";
|
||||
defaultPort = "4443";
|
||||
scheme = "https";
|
||||
dataDirEnabled = false;
|
||||
startDelay = 10;
|
||||
dependencies = [
|
||||
"network.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"pi-hole/web_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
pi-hole:
|
||||
image: docker.io/pihole/pihole:${piholeVersion}
|
||||
container_name: pi-hole
|
||||
hostname: pi-hole
|
||||
network_mode: pasta
|
||||
ports:
|
||||
- "${cfg.port}:443/tcp"
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/etc/pihole
|
||||
environment:
|
||||
PIHOLE_UID: '1000'
|
||||
PIHOLE_GID: '1000'
|
||||
TZ: ${config.time.timeZone}
|
||||
FTLCONF_webserver_domain: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
FTLCONF_dns_domain_name: "${config.numbus-server.services.domain}"
|
||||
FTLCONF_webserver_api_password: ${config.sops.placeholder."pi-hole/web_password"}
|
||||
FTLCONF_dns_upstreams: 9.9.9.9;149.112.112.112
|
||||
FTLCONF_dns_listeningMode: "BIND"
|
||||
FTLCONF_dns_domain_local: "true"
|
||||
FTLCONF_dhcp_active: "false"
|
||||
FTLCONF_ntp_ipv4_active: "false"
|
||||
FTLCONF_ntp_ipv6_active: "false"
|
||||
FTLCONF_ntp_sync_active: "false"
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: unless-stopped
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.clamav;
|
||||
onAccessPaths = lib.mapAttrsToList (n: v: v.dataDir) (lib.filterAttrs (n: v:
|
||||
v ? enable && v.enable && v ? dataDir && v.dataDir != null && v.dataDir != false
|
||||
) config.numbus-server.services);
|
||||
clamonacc_virus_notifier = pkgs.writeScript "clamonacc_virus_notifier.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
echo "CLAM_VIRUSEVENT_VIRUSNAME=\"$CLAM_VIRUSEVENT_VIRUSNAME\"" > /var/lib/clamav/virus_event.env
|
||||
echo "CLAM_VIRUSEVENT_FILENAME=\"$CLAM_VIRUSEVENT_FILENAME\"" >> /var/lib/clamav/virus_event.env
|
||||
|
||||
/run/wrappers/bin/sudo /run/current-system/sw/bin/systemctl start clamav-virus-notify.service
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.services.clamav = {
|
||||
enable = mkEnableOption "ClamAV open-source anti-virus software";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.systemPackages = [ pkgs.clamav pkgs.curl ];
|
||||
|
||||
system.activationScripts.clamav-quarantine = ''
|
||||
mkdir -p /quarantine
|
||||
chown clamav:clamav /quarantine
|
||||
chmod 440 /quarantine
|
||||
'';
|
||||
|
||||
security.sudo.extraRules = [{
|
||||
users = [ "clamav" ];
|
||||
commands = [{
|
||||
command = "/run/current-system/sw/bin/systemctl start clamav-virus-notify.service";
|
||||
options = [ "NOPASSWD" ];
|
||||
}];
|
||||
}];
|
||||
|
||||
services.clamav = {
|
||||
updater.enable = true;
|
||||
clamonacc.enable = true;
|
||||
|
||||
scanner = {
|
||||
enable = true;
|
||||
interval = "*-*-* 04:00:00"; # Everyday at 4am
|
||||
scanDirectories = [
|
||||
"/etc"
|
||||
"/home"
|
||||
"/var/lib"
|
||||
"/var/tmp"
|
||||
"/tmp"
|
||||
];
|
||||
};
|
||||
|
||||
daemon = {
|
||||
enable = true;
|
||||
settings = {
|
||||
OnAccessPrevention = true;
|
||||
OnAccessIncludePath = onAccessPaths;
|
||||
VirusEvent = "${clamonacc_virus_notifier}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.clamav-periodic-scan = mkIf (onAccessPaths != []) {
|
||||
description = "Periodic ClamAV virus scan";
|
||||
after = [ "clamav-daemon.service" "clamav-freshclam.service" ];
|
||||
requires = [ "clamav-daemon.service" ];
|
||||
wants = [ "clamav-freshclam.service" ];
|
||||
onFailure = [ "clamav-virus-notify.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.clamav}/bin/clamdscan --multiscan --fdpass --infected --allmatch --move=/quarantine ${lib.escapeShellArgs onAccessPaths}";
|
||||
Slice = "system-clamav.slice";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.clamav-periodic-scan = mkIf (onAccessPaths != []) {
|
||||
description = "Timer for ClamAV periodic scan";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*-1/3-01 04:00:00";
|
||||
Persistent = true;
|
||||
Unit = "clamav-periodic-scan.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To add
|
||||
# ./backup-client.nix
|
||||
# To test
|
||||
./clamav.nix
|
||||
./virtualization.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.virtualization;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.services.virtualization = {
|
||||
enable = mkEnableOption "QEMU/KVM virtualization software";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
virtualisation.libvirtd.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./hardware/default.nix
|
||||
./mail/default.nix
|
||||
./misc/default.nix
|
||||
./packages/default.nix
|
||||
./global.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{ lib, deviceType, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus;
|
||||
|
||||
country = "";
|
||||
language = "";
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus = {
|
||||
owner = mkOption {
|
||||
type = types.str;
|
||||
example = "Alex";
|
||||
default = "Numbus";
|
||||
description = "The name of the person who owns this ${deviceType}.";
|
||||
};
|
||||
|
||||
internationalization = {
|
||||
country = mkOption {
|
||||
type = types.str;
|
||||
example = "FR";
|
||||
default = country;
|
||||
description = "The country where this ${deviceType} is located.";
|
||||
};
|
||||
language = mkOption {
|
||||
type = types.str;
|
||||
example = "fr";
|
||||
default = language;
|
||||
description = "The language for this ${deviceType}.";
|
||||
};
|
||||
locale = mkOption {
|
||||
type = types.str;
|
||||
example = "fr_FR";
|
||||
default = "fr_FR";
|
||||
description = "The locale for this ${deviceType}.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{ config, deviceType, ... }:
|
||||
|
||||
{
|
||||
config = mkMerge [
|
||||
({
|
||||
boot = {
|
||||
plymouth.enable = true;
|
||||
# Enable "Silent boot"
|
||||
consoleLogLevel = 3;
|
||||
initrd.verbose = false;
|
||||
loader.timeout = 1;
|
||||
};
|
||||
})
|
||||
|
||||
( mkIf (deviceType == "computer" || deviceType == "tv") {
|
||||
# Bootloader options
|
||||
boot = {
|
||||
initrd.systemd.enable = true;
|
||||
loader.systemd-boot.enable = true;
|
||||
loader.efi.canTouchEfiVariables = true;
|
||||
kernelParams = [
|
||||
"quiet"
|
||||
"udev.log_level=3"
|
||||
"systemd.show_status=auto"
|
||||
"pcie_aspm=force"
|
||||
"consoleblank=60"
|
||||
];
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
hardware.cpu.intel.updateMicrocode = true;
|
||||
hardware.cpu.amd.updateMicrocode = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./disks/default.nix
|
||||
./boot-params.nix
|
||||
./cpu.nix
|
||||
./graphics.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.disks.boot;
|
||||
|
||||
bootCount = builtins.length cfg.list;
|
||||
|
||||
singleDiskConfig = {
|
||||
disko.devices.disk.main = {
|
||||
type = "disk";
|
||||
device = head cfg.list;
|
||||
content = {
|
||||
type = cfg.partitionTableScheme;
|
||||
partitions = {
|
||||
ESP = {
|
||||
size = cfg.partition.boot.size;
|
||||
type = cfg.partition.boot.esp;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.boot.filesystem;
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
swap = {
|
||||
size = cfg.partition.swap.size;
|
||||
content = {
|
||||
type = "swap";
|
||||
randomEncryption = cfg.partition.swap.encrypt;
|
||||
};
|
||||
};
|
||||
luks = {
|
||||
size = cfg.partition.root.size;
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "boot";
|
||||
settings.keyFile = "/run/secrets/disks/boot";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.root.filesystem;
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
raid1DiskConfig = {
|
||||
disko.devices.disk = lib.listToAttrs (lib.imap0 (i: device: {
|
||||
name = "boot-${toString i}";
|
||||
value = {
|
||||
type = "disk";
|
||||
inherit device;
|
||||
content = {
|
||||
type = cfg.partitionTableScheme;
|
||||
partitions = {
|
||||
ESP = { size = cfg.partition.boot.size; type = cfg.partition.boot.esp; content = { type = "mdraid"; name = "boot"; }; };
|
||||
swap = { size = cfg.partition.swap.size; content = { type = "mdraid"; name = "swap"; }; };
|
||||
mdadm = { size = cfg.partition.root.size; content = { type = "mdraid"; name = "raid1"; }; };
|
||||
};
|
||||
};
|
||||
};
|
||||
}) cfg.list);
|
||||
|
||||
disko.devices.mdadm = {
|
||||
boot = {
|
||||
type = "mdadm";
|
||||
level = 1;
|
||||
metadata = "1.0";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.boot.filesystem;
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
swap = {
|
||||
type = "mdadm";
|
||||
level = 1;
|
||||
content = {
|
||||
type = "swap";
|
||||
randomEncryption = cfg.partition.swap.encrypt;
|
||||
};
|
||||
};
|
||||
raid1 = {
|
||||
type = "mdadm";
|
||||
level = 1;
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "boot";
|
||||
settings.keyFile = "/run/secrets/disks/boot";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.root.filesystem;
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.hardware.disks = {
|
||||
boot = {
|
||||
list = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "/dev/disk/by-id/nvme_SAMSUNG_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ];
|
||||
description = "A set of by-id path of disk(s) that will be used as boot disk(s). At least one disk must be set.";
|
||||
};
|
||||
partitionTableScheme = mkOption {
|
||||
type = types.enum [ "gpt" "mbr" ];
|
||||
default = "gpt";
|
||||
example = "gpt";
|
||||
description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones.";
|
||||
};
|
||||
partition = {
|
||||
root = {
|
||||
filesystem = mkOption {
|
||||
type = types.enum [ "ext4" "btrfs" "xfs" ];
|
||||
default = "ext4";
|
||||
example = "ext4";
|
||||
description = "The filesystem to use for the root partition of the boot disk(s).";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "100%";
|
||||
example = "100%";
|
||||
description = "The size of the root partition. Use G for GBs and M for MBs.";
|
||||
};
|
||||
};
|
||||
boot = {
|
||||
filesystem = mkOption {
|
||||
type = types.enum [ "vfat" ];
|
||||
default = "vfat";
|
||||
example = "vfat";
|
||||
description = "The filesystem to use for the boot partition of the boot disk(s).";
|
||||
};
|
||||
esp = mkOption {
|
||||
type = types.enum [ "EF00" "EF02" ];
|
||||
default = "EF00";
|
||||
example = "EF00";
|
||||
description = "The ESP type to use for the boot partition. Use EF02 for UEFI and EF00 for BIOS.";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "1G";
|
||||
example = "1G";
|
||||
description = "The size of the boot partition.";
|
||||
};
|
||||
};
|
||||
swap = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
example = true;
|
||||
description = "Wether to create a swap partition. Useful for servers that don't have a lot of RAM.";
|
||||
};
|
||||
encrypt = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
example = true;
|
||||
description = "Wether to encrypt randomly the swap partition. Disable if you need hibernation";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "16G";
|
||||
example = "16G";
|
||||
description = "Size of the swap partition. Use G for GBs and M for MBs.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
{
|
||||
sops.secrets."disks/boot" = {
|
||||
sopsFile = "/etc/nixos/secrets/disks/boot.yaml";
|
||||
gid = "0";
|
||||
uid = "0";
|
||||
mode = "0400";
|
||||
};
|
||||
}
|
||||
(mkIf (bootCount == 1) singleDiskConfig)
|
||||
(mkIf (bootCount == 2) raid1DiskConfig)
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.disks.content;
|
||||
|
||||
contentCount = builtins.length cfg.list;
|
||||
parityCount = builtins.length config.numbus.hardware.disks.parity.list;
|
||||
|
||||
contentDisks = lib.imap0 (i: device: {
|
||||
name = "content-${toString i}";
|
||||
value = {
|
||||
type = "disk";
|
||||
inherit device;
|
||||
content = {
|
||||
type = cfg.partitionTableScheme;
|
||||
partitions.luks = {
|
||||
size = cfg.partition.size;
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "content-${toString i}";
|
||||
settings.keyFile = "/run/secrets/disks/content-${toString i}";
|
||||
initrdUnlock = false;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.filesystem;
|
||||
mountpoint = "/mnt/content-${toString i}";
|
||||
mountOptions = [ "noauto" "nofail" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}) cfg.list;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.hardware.disks = {
|
||||
content = {
|
||||
list = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "/dev/disk/by-id/ata_Hitachi_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-WD_159Ejz224G" ];
|
||||
default = [];
|
||||
description = "A set of by-id path of disk(s) that will be used as content disk(s).";
|
||||
};
|
||||
partitionTableScheme = mkOption {
|
||||
type = types.enum [ "gpt" "mbr" ];
|
||||
default = "gpt";
|
||||
example = "gpt";
|
||||
description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones.";
|
||||
};
|
||||
partition = {
|
||||
filesystem = mkOption {
|
||||
type = types.enum [ "ext4" "btrfs" "xfs" ];
|
||||
default = "xfs";
|
||||
example = "xfs";
|
||||
description = "The filesystem to use for the main partition of the content disk(s).";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "100%";
|
||||
example = "100%";
|
||||
description = "The size of the main partition. Use G for GBs and M for MBs.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (contentCount > 0 && (parityCount != 1 && contentCount != 1)) {
|
||||
disko.devices.disk = builtins.listToAttrs contentDisks;
|
||||
|
||||
sops.secrets = listToAttrs (map (i:
|
||||
nameValuePair "disks/content-${toString i}" {
|
||||
sopsFile = "/etc/nixos/secrets/disks/content.yaml";
|
||||
gid = "0";
|
||||
uid = "0";
|
||||
mode = "0400";
|
||||
}
|
||||
) (range 0 (contentCount - 1)));
|
||||
|
||||
systemd.services.mount-content-disks = {
|
||||
description = "Mount content disks.";
|
||||
before = [ "mnt-data.mount" ];
|
||||
requiredBy = [ "mnt-data.mount" ];
|
||||
requires = [ "sops-install-secrets.service" ];
|
||||
path = [ pkgs.cryptsetup pkgs.util-linux ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = let
|
||||
mountContentDisk = i: ''
|
||||
if [ ! -e /dev/mapper/content-${toString i} ]; then
|
||||
cryptsetup luksOpen --key-file /run/secrets/disks/content-${toString i} /dev/disk/by-partlabel/disk-content-${toString i}-luks content-${toString i}
|
||||
fi
|
||||
mkdir -p /mnt/content-${toString i}
|
||||
if ! mountpoint -q /mnt/content-${toString i}; then
|
||||
mount -t ${cfg.partition.filesystem} /dev/mapper/content-${toString i} /mnt/content-${toString i}
|
||||
fi
|
||||
'';
|
||||
in ''
|
||||
${concatMapStrings mountContentDisk (range 0 (contentCount - 1))}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./boot.nix
|
||||
./content.nix
|
||||
./mergerfs-snapraid.nix
|
||||
./mirror.nix
|
||||
./parity.nix
|
||||
./spindown.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.disks;
|
||||
|
||||
contentCount = builtins.length cfg.content.list;
|
||||
parityCount = builtins.length cfg.parity.list;
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf (contentCount >= 2 && parityCount >= 1) {
|
||||
services.snapraid = {
|
||||
enable = true;
|
||||
contentFiles = map (i: "/mnt/content-${toString i}/snapraid.content") (range 0 (contentCount - 1));
|
||||
parityFiles = map (i: "/mnt/parity-${toString i}/snapraid.parity") (range 0 (parityCount - 1));
|
||||
dataDisks = listToAttrs (imap0 (i: _: nameValuePair "d${toString i}" "/mnt/content-${toString i}") cfg.content.list);
|
||||
};
|
||||
|
||||
fileSystems."/mnt/data" = {
|
||||
device = concatStringsSep ":" (map (i: "/mnt/content-${toString i}") (range 0 (contentCount - 1)));
|
||||
fsType = "fuse.mergerfs";
|
||||
options = [
|
||||
"category.create=ff"
|
||||
"cache.files=partial"
|
||||
"dropcacheonclose=true"
|
||||
"defaults"
|
||||
"noauto"
|
||||
"nofail"
|
||||
"allow_other"
|
||||
"moveonenospc=1"
|
||||
"minfreespace=50G"
|
||||
"func.getattr=newest"
|
||||
"fsname=mergerfs_data"
|
||||
"x-mount.mkdir"
|
||||
"x-systemd.automount"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.disks;
|
||||
|
||||
contentCount = builtins.length cfg.content.list;
|
||||
parityCount = builtins.length cfg.parity.list;
|
||||
|
||||
dataMirror = {
|
||||
disko.devices.disk = listToAttrs (imap0 (i: device: {
|
||||
name = "mirror-${toString i}";
|
||||
value = {
|
||||
type = "disk";
|
||||
inherit device;
|
||||
content = {
|
||||
type = cfg.partitionTableScheme;
|
||||
partitions.raid = {
|
||||
size = cfg.content.partition.size;
|
||||
content = {
|
||||
type = "mdraid";
|
||||
name = "mirror";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}) (cfg.content.list ++ cfg.parity.list));
|
||||
|
||||
disko.devices.mdadm.mirror = {
|
||||
type = "mdadm";
|
||||
level = 1;
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "mirror";
|
||||
settings.keyFile = "/run/secrets/disks/mirror";
|
||||
initrdUnlock = false;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.content.partition.filesystem;
|
||||
mountpoint = "/mnt/data";
|
||||
mountOptions = [ "noauto" "nofail" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf (contentCount == 1 && parityCount == 1) (mkMerge [
|
||||
dataMirror
|
||||
{
|
||||
systemd.services.mount-mirror = {
|
||||
description = "Mount the disks mirror.";
|
||||
before = [ "mnt-data.mount" ];
|
||||
requiredBy = [ "mnt-data.mount" ];
|
||||
requires = [ "sops-install-secrets.service" ];
|
||||
path = [ pkgs.cryptsetup pkgs.util-linux ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
if [ ! -e /dev/mapper/mirror ]; then
|
||||
cryptsetup open /dev/md/mirror mirror --key-file /run/secrets/disks/mirror
|
||||
fi
|
||||
mkdir -p /mnt/data
|
||||
if ! mountpoint -q /mnt/data; then
|
||||
mount -t ${cfg.content.partition.filesystem} /dev/mapper/mirror /mnt/data
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.disks.parity;
|
||||
|
||||
parityCount = builtins.length cfg.list;
|
||||
|
||||
parityDisks = lib.imap0 (i: device: {
|
||||
name = "parity-${toString i}";
|
||||
value = {
|
||||
type = "disk";
|
||||
inherit device;
|
||||
content = {
|
||||
type = cfg.partitionTableScheme;
|
||||
partitions.luks = {
|
||||
size = cfg.partition.size;
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "parity-${toString i}";
|
||||
settings.keyFile = "/run/secrets/disks/parity-${toString i}";
|
||||
initrdUnlock = false;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = cfg.partition.filesystem;
|
||||
mountpoint = "/mnt/parity-${toString i}";
|
||||
mountOptions = [ "noauto" "nofail" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}) cfg.list;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.hardware.disks = {
|
||||
parity = {
|
||||
list = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "/dev/disk/by-id/ata_WDC_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-San_Disk_159Ejz224G" ];
|
||||
default = [];
|
||||
description = "A set of by-id path of disk(s) that will be used as parity disk(s).";
|
||||
};
|
||||
partitionTableScheme = mkOption {
|
||||
type = types.enum [ "gpt" "mbr" ];
|
||||
default = "gpt";
|
||||
example = "gpt";
|
||||
description = "The scheme of the partition table. Use \"gpt\" for modern devices and \"mbr\" for legacy ones.";
|
||||
};
|
||||
partition = {
|
||||
filesystem = mkOption {
|
||||
type = types.enum [ "ext4" "btrfs" "xfs" ];
|
||||
default = "xfs";
|
||||
example = "xfs";
|
||||
description = "The filesystem to use for the main partition of the parity disk(s).";
|
||||
};
|
||||
size = mkOption {
|
||||
type = types.str;
|
||||
default = "100%";
|
||||
example = "100%";
|
||||
description = "The size of the main partition. Use G for GBs and M for MBs.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (parityCount > 1) {
|
||||
disko.devices.disk = builtins.listToAttrs parityDisks;
|
||||
|
||||
sops.secrets = listToAttrs (map (i:
|
||||
nameValuePair "disks/parity-${toString i}" {
|
||||
sopsFile = "/etc/nixos/secrets/disks/parity.yaml";
|
||||
gid = "0";
|
||||
uid = "0";
|
||||
mode = "0400";
|
||||
}
|
||||
) (range 0 (parityCount - 1)));
|
||||
|
||||
systemd.services.mount-parity-disks = {
|
||||
description = "Mount parity disks.";
|
||||
before = [ "mnt-data.mount" ];
|
||||
requiredBy = [ "mnt-data.mount" ];
|
||||
requires = [ "sops-install-secrets.service" ];
|
||||
path = [ pkgs.cryptsetup pkgs.util-linux ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = let
|
||||
mountparityDisk = i: ''
|
||||
if [ ! -e /dev/mapper/parity-${toString i} ]; then
|
||||
cryptsetup luksOpen --key-file /run/secrets/disks/parity-${toString i} /dev/disk/by-partlabel/disk-parity-${toString i}-luks parity-${toString i}
|
||||
fi
|
||||
mkdir -p /mnt/parity-${toString i}
|
||||
if ! mountpoint -q /mnt/parity-${toString i}; then
|
||||
mount -t ${cfg.partition.filesystem} /dev/mapper/parity-${toString i} /mnt/parity-${toString i}
|
||||
fi
|
||||
'';
|
||||
in
|
||||
''
|
||||
${concatMapStrings mountparityDisk (range 0 (parityCount - 1))}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
hardDrives = config.numbus.hardware.spindown.list;
|
||||
cfg = config.numbus.hardware;
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf (cfg.HddSpindown.enable == true) {
|
||||
systemd.services.hd-idle = {
|
||||
description = "External HD spin down daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart =
|
||||
let
|
||||
idleTime = toString 1800;
|
||||
hardDriveParameter = lib.strings.concatMapStringsSep " " (x: "-a ${x} -i ${idleTime}") hardDrives;
|
||||
in
|
||||
"${pkgs.hd-idle}/bin/hd-idle -i 0 ${hardDriveParameter}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
options.numbus = {
|
||||
hardware = {
|
||||
spindown = {
|
||||
enable = mkEnableOption "hard drives spin down when inactive in order to save power.";
|
||||
list = mkOption {
|
||||
description = "The list of compatible hard drives that will spin down.";
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "/dev/disk/by-id/ata_Hitachi_MZVPYEHCO_159Ejz224G0000" "/dev/disk/by-id/ata-WD_159Ejz224G" ];
|
||||
};
|
||||
optimize = mkOption {
|
||||
description = "Optimize services to reduce HDD wakeups when spindown is enabled. Can be set to \"compatible\" to optimize all compatible services, or a list of service names to optimize.";
|
||||
type = types.nullOr (types.either (types.enum [ "compatible" ]) (types.listOf types.str));
|
||||
default = "compatible";
|
||||
example = "[ \"crafty\" \"gitea\" ]";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.hardware = {
|
||||
nvidia = {
|
||||
enable = mkEnableOption "Wether to install the NVIDIA driver. Required for better performance with NVIDIA graphics cards."
|
||||
}
|
||||
}
|
||||
|
||||
config = mkMerge [
|
||||
({
|
||||
# Enable OpenGL
|
||||
hardware.graphics = {
|
||||
enable = true;
|
||||
};
|
||||
})
|
||||
|
||||
( mkIf (cfg.nvidia.enable == true) {
|
||||
# Load nvidia driver for Xorg and Wayland
|
||||
services.xserver.videoDrivers = [ "nvidia" ];
|
||||
|
||||
hardware.nvidia = {
|
||||
# Modesetting is required.
|
||||
modesetting.enable = true;
|
||||
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
|
||||
# Enable this if you have graphical corruption issues or application crashes after waking
|
||||
# up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
|
||||
# of just the bare essentials.
|
||||
powerManagement.enable = false;
|
||||
|
||||
# Fine-grained power management. Turns off GPU when not in use.
|
||||
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
|
||||
powerManagement.finegrained = false;
|
||||
|
||||
# Use the NVidia open source kernel module (not to be confused with the
|
||||
# independent third-party "nouveau" open source driver).
|
||||
# Support is limited to the Turing and later architectures. Full list of
|
||||
# supported GPUs is at:
|
||||
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
|
||||
# Only available from driver 515.43.04+
|
||||
open = false;
|
||||
|
||||
nvidiaSettings = true;
|
||||
# Optionally, you may need to select the appropriate driver version for your specific GPU.
|
||||
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.numbus.hardware.pcie-coral;
|
||||
|
||||
gasket-driver = { stdenv, lib, fetchFromGitHub, kernel }: stdenv.mkDerivation rec {
|
||||
pname = "gasket";
|
||||
version = "1.0-18";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "google";
|
||||
repo = "gasket-driver";
|
||||
rev = "97aeba584efd18983850c36dcf7384b0185284b3";
|
||||
sha256 = "pJwrrI7jVKFts4+bl2xmPIAD01VKFta2SRuElerQnTo=";
|
||||
};
|
||||
|
||||
makeFlags = [
|
||||
"-C"
|
||||
"${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"
|
||||
"M=$(PWD)"
|
||||
];
|
||||
buildFlags = [ "modules" ];
|
||||
|
||||
installFlags = [ "INSTALL_MOD_PATH=${placeholder "out"}" ];
|
||||
installTargets = [ "modules_install" ];
|
||||
|
||||
sourceRoot = "source/src";
|
||||
hardeningDisable = [ "pic" "format" ];
|
||||
nativeBuildInputs = kernel.moduleBuildDependencies;
|
||||
|
||||
meta = with lib; {
|
||||
description = "The Coral Gasket Driver allows usage of the Coral EdgeTPU on Linux systems.";
|
||||
homepage = "https://github.com/google/gasket-driver";
|
||||
license = licenses.gpl2;
|
||||
maintainers = [ maintainers.kylehendricks ];
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
};
|
||||
|
||||
libedgetpu-pkg = { stdenv, lib, fetchFromGitHub, libusb1, abseil-cpp, flatbuffers, xxd }:
|
||||
let
|
||||
flatbuffers_1_12 = flatbuffers.overrideAttrs (oldAttrs: rec {
|
||||
version = "1.12.0";
|
||||
NIX_CFLAGS_COMPILE = "-Wno-error=class-memaccess -Wno-error=maybe-uninitialized";
|
||||
cmakeFlags = (oldAttrs.cmakeFlags or []) ++ ["-DFLATBUFFERS_BUILD_SHAREDLIB=ON"];
|
||||
NIX_CXXSTDLIB_COMPILE = "-std=c++17";
|
||||
configureFlags = (oldAttrs.configureFlags or []) ++ ["--enable-shared"];
|
||||
src = fetchFromGitHub {
|
||||
owner = "google";
|
||||
repo = "flatbuffers";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-L1B5Y/c897Jg9fGwT2J3+vaXsZ+lfXnskp8Gto1p/Tg=";
|
||||
};
|
||||
});
|
||||
|
||||
in stdenv.mkDerivation rec {
|
||||
pname = "libedgetpu";
|
||||
version = "grouper";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "google-coral";
|
||||
repo = pname;
|
||||
rev = "release-${version}";
|
||||
sha256 = "sha256-73hwItimf88Iqnb40lk4ul/PzmCNIfdt6Afi+xjNiBE=";
|
||||
};
|
||||
|
||||
makeFlags = ["-f" "makefile_build/Makefile" "libedgetpu" ];
|
||||
|
||||
buildInputs = [
|
||||
libusb1
|
||||
abseil-cpp
|
||||
flatbuffers_1_12
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
xxd
|
||||
];
|
||||
|
||||
NIX_CXXSTDLIB_COMPILE = "-std=c++17";
|
||||
|
||||
TFROOT = "${fetchFromGitHub {
|
||||
owner = "tensorflow";
|
||||
repo = "tensorflow";
|
||||
rev = "v2.7.4";
|
||||
sha256 = "sha256-liDbUAdaVllB0b74aBeqNxkYNu/zPy7k3CevzRF5dk0=";
|
||||
}}";
|
||||
|
||||
enableParallelBuilding = false;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/lib
|
||||
cp out/direct/k8/libedgetpu.so.1.0 $out/lib
|
||||
ln -s $out/lib/libedgetpu.so.1.0 $out/lib/libedgetpu.so.1
|
||||
mkdir -p $out/lib/udev/rules.d
|
||||
cp debian/edgetpu-accelerator.rules $out/lib/udev/rules.d/99-edgetpu-accelerator.rules
|
||||
'';
|
||||
};
|
||||
|
||||
gasket = config.boot.kernelPackages.callPackage gasket-driver {};
|
||||
libedgetpu = pkgs.callPackage libedgetpu-pkg {};
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.hardware.pcie-coral = lib.mkEnableOption "PCIe Coral TPU support";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.udev.packages = [ "libedgetpu" ];
|
||||
users.groups.plugdev = {};
|
||||
boot.extraModulePackages = [ "gasket" ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.clamav;
|
||||
clamav_notifier = pkgs.writeScript "clamav-notify.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
# Check if triggered by Real-time event (file exists)
|
||||
if [ -f /var/lib/clamav/virus_event.env ]; then
|
||||
source /var/lib/clamav/virus_event.env
|
||||
rm /var/lib/clamav/virus_event.env
|
||||
fi
|
||||
|
||||
ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}"
|
||||
USER_EMAIL="${config.numbus-server.mail.userAddress}"
|
||||
OWNER_NAME="${config.numbus-server.owner}"
|
||||
|
||||
if [ -n "$CLAM_VIRUSEVENT_VIRUSNAME" ]; then
|
||||
# --- Real-time / VirusEvent Mode ---
|
||||
SUBJECT="Numbus Server Alert: Virus Detected (Real-time)"
|
||||
|
||||
# Retrieve logs from clamav-daemon
|
||||
LOGS=$(journalctl -u clamav-daemon.service -n 50 --no-pager | grep "FOUND")
|
||||
|
||||
TECH_BODY="
|
||||
ClamAV Real-time Alert:
|
||||
Server owner: $OWNER_NAME
|
||||
|
||||
Virus detected: $CLAM_VIRUSEVENT_VIRUSNAME
|
||||
File: $CLAM_VIRUSEVENT_FILENAME
|
||||
|
||||
Logs:
|
||||
$LOGS
|
||||
|
||||
Action taken: Access blocked (OnAccessPrevention).
|
||||
Please investigate manually.
|
||||
"
|
||||
|
||||
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
|
||||
|
||||
L'antivirus de votre serveur a détecté et bloqué une menace en temps réel.
|
||||
Fichier : $CLAM_VIRUSEVENT_FILENAME
|
||||
|
||||
Votre administrateur a été notifié.
|
||||
"
|
||||
else
|
||||
# --- Scheduled Scan Summary Mode ---
|
||||
SUBJECT="Numbus Server Alert: Virus Detected during Scheduled Scan"
|
||||
|
||||
# Retrieve logs (clamdscan prints FOUND when a virus is detected)
|
||||
LOGS=$(journalctl -u clamav-periodic-scan.service -n 100 --no-pager | grep "FOUND")
|
||||
|
||||
TECH_BODY="
|
||||
ClamAV Scan Alert:
|
||||
Server owner: $OWNER_NAME
|
||||
|
||||
Viruses detected:
|
||||
$LOGS
|
||||
|
||||
Action taken: Detection only.
|
||||
Please investigate manually.
|
||||
"
|
||||
|
||||
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
|
||||
|
||||
L'antivirus de votre serveur a détecté une menace potentielle lors de l'analyse périodique.
|
||||
Votre administrateur a été notifié avec les détails techniques.
|
||||
Nous vous conseillons d'être prudent avec vos fichiers récents.
|
||||
"
|
||||
fi
|
||||
|
||||
printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
|
||||
printf "Subject: [Alerte] Menace détectée sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.clamav-virus-notify = {
|
||||
description = "Email notification for ClamAV virus detection";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${clamav_notifier}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./disk-space.nix
|
||||
./smart.nix
|
||||
./smtp.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.disk-space-checker;
|
||||
|
||||
disk_space_notifier = pkgs.writeScript "disk-space-notifier.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
ALERT_FILE="/var/lib/numbus-server/disk_alert.env"
|
||||
if [ ! -f "$ALERT_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
source "$ALERT_FILE"
|
||||
rm "$ALERT_FILE"
|
||||
|
||||
# Update the timestamp for this specific path to prevent spamming
|
||||
SAFE_PATH=$(echo "$DISK_ALERT_PATH" | tr '/' '_')
|
||||
date +%s > "/var/lib/numbus-server/last_alert_$SAFE_PATH.ts"
|
||||
|
||||
ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}"
|
||||
USER_EMAIL="${config.numbus-server.mail.userAddress}"
|
||||
OWNER_NAME="${config.numbus-server.owner}"
|
||||
|
||||
SUBJECT="Numbus Server Alert: Low Disk Space Detected"
|
||||
|
||||
TECH_BODY="
|
||||
Disk Space Alert:
|
||||
Server owner: $OWNER_NAME
|
||||
|
||||
The following mount point has exceeded the safety threshold:
|
||||
Mount: $DISK_ALERT_PATH
|
||||
Usage: $DISK_ALERT_USAGE%
|
||||
|
||||
Full partition details:
|
||||
$(df -h "$DISK_ALERT_PATH")
|
||||
|
||||
Action required: Please investigate and clear space or expand the storage capacity.
|
||||
"
|
||||
|
||||
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
|
||||
|
||||
L'espace de stockage de votre serveur Numbus est presque saturé.
|
||||
Disque concerné : $DISK_ALERT_PATH ($DISK_ALERT_USAGE% utilisé)
|
||||
|
||||
Votre administrateur a été notifié avec les détails techniques.
|
||||
Nous vous conseillons d'éviter d'ajouter des fichiers volumineux pour garantir le bon fonctionnement de vos services.
|
||||
Contactez votre administrateur afin d'évoquer les possibilités d'expansion du stockage.
|
||||
"
|
||||
|
||||
printf "Subject: [ADMIN] %s\n\n%s" "$SUBJECT" "$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
|
||||
printf "Subject: [Alerte] Espace disque presque saturé sur votre serveur Numbus\n\n%s\n\nMerci de votre confiance,\nL'équipe de support,\nNumbus-Server." "$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
|
||||
'';
|
||||
|
||||
disk_space_checker = pkgs.writeScript "disk-space-checker.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
# Safety threshold in percentage
|
||||
THRESHOLD=90
|
||||
# Paths to monitor (Root and MergerFS data pool)
|
||||
PATHS=("/" "/mnt/data")
|
||||
ALERT_FILE="/var/lib/numbus-server/disk_alert.env"
|
||||
|
||||
for path in "''${PATHS[@]}"; do
|
||||
# Skip if path does not exist (e.g. if mergerfs is not mounted yet)
|
||||
if [ ! -d "$path" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Anti-spam logic: Check if we alerted on this path recently (7 days = 604800 seconds)
|
||||
SAFE_PATH=$(echo "$path" | tr '/' '_')
|
||||
TS_FILE="/var/lib/numbus-server/last_alert_$SAFE_PATH.ts"
|
||||
NOW=$(date +%s)
|
||||
|
||||
if [ -f "$TS_FILE" ]; then
|
||||
LAST_SENT=$(cat "$TS_FILE")
|
||||
DIFF=$((NOW - LAST_SENT))
|
||||
if [ "$DIFF" -lt 604800 ]; then
|
||||
echo "Alert for $path was sent recently. Skipping notification to avoid spam."
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract usage percentage using df
|
||||
USAGE=$(df -h "$path" | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
|
||||
if [ "$USAGE" -ge "$THRESHOLD" ]; then
|
||||
echo "DISK_ALERT_PATH=$path" > "$ALERT_FILE"
|
||||
echo "DISK_ALERT_USAGE=$USAGE" >> "$ALERT_FILE"
|
||||
|
||||
echo "Threshold exceeded for $path ($USAGE%). Triggering notification."
|
||||
|
||||
# Trigger the notification service
|
||||
/run/current-system/sw/bin/systemctl start disk-space-notifier.service
|
||||
|
||||
# We exit after the first alert to avoid multiple overlapping emails in one run
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.disk-space-notifier = {
|
||||
description = "Email notification for low disk space";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${disk_space_notifier}";
|
||||
};
|
||||
};
|
||||
systemd.services.disk-space-checker = {
|
||||
description = "Check for low disk space";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${disk_space_checker}";
|
||||
};
|
||||
};
|
||||
systemd.timers.disk-space-checker = {
|
||||
description = "Run disk space check every day";
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
};
|
||||
wantedBy = [ "timers.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
smartd_notifier = pkgs.writeScript "smartd-notify.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
# 1. Send Technical Email to Admin
|
||||
ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}"
|
||||
SUBJECT="Numbus Server Alert: $SMARTD_FAILTYPE on $SMARTD_DEVICE"
|
||||
|
||||
TECH_BODY="
|
||||
SMARTD Alert Details:
|
||||
Server owner: $OWNER_NAME
|
||||
Device: $SMARTD_DEVICE
|
||||
Type: $SMARTD_DEVICETYPE
|
||||
Failure Type: $SMARTD_FAILTYPE
|
||||
Message: $SMARTD_MESSAGE
|
||||
|
||||
Full Message:
|
||||
$SMARTD_FULLMESSAGE
|
||||
"
|
||||
printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
|
||||
|
||||
# 2. Send Friendly Email to Owner
|
||||
USER_EMAIL="${config.numbus-server.mail.userAddress}"
|
||||
OWNER_NAME="${config.numbus-server.owner}"
|
||||
|
||||
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
|
||||
|
||||
Votre serveur a automatiquement détecté une panne matérielle de disque dur.
|
||||
Ce genre de panne est tout à fait normal selon l'âge de votre matériel et n'entraîne
|
||||
dans la grande majorité des cas aucune perte de données grâce au système de
|
||||
stockage redondant préventif.
|
||||
|
||||
Votre administrateur a été notifié de cette panne. Il vous recontactera dans de très
|
||||
brefs délais afin de procéder au remplacement, si nécessaire, du disque dur défaillant.
|
||||
|
||||
Merci de votre confiance,
|
||||
L'équipe de support,
|
||||
Numbus-Server."
|
||||
|
||||
printf "Subject: [Alerte] Défaillance matérielle sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
services.smartd = {
|
||||
enable = true;
|
||||
defaults.autodetected = "-a -o on -S on -s (S/../.././00|L/../../6/01) -n standby,q -M exec ${smartd_notifier}";
|
||||
notifications = {
|
||||
wall = {
|
||||
enable = true;
|
||||
};
|
||||
mail = {
|
||||
enable = true;
|
||||
sender = config.numbus-server.mail.fromAddress;
|
||||
recipient = "${config.numbus-server.mail.userAddress},${config.numbus-server.mail.adminAddress}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.mail;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus.mail = {
|
||||
enable = mkEnableOption "Email sending functionality";
|
||||
|
||||
userAddress = mkOption {
|
||||
description = "The address of the user this server will send emails to";
|
||||
type = types.str;
|
||||
example = "user@your-domain.com";
|
||||
};
|
||||
|
||||
adminAddress = mkOption {
|
||||
description = "The address of the admin this server will send emails to";
|
||||
type = types.str;
|
||||
example = "admin@your-domain.com";
|
||||
};
|
||||
|
||||
smtpUsername = mkOption {
|
||||
description = "The username/email that will be use to authenticate to the SMTP server";
|
||||
type = types.str;
|
||||
example = "your-smtp-enabled-address@your-domain.com";
|
||||
};
|
||||
|
||||
smtpPasswordPath = mkOption {
|
||||
description = "The path to a file containing the password that will be use to authenticate to the SMTP server";
|
||||
type = types.path;
|
||||
example = /run/secrets/system/mail/smtpPassword;
|
||||
};
|
||||
|
||||
fromAddress = mkOption {
|
||||
description = "This server will send emails from this address";
|
||||
type = types.str;
|
||||
default = "numbus-server-noreply@${config.numbus.services.domain}";
|
||||
example = "numbus-server-noreply@your-domain.com";
|
||||
};
|
||||
|
||||
smtpServer = mkOption {
|
||||
description = "The SMTP server address your server will use to send emails";
|
||||
type = types.str;
|
||||
default = "smtp.gmail.com";
|
||||
example = "smtp.your-provider.com";
|
||||
};
|
||||
|
||||
smtpPort = mkOption {
|
||||
description = "The SMTP port your server will connect to to send emails";
|
||||
type = types.port;
|
||||
default = 587;
|
||||
example = 587;
|
||||
};
|
||||
|
||||
smtpEncryption = mkOption {
|
||||
description = "The encryption method for SMTP : NONE (NOT RECOMMENDED), TLS (port 465, also called SSL), or STARTTLS (port 587). STARTTLS is recommended.";
|
||||
type = types.enum [ "NONE" "TLS" "STARTTLS" ];
|
||||
default = "STARTTLS";
|
||||
example = "STARTTLS";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
sops.secrets."smtpPassword" = {
|
||||
sopsFile = /etc/nixos/secrets/system/mail.yaml;
|
||||
owner = "numbus-admin";
|
||||
mode = "0600";
|
||||
};
|
||||
|
||||
environment.etc."aliases" ={
|
||||
mode = "0440";
|
||||
text = ''
|
||||
root: ${cfg.userAddress}, ${cfg.adminAddress}
|
||||
'';
|
||||
};
|
||||
|
||||
programs.msmtp = {
|
||||
enable = true;
|
||||
defaults = {
|
||||
aliases = "/etc/aliases";
|
||||
timeout = 60;
|
||||
syslog = "on";
|
||||
};
|
||||
accounts.default = {
|
||||
auth = true;
|
||||
host = cfg.smtpServer;
|
||||
port = cfg.smtpPort;
|
||||
from = cfg.fromAddress;
|
||||
user = cfg.smtpUsername;
|
||||
tls = true;
|
||||
tls_starttls = true;
|
||||
passwordeval = "${pkgs.coreutils}/bin/cat ${cfg.smtpPasswordPath}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
systemd_notifier = pkgs.writeScript "systemd-email-notify.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
# The failing service name is passed as the first argument
|
||||
UNIT=$1
|
||||
|
||||
# 1. Send Technical Email to Admin
|
||||
ADMIN_EMAIL="${config.numbus-server.mail.adminAddress}"
|
||||
SUBJECT="Numbus Server Alert: Service $UNIT Failed"
|
||||
|
||||
# Retrieve recent logs for context
|
||||
LOGS=$(journalctl -u "$UNIT" -n 20 --no-pager)
|
||||
|
||||
TECH_BODY="
|
||||
Systemd Service Failure Alert:
|
||||
Server owner: ${config.numbus-server.owner}
|
||||
Service: $UNIT
|
||||
|
||||
Recent Logs:
|
||||
$LOGS
|
||||
"
|
||||
printf "Subject: [ADMIN] $SUBJECT\n\n$TECH_BODY" | /run/wrappers/bin/sendmail -t "$ADMIN_EMAIL"
|
||||
|
||||
# 2. Send Friendly Email to Owner
|
||||
USER_EMAIL="${config.numbus-server.mail.userAddress}"
|
||||
OWNER_NAME="${config.numbus-server.owner}"
|
||||
|
||||
FRIENDLY_BODY="Cher/Chère $OWNER_NAME,
|
||||
|
||||
Votre serveur a détecté une défaillance du service $UNIT.
|
||||
Le système a tenté de gérer l'erreur, mais une intervention peut être nécessaire.
|
||||
|
||||
Votre administrateur a été notifié de cet incident avec les détails techniques nécessaires.
|
||||
Il interviendra si une action manuelle est requise.
|
||||
|
||||
Merci de votre confiance,
|
||||
L'équipe de support,
|
||||
Numbus-Server."
|
||||
|
||||
printf "Subject: [Alerte] Erreur sur votre serveur Numbus\n\n$FRIENDLY_BODY" | /run/wrappers/bin/sendmail -t "$USER_EMAIL"
|
||||
'';
|
||||
in
|
||||
{
|
||||
systemd.services."service-failure-notify@" = {
|
||||
description = "Email notification for failed service %i";
|
||||
onFailure = [ ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${systemd_notifier} %i";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{ config, deviceType, ... }:
|
||||
|
||||
{
|
||||
config = mkIf (deviceType == "computer" || deviceType == "tv" ) {
|
||||
# Enable sound with pipewire.
|
||||
services.pulseaudio.enable = false;
|
||||
security.rtkit.enable = true;
|
||||
services.pipewire = {
|
||||
enable = true;
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true;
|
||||
pulse.enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./audio.nix
|
||||
./internationalisation.nix
|
||||
./power.nix
|
||||
./printer.nix
|
||||
./update.nix
|
||||
./users.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus.internationalization;
|
||||
in
|
||||
|
||||
{
|
||||
config = {
|
||||
i18n.defaultLocale = "${cfg.locale}.UTF-8";
|
||||
i18n.extraLocaleSettings = {
|
||||
LC_ADDRESS = "${cfg.locale}.UTF-8";
|
||||
LC_IDENTIFICATION = "${cfg.locale}.UTF-8";
|
||||
LC_MEASUREMENT = "${cfg.locale}.UTF-8";
|
||||
LC_MONETARY = "${cfg.locale}.UTF-8";
|
||||
LC_NAME = "${cfg.locale}.UTF-8";
|
||||
LC_NUMERIC = "${cfg.locale}.UTF-8";
|
||||
LC_PAPER = "${cfg.locale}.UTF-8";
|
||||
LC_TELEPHONE = "${cfg.locale}.UTF-8";
|
||||
LC_TIME = "${cfg.locale}.UTF-8";
|
||||
};
|
||||
|
||||
console.keyMap = toLower cfg.language;
|
||||
services.xserver.xkb = {
|
||||
layout = toLower cfg.language;
|
||||
variant = "";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
services.autoaspm.enable = true;
|
||||
powerManagement.powertop.enable = true;
|
||||
boot.kernelParams = [
|
||||
"pcie_aspm=force"
|
||||
"consoleblank=60"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
config = mkIf (deviceType == "computer" || deviceType == "tv" ) {
|
||||
# Enable CUPS to print documents.
|
||||
services.printing.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ config, inputs, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
allowReboot = false;
|
||||
flake = inputs.self.outPath;
|
||||
flags = [ "--print-build-logs" ];
|
||||
dates = "02:00";
|
||||
randomizedDelaySec = "45min";
|
||||
};
|
||||
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 7d";
|
||||
};
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
nix.settings.auto-optimise-store = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.numbus;
|
||||
in
|
||||
|
||||
{
|
||||
users.users.numbus-admin = {
|
||||
shell = pkgs.fish;
|
||||
isNormalUser = true;
|
||||
description = cfg.owner;
|
||||
extraGroups = [ "wheel" ];
|
||||
uid = 1000;
|
||||
initialPassword = "changeMe!";
|
||||
# required for auto start before user login
|
||||
linger = true;
|
||||
# required for rootless container with multiple users
|
||||
autoSubUidGidRange = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./fail2ban.nix
|
||||
./flatpaks.nix
|
||||
./numbus-cli.nix
|
||||
./ssh.nix
|
||||
./terminal.nix
|
||||
./updates.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
services.fail2ban.enable = true;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
config = mkIf (services.flatpak.packages != []) {
|
||||
services.flatpak.enable = true;
|
||||
services.flatpak.update.auto.enable = true;
|
||||
services.flatpak.uninstallUnmanaged = true;
|
||||
|
||||
services.flatpak.remotes = mkOptionDefault [{
|
||||
name = "flathub";
|
||||
location = "https://dl.flathub.org/repo/flathub.flatpakrepo";
|
||||
}];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# Base script header and common setup for all device types
|
||||
baseScriptHeader = ''
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# The device type is baked into the script at build time
|
||||
readonly NUMBUS_DEVICE_TYPE="${deviceType}"
|
||||
|
||||
# Common utility function for consistent output
|
||||
numbus_echo() {
|
||||
echo "[Numbus CLI - $NUMBUS_DEVICE_TYPE] $*"
|
||||
}
|
||||
'';
|
||||
|
||||
# --- Device-specific script definitions ---
|
||||
|
||||
serverScript = baseScriptHeader + ''
|
||||
case "$1" in
|
||||
test)
|
||||
numbus_echo "Hello World! This is a Numbus Server."
|
||||
;;
|
||||
status)
|
||||
numbus_echo "Checking system status for Server..."
|
||||
numbus_echo "--- Podman Containers ---"
|
||||
podman ps || numbus_echo "No Podman containers found or Podman not running."
|
||||
systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found."
|
||||
;;
|
||||
upgrade)
|
||||
numbus_echo "Pulling latest configuration and upgrading for Server..."
|
||||
# Add server-specific upgrade logic here (e.g., nixos-rebuild switch)
|
||||
;;
|
||||
*)
|
||||
numbus_echo "Numbus CLI (Server edition)"
|
||||
echo ""
|
||||
echo "Usage: numbus <command>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " test - Print a test message"
|
||||
numbus_echo " status - Show status of Numbus services (Podman, systemd)"
|
||||
numbus_echo " upgrade - Upgrade the server configuration"
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
|
||||
backupScript = baseScriptHeader + ''
|
||||
case "$1" in
|
||||
test)
|
||||
numbus_echo "Hello World! This is a Numbus Backup Server."
|
||||
;;
|
||||
status)
|
||||
numbus_echo "Checking system status for Backup Server..."
|
||||
systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found."
|
||||
# Add backup-specific status checks here (e.g., SnapRAID status, rsync jobs)
|
||||
;;
|
||||
restore)
|
||||
numbus_echo "Starting interactive restore wizard for Backup Server..."
|
||||
# Add backup-specific restore logic here
|
||||
;;
|
||||
upgrade)
|
||||
numbus_echo "Pulling latest configuration and upgrading for Backup Server..."
|
||||
# Add backup-specific upgrade logic here
|
||||
;;
|
||||
*)
|
||||
numbus_echo "Numbus CLI (Backup Server edition)"
|
||||
echo ""
|
||||
echo "Usage: numbus <command>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
numbus_echo " test - Print a test message"
|
||||
numbus_echo " status - Show status of Numbus services"
|
||||
numbus_echo " restore - Start interactive restore wizard"
|
||||
numbus_echo " upgrade - Upgrade the backup server configuration"
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
|
||||
computerScript = baseScriptHeader + ''
|
||||
case "$1" in
|
||||
test)
|
||||
numbus_echo "Hello World! This is a Numbus Computer."
|
||||
;;
|
||||
status)
|
||||
numbus_echo "Checking system status for Computer..."
|
||||
systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found."
|
||||
# Add computer-specific status checks (e.g., GPU status, Flatpak updates)
|
||||
;;
|
||||
upgrade)
|
||||
numbus_echo "Pulling latest configuration and upgrading for Computer..."
|
||||
# Add computer-specific upgrade logic here
|
||||
;;
|
||||
*)
|
||||
numbus_echo "Numbus CLI (Computer edition)"
|
||||
echo ""
|
||||
echo "Usage: numbus <command>"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
numbus_echo " test - Print a test message"
|
||||
numbus_echo " status - Show status of Numbus services"
|
||||
numbus_echo " upgrade - Upgrade the computer configuration"
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
|
||||
tvScript = baseScriptHeader + ''
|
||||
case "$1" in
|
||||
test)
|
||||
numbus_echo "Hello World! This is a Numbus TV."
|
||||
;;
|
||||
status)
|
||||
numbus_echo "Checking system status for TV..."
|
||||
systemctl list-units --type=service "numbus-*" --no-pager || numbus_echo "No Numbus services found."
|
||||
# Add TV-specific status checks (e.g., media server status, remote connectivity)
|
||||
;;
|
||||
remote)
|
||||
numbus_echo "Pairing a new Bluetooth remote for TV..."
|
||||
# Add TV-specific remote pairing logic here
|
||||
;;
|
||||
upgrade)
|
||||
numbus_echo "Pulling latest configuration and upgrading for TV..."
|
||||
# Add TV-specific upgrade logic here
|
||||
;;
|
||||
*)
|
||||
numbus_echo "Numbus CLI (TV edition)"
|
||||
echo ""
|
||||
echo "Usage: numbus <command>"
|
||||
echo ""
|
||||
numbus_echo "Commands:"
|
||||
numbus_echo " test - Print a test message"
|
||||
numbus_echo " status - Show status of Numbus services"
|
||||
numbus_echo " remote - Pair a new Bluetooth remote"
|
||||
numbus_echo " upgrade - Upgrade the TV configuration"
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# Use lib.switch to select the correct script based on deviceType
|
||||
selectedScript = lib.switch deviceType {
|
||||
server = serverScript;
|
||||
backup = backupScript;
|
||||
computer = computerScript;
|
||||
tv = tvScript;
|
||||
} (throw "Unknown Numbus device type: ${deviceType}"); # Fail if an unknown deviceType is encountered
|
||||
|
||||
# Define the numbus-cli package using the selected script
|
||||
numbus = pkgs.writeShellScriptBin "numbus" selectedScript;
|
||||
|
||||
in {
|
||||
environment.systemPackages = [ numbus ];
|
||||
|
||||
# Add a useful alias so people can check the type via env
|
||||
environment.variables.NUMBUS_DEVICE_TYPE = deviceType;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
config.services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
KbdInteractiveAuthentication = false;
|
||||
PermitRootLogin = "no";
|
||||
};
|
||||
AllowUsers = [ "numbus-admin" ];
|
||||
ports = [ 245 ]
|
||||
};
|
||||
|
||||
config.sops.secrets."authorizedSshPublicKeys" = {
|
||||
sopsFile = /etc/nixos/secrets/system/ssh.yaml;
|
||||
mode = "0440";
|
||||
owner = "numbus-admin";
|
||||
path = "/home/numbus-admin/.ssh/authorized_keys";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
fish
|
||||
fishPlugins.fzf-fish
|
||||
fishPlugins.grc
|
||||
grc
|
||||
fzf
|
||||
];
|
||||
|
||||
programs.fish = {
|
||||
enable = true;
|
||||
interactiveShellInit = ''
|
||||
set fish_greeting # Disable greeting
|
||||
fastfetch
|
||||
echo -e "\n\nWelcome to Numbus !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support to get assistance\n- Have a nice day and enjoy !"
|
||||
'';
|
||||
shellAliases = {
|
||||
nixup = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch --upgrade && cd -";
|
||||
nixwitch = "cd /etc/nixos/ && sudo nix flake update && sudo nixos-rebuild --flake . switch && cd -";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ config, inputs, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
allowReboot = false;
|
||||
flake = inputs.self.outPath;
|
||||
flags = [ "--print-build-logs" ];
|
||||
dates = "21:00";
|
||||
randomizedDelaySec = "45min";
|
||||
};
|
||||
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 7d";
|
||||
};
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
nix.settings.auto-optimise-store = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
cockpit-numbus = pkgs.stdenv.mkDerivation {
|
||||
name = "cockpit-numbus";
|
||||
src = ./cockpit-numbus;
|
||||
installPhase = ''
|
||||
mkdir -p $out/share/cockpit/numbus
|
||||
cp -r * $out/share/cockpit/numbus
|
||||
'';
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
services.cockpit = {
|
||||
enable = true;
|
||||
port = 9090;
|
||||
openFirewall = false;
|
||||
settings = {
|
||||
WebService = {
|
||||
AllowUnencrypted = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Link the extension into the system cockpit path
|
||||
environment.systemPackages = [ cockpit-numbus ];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports=[
|
||||
./hardware/default.nix
|
||||
./misc/default.nix
|
||||
./packages/default.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{ lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
options.numbus-computer = {
|
||||
owner = mkOption {
|
||||
type = types.str;
|
||||
example = "Alex";
|
||||
default = "Numbus";
|
||||
description = "The name of the person who owns this computer";
|
||||
};
|
||||
language = mkOption {
|
||||
type = types.str;
|
||||
example = "FR";
|
||||
default = "FR";
|
||||
description = "The language for this computer";
|
||||
};
|
||||
keyboardLayout = mkOption {
|
||||
type = types.str;
|
||||
example = "FR";
|
||||
default = "FR";
|
||||
description = "The keyboard layout for this computer";
|
||||
};
|
||||
locale = mkOption {
|
||||
type = types.str;
|
||||
example = "fr_FR";
|
||||
default = "fr_FR";
|
||||
description = "The default locale for this computer";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports=[
|
||||
./audio.nix
|
||||
./internationalization.nix
|
||||
./networking.nix
|
||||
./printer.nix
|
||||
./users.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
# Enable networking
|
||||
networking.networkmanager.enable = true;
|
||||
networking.hostName = "numbus-computer";
|
||||
|
||||
# Open ports in the firewall.
|
||||
networking.firewall.allowedTCPPorts = [ ];
|
||||
networking.firewall.allowedUDPPorts = [ ];
|
||||
networking.firewall.enable = true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports=[
|
||||
./desktop-environment.nix
|
||||
./flatpaks.nix
|
||||
./terminal.nix
|
||||
./updates.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-computer.packages.desktop;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-computer.packages.desktop = {
|
||||
gnome = {
|
||||
enable = mkEnableOption "Wether to enable the GNOME desktop environment.";
|
||||
|
||||
extensions = mkOption {
|
||||
type = types.listOf types.str ;
|
||||
default = [];
|
||||
example = [ "dash-to-dock" "caffeine" "clipboard-history" "appindicator-support" ];
|
||||
description = "Extensions to add to the Gnome desktop environment to improve your experience.";
|
||||
};
|
||||
};
|
||||
kde_plasma = {
|
||||
enable = mkEnableOption "Wether to enable the KDE Plasma desktop environment.";
|
||||
};
|
||||
hyprland = {
|
||||
enable = mkEnableOption "Wether to enable the hyprland desktop environment.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
# GNOME
|
||||
(mkIf (cfg.gnome.enable == true) {
|
||||
services.xserver.enable = false;
|
||||
services.displayManager.gdm.enable = true;
|
||||
services.desktopManager.gnome.enable = true;
|
||||
services.gnome.core-apps.enable = false;
|
||||
services.gnome.core-developer-tools.enable = false;
|
||||
services.gnome.games.enable = false;
|
||||
environment.gnome.excludePackages = with pkgs; [ gnome-tour gnome-user-docs ];
|
||||
})
|
||||
|
||||
# GNOME extensions
|
||||
(mkIf (cfg.gnome.enable == true && cfg.desktop.gnome.extensions != [ ]) {
|
||||
environment.systemPackages = map (ext: pkgs.gnomeExtensions.${ext}) cfg.desktop.gnome.extensions;
|
||||
})
|
||||
|
||||
# KDE Plasma
|
||||
(mkIf (cfg.kde_plasma.enable == true) {
|
||||
services = {
|
||||
desktopManager.plasma6.enable = true;
|
||||
displayManager.sddm.enable = true;
|
||||
displayManager.sddm.wayland.enable = true;
|
||||
};
|
||||
environment.systemPackages = with pkgs; [
|
||||
kdePackages.discover
|
||||
kdePackages.kcalc
|
||||
kdePackages.kcharselect
|
||||
kdePackages.kclock
|
||||
kdePackages.kcolorchooser
|
||||
kdePackages.kolourpaint
|
||||
kdePackages.sddm-kcm
|
||||
kdiff3
|
||||
wayland-utils
|
||||
wl-clipboard
|
||||
];
|
||||
})
|
||||
|
||||
# Hyprland
|
||||
(mkIf (cfg.hyprland.enable == true) {
|
||||
programs.hyprland.enable = true;
|
||||
environment.systemPackages = [
|
||||
pkgs.kitty
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./dns-services/default.nix
|
||||
./hardware/default.nix
|
||||
./mail/default.nix
|
||||
./misc/default.nix
|
||||
./networking/default.nix
|
||||
./packages/default.nix
|
||||
./system-services/default.nix
|
||||
./web-applications/default.nix
|
||||
./global.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./firewall.nix
|
||||
./networking.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
config = {
|
||||
networking.nftables.enable = true;
|
||||
networking.firewall = {
|
||||
enable = true;
|
||||
allowPing = true;
|
||||
allowedTCPPorts = [ 53 80 443 ];
|
||||
allowedUDPPorts = [ 53 443 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.networking;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.networking = {
|
||||
ipAddress = mkOption {
|
||||
description = "The IP address that this server will use";
|
||||
type = types.str;
|
||||
example = "192.168.1.100";
|
||||
};
|
||||
interface = mkOption {
|
||||
description = "The interface that this server will use to connect to the network";
|
||||
type = types.str;
|
||||
example = "enp1s0";
|
||||
};
|
||||
routerIpAddress = mkOption {
|
||||
description = "The IP address of the router of your network";
|
||||
type = types.str;
|
||||
example = "192.168.1.1";
|
||||
};
|
||||
networkSubnet = mkOption {
|
||||
description = "The subnet of your network";
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "192.168.1.0/24";
|
||||
};
|
||||
dnsServers = mkOption {
|
||||
description = "The list of DNS servers that this server will use";
|
||||
type = types.listOf types.str;
|
||||
default = [ "${cfg.ipAddress}" "9.9.9.9" ];
|
||||
example = [ "${cfg.ipAddress}" "9.9.9.9" ];
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
networking.hostName = "numbus-server";
|
||||
networking.networkmanager.enable = false;
|
||||
|
||||
# Allow rootless containers to bind to port 53 and up
|
||||
boot.kernel.sysctl."net.ipv4.ip_unprivileged_port_start" = 53;
|
||||
|
||||
networking.bridges.br0.interfaces = [ cfg.interface ];
|
||||
networking.interfaces."${cfg.interface}".useDHCP = false;
|
||||
networking.interfaces.br0.useDHCP = false;
|
||||
networking.nameservers = cfg.dnsServers;
|
||||
networking.interfaces.br0.ipv4.addresses = [{
|
||||
address = cfg.ipAddress;
|
||||
prefixLength = 24;
|
||||
}];
|
||||
networking.defaultGateway = {
|
||||
address = cfg.routerIpAddress;
|
||||
interface = "br0";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
# Secrets
|
||||
age
|
||||
sops
|
||||
# HDD tools
|
||||
hdparm
|
||||
hd-idle
|
||||
hddtemp
|
||||
smartmontools
|
||||
ncdu
|
||||
# CPU tools
|
||||
cpufrequtils
|
||||
intel-gpu-tools
|
||||
# Filesystem tools
|
||||
snapraid
|
||||
mergerfs
|
||||
mergerfs-tools
|
||||
# Powersave tools
|
||||
powertop
|
||||
# PCI devices tools
|
||||
pciutils
|
||||
tpm2-tss
|
||||
# Misc
|
||||
git
|
||||
fastfetch
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./packages.nix
|
||||
./podman.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
virtualisation.podman.enable = true;
|
||||
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
|
||||
|
||||
virtualisation.containers.containersConf.settings = {
|
||||
network.default_rootless_network_cmd = "slirp4netns";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
podman-compose
|
||||
podman-tui
|
||||
slirp4netns
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{ lib, config, pkgs }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
mkPodmanService = {
|
||||
name,
|
||||
description,
|
||||
defaultPort ? "0",
|
||||
defaultSubdomain ? name,
|
||||
pod ? "false",
|
||||
reverseProxied ? true,
|
||||
composeText,
|
||||
scheme ? "http",
|
||||
middlewares ? null,
|
||||
dependencies ? [ "sops-install-secrets.service" "traefik.service" "authelia.service" "${config.numbus-server.services.dns}.service" ],
|
||||
extraOptions ? {},
|
||||
extraConfig ? {},
|
||||
configDirEnabled ? true,
|
||||
dataDirEnabled ? true,
|
||||
startDelay ? 180,
|
||||
dirPermissions ? [],
|
||||
secrets ? [],
|
||||
envFile ? null,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.${name};
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.services.${name} = recursiveUpdate ({
|
||||
enable = mkEnableOption description;
|
||||
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = defaultSubdomain;
|
||||
example = defaultSubdomain;
|
||||
description = "The subdomain that ${name} will use";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.str;
|
||||
default = defaultPort;
|
||||
example = defaultPort;
|
||||
description = "The port that ${name} will use.";
|
||||
};
|
||||
|
||||
reverseProxied = mkOption {
|
||||
type = types.bool;
|
||||
default = reverseProxied;
|
||||
example = reverseProxied;
|
||||
description = "Whether to create a basic Traefik reverse proxy configuration for this service. You might need to set it to false for custom configurations or services that don't need to be reverse proxied.";
|
||||
};
|
||||
} // (optionalAttrs configDirEnabled {
|
||||
configDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/mnt/config/${name}";
|
||||
example = "/mnt/config/${name}";
|
||||
description = "The directory where ${name}'s configuration files will be stored";
|
||||
};
|
||||
}) // (optionalAttrs dataDirEnabled {
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/mnt/data/${name}";
|
||||
example = "/mnt/data/${name}";
|
||||
description = "The directory where ${name}'s data will be stored";
|
||||
};
|
||||
})) extraOptions;
|
||||
|
||||
config = mkIf cfg.enable (mkMerge [
|
||||
{
|
||||
# Compose file
|
||||
sops.templates."podman/${name}" = {
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
content = composeText;
|
||||
path = "/etc/podman/${name}/compose.yaml";
|
||||
};
|
||||
|
||||
# Traefik config
|
||||
sops.templates."traefik/rules/${name}" = mkIf cfg.reverseProxied {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
${name}:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: ${name}
|
||||
middlewares:
|
||||
${concatStringsSep "\n" (map (m: " - ${m}") middlewares)}
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
services:
|
||||
${name}:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "${scheme}://host.containers.internal:${cfg.port}"
|
||||
'';
|
||||
path = "/etc/traefik/rules/${name}.yaml";
|
||||
};
|
||||
|
||||
# Secrets config
|
||||
sops.secrets = genAttrs secrets (secretPath: {
|
||||
sopsFile = "/etc/nixos/secrets/podman/${name}.yaml";
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
});
|
||||
|
||||
# SystemD config
|
||||
systemd.services."${name}" = {
|
||||
description = "Podman container : ${name}";
|
||||
after = dependencies;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.podman pkgs.podman-compose pkgs.slirp4netns pkgs.su pkgs.sudo pkgs.coreutils ];
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
TimeoutStartSec = "1000";
|
||||
ExecStartPre = [
|
||||
"${pkgs.bash}/bin/bash -c 'sleep $((RANDOM % ${toString startDelay}))'"
|
||||
"${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose -f /etc/podman/${name}/compose.yaml pull'"
|
||||
];
|
||||
ExecStart = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml up --remove-orphans'";
|
||||
ExecStop = "${pkgs.bash}/bin/bash -c 'export PATH=/run/wrappers/bin:$PATH; exec ${pkgs.sudo}/bin/sudo -u numbus-admin podman-compose --in-pod ${toString pod} -f /etc/podman/${name}/compose.yaml down'";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "3m";
|
||||
};
|
||||
};
|
||||
|
||||
# Permissions config
|
||||
systemd.services."${name}-permissions" = mkIf (dirPermissions != []) {
|
||||
description = "Podman container : ${name} : check and fix permissions";
|
||||
before = [ "${name}.service" ];
|
||||
wantedBy = [ "multi-user.target" "${name}.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5m";
|
||||
};
|
||||
script = ''
|
||||
${concatStringsSep "\n" (map (perm: ''
|
||||
set -- ${perm}
|
||||
WANTED_PERMISSIONS=$1
|
||||
FOLDER_PATH=$2
|
||||
|
||||
if [[ ! -e "$FOLDER_PATH" ]]; then
|
||||
mkdir -p "$FOLDER_PATH"
|
||||
elif [[ ! -d "$FOLDER_PATH" ]]; then
|
||||
rm "$FOLDER_PATH"
|
||||
mkdir -p "$FOLDER_PATH"
|
||||
fi
|
||||
|
||||
ACTUAL_PERMISSIONS=$(stat -c '%u:%g' "$FOLDER_PATH")
|
||||
if [[ "$ACTUAL_PERMISSIONS" != "$WANTED_PERMISSIONS" ]]; then
|
||||
chown -R "$WANTED_PERMISSIONS" "$FOLDER_PATH"
|
||||
fi
|
||||
'') dirPermissions)}
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
}
|
||||
extraConfig
|
||||
]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "authelia";
|
||||
# Version tagging
|
||||
autheliaVersion = "v4.39.16";
|
||||
databaseVersion = "18.3";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.authelia;
|
||||
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
|
||||
domainParts = splitString "." config.numbus-server.services.domain;
|
||||
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
|
||||
# Generate dynamic access control rules based on groups and allowedApps
|
||||
mkGroupRule = groupName: appName:
|
||||
let
|
||||
app = config.numbus-server.service.${appName} or {};
|
||||
in
|
||||
if app ? subdomain && app ? domain then ''
|
||||
- domain: "${app.subdomain}.${app.domain}"
|
||||
policy: two_factor
|
||||
subject: "group:${groupName}"''
|
||||
else "";
|
||||
allGroupRules = concatStringsSep "\n" (filter (s: s != "") (flatten (mapAttrsToList (groupName: groupCfg:
|
||||
map (appName: mkGroupRule groupName appName) (groupCfg.allowedApps or [])
|
||||
) (config.numbus-server.groups or {}))));
|
||||
|
||||
defaultRedirectionUrl =
|
||||
if config.numbus-server.services.homepage.enable then
|
||||
"https://${config.numbus-server.services.homepage.subdomain}.${config.numbus-server.services.domain}"
|
||||
else if config.numbus-server.services.dashy.enable then
|
||||
"https://${config.numbus-server.services.dashy.subdomain}.${config.numbus-server.services.domain}"
|
||||
else null;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = name;
|
||||
description = "Authelia, your own unified login provider";
|
||||
defaultPort = "9091";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"authelia/db_name"
|
||||
"authelia/db_username"
|
||||
"authelia/db_password"
|
||||
"authelia/jwt_secret"
|
||||
"authelia/session_secret"
|
||||
"authelia/storage_secret"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
authelia-server:
|
||||
image: ghcr.io/authelia/authelia:${autheliaVersion}
|
||||
container_name: authelia-server
|
||||
hostname: authelia-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
authelia:
|
||||
ipv4_address: 10.89.251.253
|
||||
ports:
|
||||
- "${cfg.port}:9091/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/server:/config
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
authelia-database:
|
||||
container_name: authelia-database
|
||||
hostname: authelia-database
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
authelia:
|
||||
ipv4_address: 10.89.251.252
|
||||
environment:
|
||||
POSTGRES_DB: ${config.sops.placeholder."authelia/db_name"}
|
||||
POSTGRES_USER: ${config.sops.placeholder."authelia/db_username"}
|
||||
POSTGRES_PASSWORD: ${config.sops.placeholder."authelia/db_password"}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
authelia:
|
||||
driver: bridge
|
||||
name: authelia
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.251.0/24"
|
||||
gateway: "10.89.251.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."authelia-config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
authelia:
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: "${config.sops.placeholder."authelia/jwt_secret"}"
|
||||
jwt_lifespan: "5 minutes"
|
||||
jwt_algorithm: "HS256"
|
||||
storage:
|
||||
encryption_key: "${config.sops.placeholder."authelia/storage_secret"}"
|
||||
postgres:
|
||||
address: "tcp://authelia-database:5432"
|
||||
database: "${config.sops.placeholder."authelia/db_name"}"
|
||||
username: "${config.sops.placeholder."authelia/db_username"}"
|
||||
password: "${config.sops.placeholder."authelia/db_password"}"
|
||||
session:
|
||||
secret: "${config.sops.placeholder."authelia/session_secret"}"
|
||||
cookies:
|
||||
- domain: "${config.numbus-server.services.domain}"
|
||||
authelia_url: "https://${cfg.subdomain}.${config.numbus-server.services.domain}"
|
||||
${optionalString (defaultRedirectionUrl != null) "default_redirection_url: \"${defaultRedirectionUrl}\""}
|
||||
authentication_backend:
|
||||
ldap:
|
||||
implementation: "lldap"
|
||||
address: "ldap://host.containers.internal:3890"
|
||||
base_dn: "${baseDN}"
|
||||
user: "UID=authelia,OU=people,${baseDN}"
|
||||
password: "${config.sops.placeholder."lldap/"}"
|
||||
notifier:
|
||||
smtp:
|
||||
address: submission://${config.numbus-server.mail.smtpHost}:${config.numbus-server.mail.smtpPort}
|
||||
username: ${config.numbus-server.mail.smtpUsername}
|
||||
password: ${config.sops.placeholder.smtpPassword}
|
||||
sender: ${config.numbus-server.mail.fromAddress}
|
||||
tls:
|
||||
server_name: ${config.numbus-server.mail.smtpHost}
|
||||
minimum_version: TLS1.2
|
||||
skip_verify: false
|
||||
access_control:
|
||||
default_policy: 'deny'
|
||||
rules:
|
||||
- domain: "*.${config.numbus-server.service.domain}"
|
||||
policy: two_factor
|
||||
subject: "group:admin"
|
||||
${allGroupRules}
|
||||
'';
|
||||
path = "/etc/authelia/authelia.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "crafty";
|
||||
# Version tagging
|
||||
craftyVersion = "v4.10.1";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.crafty;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Crafty controller, one place to manage your minecraft servers";
|
||||
defaultPort = "8443";
|
||||
scheme = "https";
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/log"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${optimizedDir}/import"
|
||||
"100999:100 ${optimizedDir}/backups"
|
||||
"100999:100 ${optimizedDir}/servers"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
crafty:
|
||||
image: registry.gitlab.com/crafty-controller/crafty-4:${craftyVersion}
|
||||
container_name: crafty
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
crafty:
|
||||
ipv4_address: 10.89.250.253
|
||||
ports:
|
||||
- "${cfg.port}:8443/tcp"
|
||||
- "19132:19132/udp"
|
||||
- "25500-25600:25500-25600"
|
||||
volumes:
|
||||
- ${optimizedDir}/backups:/crafty/backups
|
||||
- ${optimizedDir}/servers:/crafty/servers
|
||||
- ${optimizedDir}/import:/crafty/import
|
||||
- ${cfg.configDir}/logs:/crafty/logs
|
||||
- ${cfg.configDir}/config:/crafty/app/config
|
||||
environment:
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
crafty:
|
||||
driver: bridge
|
||||
name: crafty
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.250.0/24"
|
||||
gateway: "10.89.250.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "dashy";
|
||||
# Version tagging
|
||||
dashyVersion = "v3.2.3";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.dashy;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Dashy, the ultimate dashboard for your homelab";
|
||||
defaultPort = "8999";
|
||||
configDirEnabled = false;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
dashy:
|
||||
image: lissy93/dashy:${dashyVersion}
|
||||
container_name: dashy
|
||||
hostname: dashy
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
dashy:
|
||||
ipv4_address: 10.89.235.253
|
||||
ports:
|
||||
- ${cfg.port}:8080
|
||||
volumes:
|
||||
- ${config.sops."dashy/config".path}:/app/user-data/conf.yml
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- NODE_ENV=production
|
||||
healthcheck:
|
||||
test: ['CMD', 'node', '/app/services/healthcheck']
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
dashy:
|
||||
driver: bridge
|
||||
name: dashy
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.235.0/24"
|
||||
gateway: "10.89.235.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."dashy/config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0440" ;
|
||||
content = ''
|
||||
pageInfo:
|
||||
title: My Homelab
|
||||
sections:
|
||||
- name: Example Section
|
||||
icon: far fa-rocket
|
||||
items:
|
||||
- title: GitHub
|
||||
description: Dashy source code and docs
|
||||
icon: fab fa-github
|
||||
url: https://github.com/Lissy93/dashy
|
||||
- title: Issues
|
||||
description: View open issues, or raise a new one
|
||||
icon: fas fa-bug
|
||||
url: https://github.com/Lissy93/dashy/issues
|
||||
- name: Local Services
|
||||
items:
|
||||
- title: Firewall
|
||||
icon: favicon
|
||||
url: http://192.168.1.1/
|
||||
- title: Game Server
|
||||
icon: https://i.ibb.co/710B3Yc/space-invader-x256.png
|
||||
url: http://192.168.130.1/
|
||||
'';
|
||||
path = "/etc/dashy/dashy.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# Good
|
||||
./gitea.nix
|
||||
./immich.nix
|
||||
./nextcloud.nix
|
||||
./passbolt.nix
|
||||
./traefik.nix
|
||||
# Testing needed
|
||||
./authelia.nix
|
||||
./crafty.nix
|
||||
./dashy.nix
|
||||
./frigate.nix
|
||||
./home-assistant.nix
|
||||
./homepage.nix
|
||||
./it-tools.nix
|
||||
./jellyfin.nix
|
||||
./lldap.nix
|
||||
./n8n.nix
|
||||
./netbird.nix
|
||||
./netbootxyz.nix
|
||||
./ntfy.nix
|
||||
./odoo.nix
|
||||
./uptime-kuma.nix
|
||||
./vscodium.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "frigate";
|
||||
# Version tagging
|
||||
frigateVersion = "0.16.4";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.frigate;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "home-assistant";
|
||||
description = "Frigate, your fully-local NVR (Network Video Recorder)";
|
||||
defaultPort = "8971";
|
||||
scheme = "https";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"authelia.service"
|
||||
"home-assistant.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"1000:100 ${cfg.configDir}"
|
||||
"1000:100 ${cfg.dataDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
frigate:
|
||||
image: ghcr.io/blakeblackshear/frigate:${frigateVersion}
|
||||
container_name: frigate
|
||||
hostname: frigate
|
||||
shm_size: "256mb"
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.253
|
||||
ports:
|
||||
- "${cfg.port}:8971/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/config
|
||||
- ${cfg.dataDir}:/media/frigate
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- type: tmpfs
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
environment:
|
||||
- FRIGATE_MQTT_USER=${config.sops.placeholder."home-assistant/mqtt_username"}
|
||||
- FRIGATE_MQTT_PASSWORD=${config.sops.placeholder."home-assistant/mqtt_password"}
|
||||
${lib.optionalString (cfg.devices != []) ''
|
||||
devices:
|
||||
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
|
||||
''}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
stop_grace_period: 30s
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
home-assistant:
|
||||
external: true
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "/dev/dri:/dev/dri" "/dev/bus/usb:/dev/bus/usb" "/dev/apex_0:/dev/apex_0" ];
|
||||
description = "List of devices to map into the container. /dev/dri is used for graphics acceleration, /dev/bus/usb for USB Coral TPUs, and /dev/apex_0 for PCI coral TPUs";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "gitea";
|
||||
# Version tagging
|
||||
giteaVersion = "1.25.4-rootless";
|
||||
databaseVersion = "18-alpine";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.gitea;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "Gitea, your own self-hosted git platform";
|
||||
defaultPort = "3000";
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}/data"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
secrets = [
|
||||
"gitea/db_name"
|
||||
"gitea/db_username"
|
||||
"gitea/db_password"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
gitea-database:
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
container_name: gitea-database
|
||||
hostname: gitea-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
gitea:
|
||||
ipv4_address: 10.89.240.253
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=${config.sops.placeholder."gitea/db_name"}
|
||||
- POSTGRES_USER=${config.sops.placeholder."gitea/db_username"}
|
||||
- POSTGRES_PASSWORD=${config.sops.placeholder."gitea/db_password"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
gitea-server:
|
||||
image: docker.gitea.com/gitea:${giteaVersion}
|
||||
container_name: gitea-server
|
||||
hostname: gitea-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
gitea:
|
||||
ipv4_address: 10.89.240.252
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}/data:/var/lib/gitea
|
||||
- ${cfg.configDir}/config:/etc/gitea
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=gitea-database:5432
|
||||
- GITEA__database__NAME=${config.sops.placeholder."gitea/db_name"}
|
||||
- GITEA__database__USER=${config.sops.placeholder."gitea/db_username"}
|
||||
- GITEA__database__PASSWD=${config.sops.placeholder."gitea/db_password"}
|
||||
- GITEA__server__SSH_PORT=2424
|
||||
- GITEA__server__ROOT_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
depends_on:
|
||||
- gitea-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
gitea:
|
||||
driver: bridge
|
||||
name: gitea
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.240.0/24"
|
||||
gateway: "10.89.240.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "home-assistant";
|
||||
# Version tagging
|
||||
homeAssistantVersion = "2026.2.3";
|
||||
mqttVersion = "2.1-alpine";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.home-assistant;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Home Assistant, libre house control and much more";
|
||||
defaultPort = "8123";
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"1000:100 ${cfg.configDir}"
|
||||
"1000:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/mqtt"
|
||||
];
|
||||
secrets = [
|
||||
"home-assistant/mqtt_user"
|
||||
"home-assistant/mqtt_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
home-assistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:${homeAssistantVersion}
|
||||
container_name: home-assistant
|
||||
hostname: home-assistant
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.252
|
||||
ports:
|
||||
- "${cfg.port}:8123/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /run/dbus:/run/dbus:ro
|
||||
${lib.optionalString (cfg.devices != []) ''
|
||||
devices:
|
||||
${lib.concatStringsSep "\n" (map (d: " - \"${d}\"") cfg.devices)}
|
||||
''}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
home-assistant-mqtt:
|
||||
image: docker.io/library/eclipse-mosquitto:${mqttVersion}
|
||||
container_name: home-assistant-mqtt
|
||||
hostname: home-assistant-mqtt
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
home-assistant:
|
||||
ipv4_address: 10.89.230.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/mqtt:/mosquitto
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
home-assistant:
|
||||
driver: bridge
|
||||
name: home-assistant
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.230.0/24"
|
||||
gateway: "10.89.230.254"
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
devices = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "/dev/serial/by-id/Sonoff_Zigbee_3.0-id-port0:/dev/ttyUSB0" ];
|
||||
description = "List of devices to map into the container. /dev/ttyUSB0 is used for Zigbee dongles";
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = {
|
||||
systemd.services."${name}-quirk" = {
|
||||
description = "Podman container quirk : ${name}";
|
||||
after = [ "${name}.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.systemd ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
mkdir -p /var/lib/numbus-server/${name}
|
||||
if [[ -e ${cfg.configDir}/config/configuration.yaml ]]; then
|
||||
if grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
|
||||
exit 0
|
||||
elif grep -qF "use_x_forwarded_for" ${cfg.configDir}/config/configuration.yaml && ! grep -qF "10.89.230.1/32" ${cfg.configDir}/config/configuration.yaml; then
|
||||
tmp=$(mktemp)
|
||||
head -n -6 ${cfg.configDir}/config/configuration.yaml > "$tmp"
|
||||
mv "$tmp" ${cfg.configDir}/config/configuration.yaml
|
||||
fi
|
||||
fi
|
||||
|
||||
until [[ -e ${cfg.configDir}/config/configuration.yaml ]]; do
|
||||
sleep 15
|
||||
done
|
||||
cat << 'EOF' >> ${cfg.configDir}/config/configuration.yaml
|
||||
|
||||
http:
|
||||
use_x_forwarded_for: true
|
||||
trusted_proxies: 10.89.230.1
|
||||
|
||||
zha:
|
||||
EOF
|
||||
|
||||
systemctl restart ${name}.service
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."mqtt-quirk" = {
|
||||
description = "Podman container quirk : Home-assistant MQTT";
|
||||
after = [ "sops-install-secrets.service" ];
|
||||
before = [ "${name}.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.mosquitto ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
if [[ -e ${cfg.configDir}/mqtt/mosquitto.conf && ${cfg.configDir}/mqtt/password.txt ]]; then
|
||||
if grep -qF "listener 1883" ${cfg.configDir}/mqtt/mosquitto.conf; then
|
||||
exit 0
|
||||
else
|
||||
rm ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
rm ${cfg.configDir}/mqtt/password.txt
|
||||
touch ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
touch ${cfg.configDir}/mqtt/password.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
cat << EOF >> ${cfg.configDir}/mqtt/mosquitto.conf
|
||||
persistence true
|
||||
persistence_location /mosquitto/data/
|
||||
log_dest file /mosquitto/log/mosquitto.log
|
||||
listener 1883
|
||||
## Authentication ##
|
||||
allow_anonymous false
|
||||
password_file /mosquitto/password.txt
|
||||
EOF
|
||||
|
||||
HOME_ASSISTANT_MQTT_USER=$(cat /run/secrets/home-assistant/mqtt_user)
|
||||
HOME_ASSISTANT_MQTT_PASSWORD=$(cat /run/secrets/home-assistant/mqtt_password)
|
||||
|
||||
mosquitto_passwd -b ${cfg.configDir}/mqtt/password.txt "$HOME_ASSISTANT_MQTT_USER" "$HOME_ASSISTANT_MQTT_PASSWORD"
|
||||
chmod 0400 ${cfg.configDir}/mqtt/password.txt
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "homepage";
|
||||
# Version tagging
|
||||
homepageVersion = "v1.10.1";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.homepage;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Homepage, a modern and highly customizable application dashboard";
|
||||
defaultPort = "3003";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/images"
|
||||
"100999:100 ${cfg.configDir}/icons"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
homepage:
|
||||
image: ghcr.io/gethomepage/homepage:${homepageVersion}
|
||||
container_name: homepage
|
||||
hostname: homepage
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
homepage:
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/app/config
|
||||
- ${cfg.configDir}/images:/app/public/images
|
||||
- ${cfg.configDir}/icons:/app/public/icons
|
||||
environment:
|
||||
PUID: 1000
|
||||
PGID: 1000
|
||||
HOMEPAGE_ALLOWED_HOSTS: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
homepage:
|
||||
driver: bridge
|
||||
name: homepage
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.220.0/24"
|
||||
gateway: "10.89.220.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container configuration
|
||||
name = "immich";
|
||||
# Version tagging
|
||||
immichVersion = "v2.5.6";
|
||||
redisVersion = "9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63";
|
||||
databaseVersion = "14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.immich;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Immich, Google Photos but better";
|
||||
defaultPort = "2283";
|
||||
middlewares = [
|
||||
"immichSecureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/redis"
|
||||
"100999:100 ${cfg.configDir}/model-cache"
|
||||
"100999:100 ${cfg.configDir}/machine-learning-cache"
|
||||
"100999:100 ${cfg.configDir}/machine-learning-config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
"100999:100 ${cfg.dataDir}"
|
||||
];
|
||||
secrets = [
|
||||
"immich/redis_hostname"
|
||||
"immich/db_hostname"
|
||||
"immich/db_name"
|
||||
"immich/db_username"
|
||||
"immich/db_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
immich-server:
|
||||
container_name: immich-server
|
||||
hostname: immich-server
|
||||
image: ghcr.io/immich-app/immich-server:${immichVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.253
|
||||
ports:
|
||||
- "${cfg.port}:2283/tcp"
|
||||
volumes:
|
||||
- $UPLOAD_LOCATION:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
environment:
|
||||
TZ: $TZ
|
||||
REDIS_HOSTNAME: ${config.sops.placeholder."immich/redis_hostname"}
|
||||
DB_HOSTNAME: ${config.sops.placeholder."immich/db_hostname"}
|
||||
DB_DATABASE_NAME: ${config.sops.placeholder."immich/db_name"}
|
||||
DB_USERNAME: ${config.sops.placeholder."immich/db_username"}
|
||||
DB_PASSWORD: ${config.sops.placeholder."immich/db_password"}
|
||||
IMMICH_TRUSTED_PROXIES: 10.89.210.1
|
||||
depends_on:
|
||||
- immich-redis
|
||||
- immich-database
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-machine-learning:
|
||||
container_name: immich-machine-learning
|
||||
hostname: immich-machine-learning
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${immichVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/model-cache:/cache
|
||||
- ${cfg.configDir}/machine-learning-config:/usr/src/.config
|
||||
- ${cfg.configDir}/machine-learning-cache:/usr/src/.cache/
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-redis:
|
||||
container_name: immich-redis
|
||||
hostname: immich-redis
|
||||
image: docker.io/valkey/valkey:${redisVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.251
|
||||
volumes:
|
||||
- ${cfg.configDir}/redis:/data
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
immich-database:
|
||||
container_name: immich-database
|
||||
hostname: immich-database
|
||||
image: ghcr.io/immich-app/postgres:${databaseVersion}
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
immich:
|
||||
ipv4_address: 10.89.210.250
|
||||
environment:
|
||||
POSTGRES_DB: ${config.sops.placeholder."immich/db_name"}
|
||||
POSTGRES_USER: ${config.sops.placeholder."immich/db_username"}
|
||||
POSTGRES_PASSWORD: ${config.sops.placeholder."immich/db_password"}
|
||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||
volumes:
|
||||
- $DB_DATA_LOCATION:/var/lib/postgresql/data
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
disable: false
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
immich:
|
||||
driver: bridge
|
||||
name: immich
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.210.0/24"
|
||||
gateway: "10.89.210.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."immich/env" = {
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
DB_DATA_LOCATION=${cfg.configDir}/database
|
||||
UPLOAD_LOCATION=${cfg.dataDir}
|
||||
'';
|
||||
path = "/etc/podman/immich/.env";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/immich-secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
immichSecureHeaders:
|
||||
headers:
|
||||
FrameDeny: true
|
||||
AccessControlAllowMethods: 'GET,POST,PUT,DELETE,OPTIONS'
|
||||
AccessControlAllowOriginList:
|
||||
- https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
ContentSecurityPolicy: "default-src 'self'; base-uri 'self'; img-src 'self' https://static.immich.cloud https://tiles.immich.cloud data: blob:; connect-src 'self' https://${cfg.subdomain}.${config.numbus-server.services.domain} wss://${cfg.subdomain}.${config.numbus-server.services.domain} https://static.immich.cloud https://tiles.immich.cloud; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: https://${cfg.subdomain}.${config.numbus-server.services.domain}; frame-ancestors 'self';"
|
||||
CustomFrameOptionsValue: SAMEORIGIN
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: vibrate 'self'
|
||||
STSSeconds: 315360000
|
||||
'';
|
||||
path = "/etc/traefik/rules/immich-secureHeaders.yaml";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "it-tools";
|
||||
# Version tagging
|
||||
it-toolsVersion = "2024.10.22-7ca5933";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.it-tools;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "IT-tools, useful tools when doing IT";
|
||||
pod = "false";
|
||||
defaultPort = "8880";
|
||||
configDirEnabled = false;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
it-tools:
|
||||
image: docker.io/corentinth/it-tools:${it-toolsVersion}
|
||||
container_name: it-tools
|
||||
hostname: it-tools
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
it-tools:
|
||||
ipv4_address: 10.89.200.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
it-tools:
|
||||
driver: bridge
|
||||
name: it-tools
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.200.0/24"
|
||||
gateway: "10.89.200.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "jellyfin";
|
||||
# Version tagging
|
||||
jellyfinVersion = "10.11.6";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.jellyfin;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Jellyfin : A self-hosted media server to stream your movies and music";
|
||||
defaultPort = "8096";
|
||||
scheme = "https"; #TODO CHECK
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.dataDir}"
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.dataDir}/media"
|
||||
"100999:100 ${cfg.dataDir}/fonts"
|
||||
"100999:100 ${cfg.configDir}/cache"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
jellyfin:
|
||||
image: docker.io/jellyfin/jellyfin:${jellyfinVersion}
|
||||
container_name: jellyfin
|
||||
hostname: jellyfin
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
jellyfin:
|
||||
ipv4_address: 10.89.190.253
|
||||
ports:
|
||||
- "${cfg.port}:8096/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${cfg.configDir}/cache:/cache
|
||||
- type: bind
|
||||
source: ${cfg.dataDir}/media
|
||||
target: /media
|
||||
- type: bind
|
||||
source: ${cfg.dataDir}/fonts
|
||||
target: /usr/local/share/fonts/custom
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
jellyfin:
|
||||
driver: bridge
|
||||
name: jellyfin
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.190.0/24"
|
||||
gateway: "10.89.190.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "lldap";
|
||||
# Version tagging
|
||||
lldapVersion = "v0.6.2";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.lldap;
|
||||
# Derive Base DN from domain (e.g., example.com -> dc=example,dc=com)
|
||||
domainParts = splitString "." config.numbus-server.services.domain;
|
||||
baseDN = concatStringsSep "," (map (p: "dc=${p}") domainParts);
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "LLDAP, unified user management";
|
||||
defaultPort = "17170";
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"network-online.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"lldap/jwt_secret"
|
||||
"lldap/key_seed"
|
||||
"lldap/admin_password"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
lldap:
|
||||
image: lldap/lldap:${lldapVersion}
|
||||
container_name: lldap
|
||||
hostname: lldap
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
lldap:
|
||||
ipv4_address: 10.89.185.253
|
||||
ports:
|
||||
- "3890:3890"
|
||||
- "${cfg.port}:17170"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/data
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- TZ=${config.time.timeZone}
|
||||
- LLDAP_LDAP_BASE_DN=${baseDN}
|
||||
- LLDAP_JWT_SECRET="${config.sops.placeholder."lldap/jwt_secret"}"
|
||||
- LLDAP_KEY_SEED="${config.sops.placeholder."lldap/key_seed"}"
|
||||
- LLDAP_LDAP_USER_PASS="${config.sops.placeholder."lldap/admin_password"}"
|
||||
- LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
|
||||
- LLDAP_SMTP_OPTIONS__SERVER=${config.numbus-server.mail.smtpServer}
|
||||
- LLDAP_SMTP_OPTIONS__PORT=${config.numbus-server.mail.smtpPort}
|
||||
- LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=${config.numbus-server.mail.smtpEncryption}
|
||||
- LLDAP_SMTP_OPTIONS__USER=${config.numbus-server.mail.smtpUsername}
|
||||
- LLDAP_SMTP_OPTIONS__PASSWORD=${config.sops.placeholder."mail/smtpPassword"}
|
||||
- LLDAP_SMTP_OPTIONS__FROM=no-reply <${config.numbus-server.mail.fromAddress}>
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
lldap:
|
||||
driver: bridge
|
||||
name: lldap
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.185.0/24"
|
||||
gateway: "10.89.185.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "n8n";
|
||||
# Version tagging
|
||||
n8nVersion = "2.11.4";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.n8n;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "n8n, the ultimate automation platform";
|
||||
defaultPort = "5678";
|
||||
scheme = "https";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:${n8nVersion}
|
||||
container_name: n8n
|
||||
hostname: n8n
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
n8n:
|
||||
ipv4_address: 10.89.180.253
|
||||
ports:
|
||||
- "${cfg.port}:5678"
|
||||
volumes:
|
||||
- ${optimizedDir}:/home/node/.n8n
|
||||
environment:
|
||||
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
|
||||
- N8N_HOST=${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- N8N_PORT=5678
|
||||
- N8N_PROTOCOL=https
|
||||
- N8N_RUNNERS_ENABLED=true
|
||||
- NODE_ENV=production
|
||||
- WEBHOOK_URL=https://${cfg.subdomain}.${config.numbus-server.services.domain}/
|
||||
- GENERIC_TIMEZONE=${time.timeZone}
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
n8n:
|
||||
driver: bridge
|
||||
name: n8n
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.180.0/24"
|
||||
gateway: "10.89.180.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "netbird";
|
||||
# Version tagging
|
||||
netbirdDashboardVersion = "";
|
||||
netbirdServerVersion = "";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.netbird;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
pod = "false";
|
||||
description = "NetBird, an all-in-one ZTNA remote access platform";
|
||||
defaultPort = "8888";
|
||||
reverseProxied = false;
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"traefik.service"
|
||||
"${config.numbus-server.services.dns}.service"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"netbird/auth_key"
|
||||
"netbird/encryption_key"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
netbird-dashboard:
|
||||
image: netbirdio/dashboard:${netbirdDashboardVersion}
|
||||
container_name: netbird-dashboard
|
||||
hostname: netbird-dashboard
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbird:
|
||||
ipv4_address: 10.89.175.253
|
||||
ports:
|
||||
- "${defaultPort}:8080/tcp"
|
||||
environment:
|
||||
# Endpoints
|
||||
- NETBIRD_MGMT_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
# OIDC - using embedded IdP
|
||||
- AUTH_AUDIENCE=netbird-dashboard
|
||||
- AUTH_CLIENT_ID=netbird-dashboard
|
||||
- AUTH_CLIENT_SECRET=
|
||||
- AUTH_AUTHORITY=https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2
|
||||
- USE_AUTH0=false
|
||||
- AUTH_SUPPORTED_SCOPES=openid profile email groups
|
||||
- AUTH_REDIRECT_URI=/nb-auth
|
||||
- AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||
# SSL
|
||||
- NGINX_SSL_PORT=443
|
||||
- LETSENCRYPT_DOMAIN=none
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
netbird-server:
|
||||
image: netbirdio/netbird-server:${netbirdServerVersion}
|
||||
container_name: netbird-server
|
||||
hostname: netbird-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbird:
|
||||
ipv4_address: 10.89.175.252
|
||||
ports:
|
||||
- "8889:8081/tcp"
|
||||
- "3478:3478/udp"
|
||||
volumes:
|
||||
- ${config.sops.templates."netbird-config".path}:/etc/netbird/config.yaml
|
||||
- ${cfg.configDir}:/var/lib/netbird
|
||||
command: ["--config", "/etc/netbird/config.yaml"]
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "500m"
|
||||
max-file: "2"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
netbird:
|
||||
driver: bridge
|
||||
name: netbird
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.175.0/24"
|
||||
gateway: "10.89.175.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."netbird-config" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
server:
|
||||
listenAddress: ":80"
|
||||
exposedAddress: "https://${cfg.subdomain}.${config.numbus-server.services.domain}:443"
|
||||
stunPorts:
|
||||
- 3478
|
||||
metricsPort: 9090
|
||||
healthcheckAddress: ":9000"
|
||||
logLevel: "info"
|
||||
logFile: "console"
|
||||
authSecret: "${config.sops.placeholder."netbird/auth_key"}"
|
||||
dataDir: "/var/lib/netbird"
|
||||
|
||||
auth:
|
||||
issuer: "https://${cfg.subdomain}.${config.numbus-server.services.domain}/oauth2"
|
||||
signKeyRefreshEnabled: true
|
||||
dashboardRedirectURIs:
|
||||
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-auth"
|
||||
- "https://${cfg.subdomain}.${config.numbus-server.services.domain}/nb-silent-auth"
|
||||
cliRedirectURIs:
|
||||
- "http://localhost:53000/"
|
||||
|
||||
reverseProxy:
|
||||
trustedHTTPProxies:
|
||||
- "10.89.175.1/32"
|
||||
|
||||
store:
|
||||
engine: "sqlite"
|
||||
encryptionKey: "${config.sops.placeholder."netbird/encryption_key"}"
|
||||
'';
|
||||
path = "/etc/netbird/netbird.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/${name}" = {
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
${name}-dashboard:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
priority: 1
|
||||
${name}-grpc:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`))"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: ${name}-server-h2c
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
${name}-backend:
|
||||
rule: "Host(`${cfg.subdomain}.${config.numbus-server.services.domain}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: ${name}-server
|
||||
middlewares:
|
||||
- secureHeaders
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
|
||||
services:${cfg.port}
|
||||
${name}-dashboard:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:${cfg.port}"
|
||||
${name}-server:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:8889"
|
||||
${name}-server-h2c:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "h2c://host.containers.internal:3478"
|
||||
'';
|
||||
path = "/etc/traefik/rules/${name}";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "netbootxyz";
|
||||
# Version tagging
|
||||
netbootxyzVersion = "3.0.0";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.netbootxyz;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Netboot.xyz, forget about flashing isos on USB sticks with PXE boot";
|
||||
pod = "false";
|
||||
defaultPort = "3004";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${optimizedDir}/assets"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
netbootxyz:
|
||||
image: ghcr.io/netbootxyz/netbootxyz:${netbootxyzVersion}
|
||||
container_name: netbootxyz
|
||||
hostname: netbootxyz
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
netbootxyz:
|
||||
ipv4_address: 10.89.170.253
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
- "69:69/udp"
|
||||
- "8008:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${optimizedDir}/assets:/assets
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${time.timeZone}
|
||||
- PORT_RANGE=30000:30010
|
||||
- SUBFOLDER=/
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
netbootxyz:
|
||||
driver: bridge
|
||||
name: netbootxyz
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.170.0/24"
|
||||
gateway: "10.89.170.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
nextcloudVersion = "33.0.0";
|
||||
redisVersion = "8.6-alpine";
|
||||
databaseVersion = "11.8";
|
||||
onlyofficeVersion = "9.2";
|
||||
whiteboardVersion = "v1.5.6";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.nextcloud;
|
||||
# Container config
|
||||
name = "nextcloud";
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Nextcloud, your own online office suite";
|
||||
defaultPort = "1100";
|
||||
middlewares = [
|
||||
"nextcloudSecureHeaders"
|
||||
];
|
||||
secrets = [
|
||||
"nextcloud/db_name"
|
||||
"nextcloud/db_username"
|
||||
"nextcloud/db_password"
|
||||
"nextcloud/redis_password"
|
||||
"nextcloud/onlyoffice_secret"
|
||||
"nextcloud/whiteboard_secret"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100032:100 ${cfg.dataDir}"
|
||||
"100032:100 ${cfg.configDir}"
|
||||
"100032:100 ${cfg.configDir}/web"
|
||||
"100999:100 ${cfg.configDir}/redis"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/log"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/cache"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/data"
|
||||
"1000:100 ${cfg.configDir}/onlyoffice/database"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
nextcloud-server:
|
||||
image: docker.io/library/nextcloud:${nextcloudVersion}
|
||||
container_name: nextcloud-server
|
||||
hostname: nextcloud-server
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/web:/var/www/html
|
||||
- ${cfg.dataDir}:/mnt/ncdata
|
||||
environment:
|
||||
MYSQL_HOST: nextcloud-database:3306
|
||||
MYSQL_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
|
||||
MYSQL_USER: ${config.sops.placeholder."nextcloud/db_username"}
|
||||
MYSQL_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
|
||||
REDIS_HOST_PASSWORD: ${config.sops.placeholder."nextcloud/redis_password"}
|
||||
REDIS_HOST: nextcloud-redis
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
NEXTCLOUD_DATA_DIR: /mnt/ncdata
|
||||
SMTP_SECURE: tls
|
||||
SMTP_HOST: ${config.numbus-server.mail.smtpServer}
|
||||
SMTP_PORT: ${toString config.numbus-server.mail.smtpPort}
|
||||
SMTP_NAME: ${config.numbus-server.mail.smtpUsername}
|
||||
SMTP_PASSWORD: ${config.sops.placeholder.smtpPassword}
|
||||
MAIL_FROM_ADDRESS: no-reply
|
||||
MAIL_DOMAIN: ${config.numbus-server.services.domain}
|
||||
APACHE_DISABLE_REWRITE_IP: 1
|
||||
OVERWRITEPROTOCOL: https
|
||||
TRUSTED_PROXIES: 10.89.160.1
|
||||
NC_default_phone_region: "${config.numbus-server.language}"
|
||||
NC_default_language: "${config.numbus-server.language}"
|
||||
NC_default_locale: "${config.numbus-server.locale}"
|
||||
NC_default_timezone: "${config.time.timeZone}"
|
||||
NC_maintenance_window_start: "1"
|
||||
depends_on:
|
||||
- nextcloud-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-redis:
|
||||
image: docker.io/library/redis:${redisVersion}
|
||||
container_name: nextcloud-redis
|
||||
hostname: nextcloud-redis
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.252
|
||||
volumes:
|
||||
- ${cfg.configDir}/redis:/data
|
||||
command: redis-server --requirepass ${config.sops.placeholder."nextcloud/redis_password"} --save 60 1 --loglevel warning
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-database:
|
||||
image: docker.io/library/mariadb:${databaseVersion}
|
||||
container_name: nextcloud-database
|
||||
hostname: nextcloud-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.251
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/mysql
|
||||
environment:
|
||||
MARIADB_DATABASE: ${config.sops.placeholder."nextcloud/db_name"}
|
||||
MARIADB_USER: ${config.sops.placeholder."nextcloud/db_username"}
|
||||
MARIADB_PASSWORD: ${config.sops.placeholder."nextcloud/db_password"}
|
||||
MARIADB_RANDOM_ROOT_PASSWORD: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
command:
|
||||
- "--transaction-isolation=READ-COMMITTED"
|
||||
- "--binlog-format=ROW"
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-onlyoffice:
|
||||
image: docker.io/onlyoffice/documentserver:${onlyofficeVersion}
|
||||
container_name: nextcloud-onlyoffice
|
||||
hostname: nextcloud-onlyoffice
|
||||
networks:
|
||||
nextcloud:
|
||||
ipv4_address: 10.89.160.250
|
||||
ports:
|
||||
- "9980:80/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/onlyoffice/log:/var/log/onlyoffice
|
||||
- ${cfg.configDir}/onlyoffice/cache:/var/lib/onlyoffice
|
||||
- ${cfg.configDir}/onlyoffice/data:/var/www/onlyoffice/Data
|
||||
- ${cfg.configDir}/onlyoffice/database:/var/lib/postgresql
|
||||
environment:
|
||||
- JWT_SECRET=${config.sops.placeholder."nextcloud/onlyoffice_secret"}
|
||||
- REDIS_SERVER_PASS=${config.sops.placeholder."nextcloud/redis_password"}
|
||||
- REDIS_SERVER_HOST=nextcloud-redis
|
||||
- REDIS_SERVER_PORT=6379
|
||||
- ADMINPANEL_ENABLED=false
|
||||
- EXAMPLE_ENABLED=false
|
||||
- METRICS_ENABLED=false
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
nextcloud-whiteboard:
|
||||
image: ghcr.io/nextcloud-releases/whiteboard:${whiteboardVersion}
|
||||
container_name: nextcloud-whiteboard
|
||||
hostname: nextcloud-whiteboard
|
||||
user: '1000:1000'
|
||||
ports:
|
||||
- "3002:3002/tcp"
|
||||
environment:
|
||||
NEXTCLOUD_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
JWT_SECRET_KEY: ${config.sops.placeholder."nextcloud/whiteboard_secret"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
nextcloud:
|
||||
driver: bridge
|
||||
name: nextcloud
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.160.0/24"
|
||||
gateway: "10.89.160.254"
|
||||
'';
|
||||
|
||||
extraOptions = {
|
||||
onlyoffice = {
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = "onlyoffice";
|
||||
example = "onlyoffice";
|
||||
description = "The subdomain that onlyoffice for nextcloud will use";
|
||||
};
|
||||
};
|
||||
whiteboard = {
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = "whiteboard";
|
||||
example = "whiteboard";
|
||||
description = "The subdomain that whiteboard for nextcloud will use";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = {
|
||||
sops.templates."traefik/rules/nextcloud-onlyoffice" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
nextcloud-onlyoffice:
|
||||
rule: "Host(`${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: nextcloud-onlyoffice
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
services:
|
||||
nextcloud-onlyoffice:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:9980"
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-onlyoffice.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/nextcloud-whiteboard" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
routers:
|
||||
nextcloud-whiteboard:
|
||||
rule: "Host(`${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}`)"
|
||||
entrypoints:
|
||||
- "websecure"
|
||||
service: nextcloud-whiteboard
|
||||
middlewares:
|
||||
- "secureHeaders"
|
||||
tls:
|
||||
certresolver: "cloudflare"
|
||||
options: "secureTLS"
|
||||
services:
|
||||
nextcloud-whiteboard:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://host.containers.internal:3002"
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-whiteboard.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/nextcloud-secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
nextcloudSecureHeaders:
|
||||
headers:
|
||||
FrameDeny: false
|
||||
CustomFrameOptionsValue: "SAMEORIGIN"
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSSeconds: 315360000
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
AccessControlAllowMethods: "GET,OPTIONS,PUT"
|
||||
AccessControlAllowOriginList:
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: "vibrate=()"
|
||||
ContentSecurityPolicy: >-
|
||||
default-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
script-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
connect-src 'self';
|
||||
img-src 'self' data:;
|
||||
font-src 'self' data:;
|
||||
frame-src https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
frame-ancestors https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain} 'self';
|
||||
object-src 'none';
|
||||
base-uri 'self';
|
||||
'';
|
||||
path = "/etc/traefik/rules/nextcloud-secureHeaders";
|
||||
};
|
||||
|
||||
systemd.services."${name}-quirk" = {
|
||||
description = "Podman container quirk : ${name}";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "${name}.service" "${name}-secrets.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
startLimitBurst = 5;
|
||||
startLimitIntervalSec = 600;
|
||||
path = [ pkgs.coreutils pkgs.sudo pkgs.podman pkgs.systemd pkgs.gnugrep ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
OCC="sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ"
|
||||
|
||||
[[ ! -e /var/lib/numbus-server/${name}/.env ]] && systemctl start ${name}-secrets.service
|
||||
until [[ -e /var/lib/numbus-server/${name}/.env ]]; do
|
||||
echo "Waiting for secrets generation..."
|
||||
sleep 5
|
||||
done
|
||||
source /var/lib/numbus-server/${name}/.env
|
||||
|
||||
until $OCC status | grep -iq "installed: true" >/dev/null 2>&1; do
|
||||
echo "Waiting for Nextcloud to be up and running..."
|
||||
sleep 60
|
||||
done
|
||||
|
||||
$OCC db:add-missing-indices
|
||||
$OCC maintenance:repair --include-expensive
|
||||
|
||||
INSTALL_APPS_LIST=( "calendar" "contacts" "mail" "notes" "onlyoffice" "cookbook" "whiteboard" )
|
||||
DISABLE_APPS_LIST=( "activity" "federation" "webhook_listeners" "photos" "recommendations" "sharebymail" "teams" "support" "richdocumentscode" )
|
||||
|
||||
for app in ''${INSTALL_APPS_LIST[@]}; do
|
||||
if ! $OCC --no-warnings app:list | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:install "$app"
|
||||
fi
|
||||
if $OCC --no-warnings app:list --disabled | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:enable "$app"
|
||||
fi
|
||||
done
|
||||
for app in ''${DISABLE_APPS_LIST[@]}; do
|
||||
if $OCC --no-warnings app:list --enabled | grep -iq "$app:"; then
|
||||
$OCC --no-warnings app:disable "$app"
|
||||
fi
|
||||
done
|
||||
$OCC --no-warnings config:system:set onlyoffice DocumentServerInternalUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
|
||||
$OCC --no-warnings config:system:set onlyoffice DocumentServerUrl --value="https://${cfg.onlyoffice.subdomain}.${config.numbus-server.services.domain}/"
|
||||
$OCC --no-warnings config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_PASSWORD"
|
||||
$OCC --no-warnings config:app:set whiteboard collabBackendUrl --value="https://${cfg.whiteboard.subdomain}.${config.numbus-server.services.domain}"
|
||||
$OCC --no-warnings config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_PASSWORD"
|
||||
|
||||
if [[ ! -f /var/lib/numbus-server/${name}/croned.true ]]; then
|
||||
$OCC background:cron
|
||||
sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php
|
||||
touch /var/lib/numbus-server/${name}/croned.true
|
||||
fi
|
||||
|
||||
if [[ ! -f /var/lib/numbus-server/${name}/scanned.true ]]; then
|
||||
$OCC files:scan --all
|
||||
$OCC files:repair-tree
|
||||
touch /var/lib/numbus-server/${name}/scanned.true
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services."${name}-cron" = {
|
||||
description = "Podman container crontab : ${name}";
|
||||
after = [ "${name}.service" "${name}-quirk.service" ];
|
||||
onFailure = [ "service-failure-notify@%n.service" ];
|
||||
path = [ pkgs.sudo pkgs.podman ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecCondition = ''${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php occ status'';
|
||||
ExecStart = "${pkgs.sudo}/bin/sudo -u numbus-admin podman exec --user www-data nextcloud-server php -f /var/www/html/cron.php";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers."${name}-cron" = {
|
||||
description = "Timer for Nextcloud cron";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "5m";
|
||||
OnUnitActiveSec = "5m";
|
||||
Unit = "${name}-cron.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "ntfy";
|
||||
# Version tagging
|
||||
ntfyVersion = "v2.18.0";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.ntfy;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Ntfy, get notified easily";
|
||||
defaultPort = "8099";
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/cache"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
ntfy:
|
||||
image: docker.io/binwiederhier/ntfy
|
||||
container_name: ntfy
|
||||
hostname: ntfy
|
||||
user: "1000:1000"
|
||||
networks:
|
||||
ntfy:
|
||||
ipv4_address: 10.89.150.253
|
||||
ports:
|
||||
- "${cfg.port}:80/tcp"
|
||||
command:
|
||||
- serve
|
||||
volumes:
|
||||
- ${cfg.config}/cache:/var/cache/ntfy
|
||||
- ${cfg.config}/config:/etc/ntfy
|
||||
environment:
|
||||
- TZ=${time.timeZone}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
ntfy:
|
||||
driver: bridge
|
||||
name: ntfy
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.150.0/24"
|
||||
gateway: "10.89.150.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "odoo";
|
||||
# Version tagging
|
||||
odooVersion = "10.11.6";
|
||||
databaseVersion = "15.17";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.odoo;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Odoo : An open ERP (Enterprise resource planning) solution";
|
||||
defaultPort = "8069";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${optimizedDir}/odoo"
|
||||
"100999:100 ${cfg.configDir}/addons"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
odoo-database:
|
||||
image: docker.io/library/postgres:${databaseVersion}
|
||||
container_name: odoo-database
|
||||
hostname: odoo-database
|
||||
user: '1000:1000'
|
||||
shm_size: 128mb
|
||||
networks:
|
||||
odoo:
|
||||
ipv4_address: 10.89.190.253
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=${config.sops.placeholder."odoo/db_name"}
|
||||
- POSTGRES_PASSWORD=${config.sops.placeholder."odoo/db_password"}
|
||||
- POSTGRES_USER=${config.sops.placeholder."odoo/db_username"}
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
odoo-server:
|
||||
image: docker.io/library/odoo:${odooVersion}
|
||||
container_name: odoo-server
|
||||
hostname: odoo-server
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
odoo:
|
||||
ipv4_address: 10.89.190.252
|
||||
ports:
|
||||
- "${cfg.port}:8069/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}/odoo:/var/lib/odoo
|
||||
- ${cfg.configDir}/config:/etc/odoo
|
||||
- ${cfg.configDir}/addons:/mnt/extra-addons
|
||||
environment:
|
||||
- HOST=odoo-database
|
||||
- USER=${config.sops.placeholder."odoo/db_username"}
|
||||
- PASSWORD=${config.sops.placeholder."odoo/db_password"}
|
||||
depends_on:
|
||||
- odoo-database
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
odoo:
|
||||
driver: bridge
|
||||
name: odoo
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.190.0/24"
|
||||
gateway: "10.89.190.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."odoo/db_name" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
sops.secrets."odoo/db_username" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
sops.secrets."odoo/db_password" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/odoo.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "passbolt";
|
||||
# Version tagging
|
||||
passboltVersion = "5.9.0-1-ce-non-root";
|
||||
databaseVersion = "12.2";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.passbolt;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Passbolt, your password manager";
|
||||
defaultPort = "4433";
|
||||
scheme = "https";
|
||||
dataDirEnabled = false;
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [
|
||||
"100032:100 ${cfg.configDir}"
|
||||
"100032:100 ${cfg.configDir}/gpg"
|
||||
"100032:100 ${cfg.configDir}/jwt"
|
||||
"100999:100 ${cfg.configDir}/database"
|
||||
];
|
||||
secrets = [
|
||||
"passbolt/db_name"
|
||||
"passbolt/db_username"
|
||||
"passbolt/db_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
passbolt-server:
|
||||
image: docker.io/passbolt/passbolt:${passboltVersion}
|
||||
container_name: passbolt-server
|
||||
hostname: passbolt-server
|
||||
user: '33:33'
|
||||
networks:
|
||||
passbolt:
|
||||
ports:
|
||||
- "${cfg.port}:4433/tcp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/gpg:/etc/passbolt/gpg
|
||||
- ${cfg.configDir}/jwt:/etc/passbolt/jwt
|
||||
environment:
|
||||
APP_DEFAULT_TIMEZONE: ${config.time.timeZone}
|
||||
APP_FULL_BASE_URL: https://${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
DATASOURCES_DEFAULT_HOST: "passbolt-database"
|
||||
DATASOURCES_DEFAULT_USERNAME: ${config.sops.placeholder."passbolt/db_username"}
|
||||
DATASOURCES_DEFAULT_PASSWORD: ${config.sops.placeholder."passbolt/db_password"}
|
||||
DATASOURCES_DEFAULT_DATABASE: ${config.sops.placeholder."passbolt/db_name"}
|
||||
EMAIL_DEFAULT_FROM_NAME: "Passbolt"
|
||||
EMAIL_TRANSPORT_DEFAULT_HOST: ${config.numbus-server.mail.smtpServer}
|
||||
EMAIL_TRANSPORT_DEFAULT_PORT: ${toString config.numbus-server.mail.smtpPort}
|
||||
EMAIL_TRANSPORT_DEFAULT_USERNAME: ${config.numbus-server.mail.smtpUsername}
|
||||
EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${config.sops.placeholder."mail/smtpPassword"}
|
||||
EMAIL_TRANSPORT_DEFAULT_TLS: true
|
||||
EMAIL_DEFAULT_FROM: passbolt-noreply@${config.numbus-server.services.domain}
|
||||
PASSBOLT_SSL_FORCE: true
|
||||
command:
|
||||
[
|
||||
"/usr/bin/wait-for.sh",
|
||||
"-t",
|
||||
"0",
|
||||
"passbolt-database:3306",
|
||||
"--",
|
||||
"/docker-entrypoint.sh"
|
||||
]
|
||||
depends_on:
|
||||
- passbolt-database
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
passbolt-database:
|
||||
image: docker.io/library/mariadb:${databaseVersion}
|
||||
container_name: passbolt-database
|
||||
hostname: passbolt-database
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
passbolt:
|
||||
volumes:
|
||||
- ${cfg.configDir}/database:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: "true"
|
||||
MYSQL_DATABASE: ${config.sops."passbolt/db_name"}
|
||||
MYSQL_USER: ${config.sops."passbolt/db_username"}
|
||||
MYSQL_PASSWORD: ${config.sops."passbolt/db_password"}
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
passbolt:
|
||||
name: passbolt
|
||||
driver: bridge
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "traefik";
|
||||
# Version tagging
|
||||
traefikVersion = "v3.6.8";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.traefik;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Traefik reverse proxy, one to rule them all";
|
||||
defaultPort = "7780";
|
||||
pod = "false";
|
||||
startDelay = 10;
|
||||
dataDirEnabled = false;
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dependencies = [
|
||||
"sops-install-secrets.service"
|
||||
"network-online.target"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${cfg.configDir}/certs"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
traefik:
|
||||
image: docker.io/library/traefik:${traefikVersion}
|
||||
container_name: traefik
|
||||
hostname: traefik
|
||||
user: '1000:1000'
|
||||
network_mode: pasta
|
||||
ports:
|
||||
- "${cfg.port}:8080/tcp"
|
||||
- "443:443/tcp"
|
||||
volumes:
|
||||
- ${config.sops.templates."traefik/config".path}:/etc/traefik/traefik.yaml:ro
|
||||
- ${cfg.configDir}/certs:/var/traefik/certs
|
||||
- /etc/traefik/rules:/etc/traefik/rules:ro
|
||||
environment:
|
||||
- CF_DNS_API_TOKEN=${config.sops.placeholder."traefik/cloudflare_api_token"}
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."traefik/cloudflare_api_token" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/traefik.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.templates."traefik/config"= {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
global:
|
||||
checkNewVersion: false
|
||||
sendAnonymousUsage: false
|
||||
log:
|
||||
level: ${cfg.logLevel}
|
||||
accesslog: {}
|
||||
api:
|
||||
dashboard: true
|
||||
insecure: false
|
||||
entryPoints:
|
||||
web:
|
||||
address: :80
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
websecure:
|
||||
address: :443
|
||||
forwardedHeaders:
|
||||
trustedIPs:
|
||||
- "127.0.0.1/32"
|
||||
- "10.0.0.0/8"
|
||||
- "192.168.0.0/16"
|
||||
- "172.16.0.0/12"
|
||||
certificatesResolvers:
|
||||
cloudflare:
|
||||
acme:
|
||||
email: ${config.numbus-server.mail.adminAddress}
|
||||
storage: /var/traefik/certs/cloudflare-acme.json
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
dnsChallenge:
|
||||
provider: cloudflare
|
||||
resolvers:
|
||||
- "1.1.1.1:53"
|
||||
- "9.9.9.9:53"
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
providers:
|
||||
file:
|
||||
directory: "/etc/traefik/rules"
|
||||
watch: true
|
||||
'';
|
||||
path = "/etc/traefik/traefik.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/secureHeaders" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
http:
|
||||
middlewares:
|
||||
secureHeaders:
|
||||
headers:
|
||||
FrameDeny: true
|
||||
AccessControlAllowMethods: 'GET,OPTIONS,PUT'
|
||||
AccessControlAllowOriginList:
|
||||
- origin-list-or-null
|
||||
AccessControlMaxAge: 100
|
||||
AddVaryHeader: true
|
||||
BrowserXssFilter: true
|
||||
ContentTypeNosniff: true
|
||||
ForceSTSHeader: true
|
||||
STSIncludeSubdomains: true
|
||||
STSPreload: true
|
||||
ContentSecurityPolicy: default-src 'self' 'unsafe-inline'
|
||||
CustomFrameOptionsValue: SAMEORIGIN
|
||||
ReferrerPolicy: same-origin
|
||||
PermissionsPolicy: vibrate 'self'
|
||||
STSSeconds: 315360000
|
||||
'';
|
||||
path = "/etc/traefik/rules/secureHeaders.yaml";
|
||||
};
|
||||
|
||||
sops.templates."traefik/rules/secureTLS" = {
|
||||
gid = "100";
|
||||
uid = "100999";
|
||||
mode = "0400";
|
||||
content = ''
|
||||
tls:
|
||||
options:
|
||||
secureTLS:
|
||||
minVersion: VersionTLS12
|
||||
sniStrict: true
|
||||
curvePreferences:
|
||||
- CurveP521
|
||||
- CurveP384
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
'';
|
||||
path = "/etc/traefik/rules/secureTLS.yaml";
|
||||
};
|
||||
};
|
||||
|
||||
extraOptions = {
|
||||
enable.default = true;
|
||||
logLevel = mkOption {
|
||||
type = types.enum [ "TRACE" "DEBUG" "INFO" "WARN" "ERROR" "FATAL" ];
|
||||
default = "ERROR";
|
||||
example = "ERROR";
|
||||
description = "The level of detail Traefik should print in the logs.";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "uptimeKuma";
|
||||
# Version tagging
|
||||
uptimeKumaVersion = "2.2.0-rootless";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.uptimeKuma;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Uptime-Kuma, \"don't let your server down !\" monitoring tools";
|
||||
defaultPort = "3001";
|
||||
scheme = "http";
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [ "100999:100 ${optimizedDir}" ];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
uptimekuma:
|
||||
image: docker.io/louislam/uptime-kuma:${uptimeKumaVersion}
|
||||
container_name: uptime-kuma
|
||||
hostname: uptime-kuma
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
uptime-kuma:
|
||||
ipv4_address: 10.89.100.253
|
||||
ports:
|
||||
- "${cfg.port}:3001/tcp"
|
||||
volumes:
|
||||
- ${optimizedDir}:/app/data
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
uptime-kuma:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.100.0/24"
|
||||
gateway: "10.89.100.254"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Container config
|
||||
name = "vscodium";
|
||||
# Version tagging
|
||||
vscodiumVersion = "1.110.11607-ls15";
|
||||
# Storage optimization
|
||||
spindown = config.numbus-server.hardware.HddSpindown;
|
||||
optimizedDir = if spindown.enable && (spindown.optimize == "compatible" || (isList spindown.optimize && elem name spindown.optimize))
|
||||
then cfg.configDir
|
||||
else cfg.dataDir;
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.vscodium;
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "VScodium, an open-source version of VScode in your web browser";
|
||||
defaultPort = "8000";
|
||||
configDirEnabled = optimizedDir == cfg.configDir;
|
||||
dataDirEnabled = optimizedDir == cfg.dataDir;
|
||||
middlewares = [ "secureHeaders" ];
|
||||
dirPermissions = [
|
||||
"100999:100 ${optimizedDir}"
|
||||
"100999:100 ${cfg.configDir}"
|
||||
"100999:100 ${optimizedDir}/workspace"
|
||||
"100999:100 ${cfg.configDir}/config"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
vscodium:
|
||||
image: lscr.io/linuxserver/vscodium-web:${vscodiumVersion}
|
||||
container_name: vscodium
|
||||
hostname: vscodium
|
||||
user: '1000:1000'
|
||||
networks:
|
||||
vscodium:
|
||||
ipv4_address: 10.89.50.253
|
||||
ports:
|
||||
- "${defaultPort}:8000"
|
||||
volumes:
|
||||
- ${cfg.configDir}/config:/config
|
||||
- ${optimizedDir}/workspace:/workspace
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=${time.timeZone}
|
||||
- CONNECTION_TOKEN=${config.sops.placeholder."vscodium/connection_token"}
|
||||
shm_size: "1gb"
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
cap_drop:
|
||||
- NET_RAW
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
vscodium:
|
||||
name: vscodium
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: "10.89.50.0/24"
|
||||
gateway: "10.89.50.254"
|
||||
'';
|
||||
|
||||
extraConfig = {
|
||||
sops.secrets."vscodium/connection_token" = {
|
||||
sopsFile = /etc/nixos/secrets/podman/vscodium.yaml;
|
||||
gid = "100";
|
||||
uid = "1000";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./applications/default.nix
|
||||
./dns/default.nix
|
||||
./system/default.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
adguardVersion = "latest";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.adguard;
|
||||
# Container config
|
||||
name = "adguard";
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "AdGuard, feature-rich DNS service";
|
||||
defaultPort = "3000";
|
||||
scheme = "http";
|
||||
dataDirEnabled = false;
|
||||
startDelay = 10;
|
||||
dependencies = [
|
||||
"network.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
|
||||
composeText = ''
|
||||
services:
|
||||
adguardhome:
|
||||
image: adguard/adguardhome:${adguardVersion}
|
||||
container_name: adguard
|
||||
hostname: adguard
|
||||
network_mode: pasta
|
||||
user: '1000:1000'
|
||||
ports:
|
||||
- "${cfg.port}:3000/tcp"
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
volumes:
|
||||
- ${cfg.configDir}/work:/opt/adguardhome/work
|
||||
- ${cfg.configDir}/config:/opt/adguardhome/conf
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: unless-stopped
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./adguard.nix
|
||||
# Tested
|
||||
./pi-hole.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
# Version tagging
|
||||
piholeVersion = "2026.02.0";
|
||||
# Helper
|
||||
helper = import ../service-helper.nix { inherit config pkgs lib; };
|
||||
cfg = config.numbus-server.services.pi-hole;
|
||||
# Container config
|
||||
name = "pi-hole";
|
||||
# DNS config
|
||||
dnsConfig = ''
|
||||
|
||||
'';
|
||||
in
|
||||
|
||||
helper.mkPodmanService {
|
||||
inherit name;
|
||||
description = "Pi-Hole, the ads black hole";
|
||||
defaultPort = "4443";
|
||||
scheme = "https";
|
||||
dataDirEnabled = false;
|
||||
startDelay = 10;
|
||||
dependencies = [
|
||||
"network.target"
|
||||
];
|
||||
middlewares = [
|
||||
"secureHeaders"
|
||||
];
|
||||
dirPermissions = [
|
||||
"100999:100 ${cfg.configDir}"
|
||||
];
|
||||
secrets = [
|
||||
"pi-hole/web_password"
|
||||
];
|
||||
|
||||
# Compose file good
|
||||
composeText = ''
|
||||
services:
|
||||
pi-hole:
|
||||
image: docker.io/pihole/pihole:${piholeVersion}
|
||||
container_name: pi-hole
|
||||
hostname: pi-hole
|
||||
network_mode: pasta
|
||||
ports:
|
||||
- "${cfg.port}:443/tcp"
|
||||
- "53:53/tcp"
|
||||
- "53:53/udp"
|
||||
volumes:
|
||||
- ${cfg.configDir}:/etc/pihole
|
||||
environment:
|
||||
PIHOLE_UID: '1000'
|
||||
PIHOLE_GID: '1000'
|
||||
TZ: ${config.time.timeZone}
|
||||
FTLCONF_webserver_domain: ${cfg.subdomain}.${config.numbus-server.services.domain}
|
||||
FTLCONF_dns_domain_name: "${config.numbus-server.services.domain}"
|
||||
FTLCONF_webserver_api_password: ${config.sops.placeholder."pi-hole/web_password"}
|
||||
FTLCONF_dns_upstreams: 9.9.9.9;149.112.112.112
|
||||
FTLCONF_dns_listeningMode: "BIND"
|
||||
FTLCONF_dns_domain_local: "true"
|
||||
FTLCONF_dhcp_active: "false"
|
||||
FTLCONF_ntp_ipv4_active: "false"
|
||||
FTLCONF_ntp_ipv6_active: "false"
|
||||
FTLCONF_ntp_sync_active: "false"
|
||||
cap_add:
|
||||
- SYS_NICE
|
||||
restart: unless-stopped
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
options.numbus = {
|
||||
services = {
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
example = "numbus.eu";
|
||||
description = "The root domain name (i.e. example.com) that your services will use";
|
||||
};
|
||||
dns = mkOption {
|
||||
type = types.enum [ "pi-hole" "adguard" ];
|
||||
default = "pi-hole";
|
||||
example = "pi-hole";
|
||||
description = "The preferred DNS resolver service (pi-hole or adguard) that other services should depend on";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.clamav;
|
||||
onAccessPaths = lib.mapAttrsToList (n: v: v.dataDir) (lib.filterAttrs (n: v:
|
||||
v ? enable && v.enable && v ? dataDir && v.dataDir != null && v.dataDir != false
|
||||
) config.numbus-server.services);
|
||||
clamonacc_virus_notifier = pkgs.writeScript "clamonacc_virus_notifier.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
echo "CLAM_VIRUSEVENT_VIRUSNAME=\"$CLAM_VIRUSEVENT_VIRUSNAME\"" > /var/lib/clamav/virus_event.env
|
||||
echo "CLAM_VIRUSEVENT_FILENAME=\"$CLAM_VIRUSEVENT_FILENAME\"" >> /var/lib/clamav/virus_event.env
|
||||
|
||||
/run/wrappers/bin/sudo /run/current-system/sw/bin/systemctl start clamav-virus-notify.service
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.services.clamav = {
|
||||
enable = mkEnableOption "ClamAV open-source anti-virus software";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.systemPackages = [ pkgs.clamav pkgs.curl ];
|
||||
|
||||
system.activationScripts.clamav-quarantine = ''
|
||||
mkdir -p /quarantine
|
||||
chown clamav:clamav /quarantine
|
||||
chmod 440 /quarantine
|
||||
'';
|
||||
|
||||
security.sudo.extraRules = [{
|
||||
users = [ "clamav" ];
|
||||
commands = [{
|
||||
command = "/run/current-system/sw/bin/systemctl start clamav-virus-notify.service";
|
||||
options = [ "NOPASSWD" ];
|
||||
}];
|
||||
}];
|
||||
|
||||
services.clamav = {
|
||||
updater.enable = true;
|
||||
clamonacc.enable = true;
|
||||
|
||||
scanner = {
|
||||
enable = true;
|
||||
interval = "*-*-* 04:00:00"; # Everyday at 4am
|
||||
scanDirectories = [
|
||||
"/etc"
|
||||
"/home"
|
||||
"/var/lib"
|
||||
"/var/tmp"
|
||||
"/tmp"
|
||||
];
|
||||
};
|
||||
|
||||
daemon = {
|
||||
enable = true;
|
||||
settings = {
|
||||
OnAccessPrevention = true;
|
||||
OnAccessIncludePath = onAccessPaths;
|
||||
VirusEvent = "${clamonacc_virus_notifier}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.clamav-periodic-scan = mkIf (onAccessPaths != []) {
|
||||
description = "Periodic ClamAV virus scan";
|
||||
after = [ "clamav-daemon.service" "clamav-freshclam.service" ];
|
||||
requires = [ "clamav-daemon.service" ];
|
||||
wants = [ "clamav-freshclam.service" ];
|
||||
onFailure = [ "clamav-virus-notify.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.clamav}/bin/clamdscan --multiscan --fdpass --infected --allmatch --move=/quarantine ${lib.escapeShellArgs onAccessPaths}";
|
||||
Slice = "system-clamav.slice";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.clamav-periodic-scan = mkIf (onAccessPaths != []) {
|
||||
description = "Timer for ClamAV periodic scan";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "*-1/3-01 04:00:00";
|
||||
Persistent = true;
|
||||
Unit = "clamav-periodic-scan.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# To test
|
||||
./backup-client.nix
|
||||
./clamav.nix
|
||||
./virtualization.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.numbus-server.services.virtualization;
|
||||
in
|
||||
|
||||
{
|
||||
options.numbus-server.services.virtualization = {
|
||||
enable = mkEnableOption "QEMU/KVM virtualization software";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
virtualisation.libvirtd.enable = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
{ config, pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [ ];
|
||||
|
||||
# Bootloader options
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
# Boot splash instead of log messages
|
||||
boot = {
|
||||
plymouth.enable = true;
|
||||
# Enable "Silent boot"
|
||||
consoleLogLevel = 3;
|
||||
initrd.verbose = false;
|
||||
kernelParams = [
|
||||
"quiet"
|
||||
"udev.log_level=3"
|
||||
"systemd.show_status=auto"
|
||||
"pcie_aspm=off"
|
||||
];
|
||||
loader.timeout = 1;
|
||||
};
|
||||
|
||||
# Hardware settings
|
||||
hardware.enableRedistributableFirmware = true;
|
||||
hardware.cpu.intel.updateMicrocode = true;
|
||||
hardware.cpu.amd.updateMicrocode = true;
|
||||
|
||||
# Enable networking
|
||||
networking.networkmanager.enable = true;
|
||||
networking.hostName = "nix-tv"; # Define your hostname.
|
||||
|
||||
# Set your time zone.
|
||||
time.timeZone = "Europe/Paris";
|
||||
|
||||
# Select internationalisation properties.
|
||||
i18n.defaultLocale = "fr_FR.UTF-8";
|
||||
|
||||
i18n.extraLocaleSettings = {
|
||||
LC_ADDRESS = "fr_FR.UTF-8";
|
||||
LC_IDENTIFICATION = "fr_FR.UTF-8";
|
||||
LC_MEASUREMENT = "fr_FR.UTF-8";
|
||||
LC_MONETARY = "fr_FR.UTF-8";
|
||||
LC_NAME = "fr_FR.UTF-8";
|
||||
LC_NUMERIC = "fr_FR.UTF-8";
|
||||
LC_PAPER = "fr_FR.UTF-8";
|
||||
LC_TELEPHONE = "fr_FR.UTF-8";
|
||||
LC_TIME = "fr_FR.UTF-8";
|
||||
};
|
||||
|
||||
# Disable the X11 windowing system.
|
||||
services.xserver.enable = false;
|
||||
services.xserver.xkb.layout = "fr";
|
||||
|
||||
# Enable the KDE Plasma Desktop Environment.
|
||||
services.displayManager.sddm.enable = true;
|
||||
services.displayManager.sddm.wayland.enable = true;
|
||||
services.desktopManager.plasma6.enable = true;
|
||||
services.displayManager.autoLogin.enable = true;
|
||||
services.displayManager.autoLogin.user = "nix-tv";
|
||||
|
||||
# Configure console keymap
|
||||
console.keyMap = "fr";
|
||||
|
||||
# Enable CUPS to print documents.
|
||||
services.printing.enable = true;
|
||||
|
||||
# Enable sound with pipewire.
|
||||
services.pulseaudio.enable = false;
|
||||
security.rtkit.enable = true;
|
||||
services.pipewire = {
|
||||
enable = true;
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true;
|
||||
pulse.enable = true;
|
||||
};
|
||||
|
||||
# Define a user account. Don't forget to set a password with ‘passwd’.
|
||||
users.users.nix-tv = {
|
||||
isNormalUser = true;
|
||||
description = "Nix TV";
|
||||
extraGroups = [ "networkmanager" "wheel" ];
|
||||
uid = 1000;
|
||||
initialPassword = "changeMe!";
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINjX8YqPMXpuqIrEClMHD2Ol+gqj3+28rYXGWybVNpim raphael@MacBook-Pro-de-Raphael.local"
|
||||
];
|
||||
};
|
||||
|
||||
# Login message
|
||||
environment.loginShellInit = ''
|
||||
if [ "$(id -u)" -eq 1000 ]; then
|
||||
if [ -n "$SSH_TTY" ]; then
|
||||
fastfetch
|
||||
echo -e "\n\nWelcome to your Nix TV !\n\n- This system is managed by NixOS\n- All changes are futile\n- Please consider buying support if you can't get your TV running\n- Have a nice day and enjoy !"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# Allow unfree packages
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
fastfetch
|
||||
];
|
||||
|
||||
# Install flatpak
|
||||
services.flatpak.enable = true;
|
||||
|
||||
|
||||
# Enable the OpenSSH daemon.
|
||||
services.openssh.enable = true;
|
||||
|
||||
# Open ports in the firewall.
|
||||
networking.firewall.allowedTCPPorts = [ ];
|
||||
networking.firewall.allowedUDPPorts = [ ];
|
||||
networking.firewall.enable = true;
|
||||
|
||||
# Enable auto updates
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
allowReboot = false;
|
||||
flake = inputs.self.outPath;
|
||||
flags = [ "--print-build-logs" ];
|
||||
dates = "20:00";
|
||||
randomizedDelaySec = "45min";
|
||||
};
|
||||
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "weekly";
|
||||
options = "--delete-older-than 7d";
|
||||
};
|
||||
|
||||
# Enable NixOS flakes
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
# Enable auto nix-store optimization
|
||||
nix.settings.auto-optimise-store = true;
|
||||
|
||||
system.stateVersion = "25.11";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
description = "Flake file for Nix TV project";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
nix-flatpak.url = "github:gmodena/nix-flatpak/?ref=latest";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nix-flatpak, ... }@inputs: {
|
||||
nixosConfigurations."nix-tv" = nixpkgs.lib.nixosSystem {
|
||||
specialArgs = { inherit inputs; };
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
# Nix flatpak
|
||||
nix-flatpak.nixosModules.nix-flatpak
|
||||
./flatpak.nix
|
||||
# Core configuration
|
||||
./configuration.nix
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
services.flatpak.remotes = lib.mkOptionDefault [{
|
||||
name = "flathub";
|
||||
location = "https://dl.flathub.org/repo/flathub.flatpakrepo";
|
||||
}];
|
||||
|
||||
services.flatpak.update.auto.enable = true;
|
||||
services.flatpak.uninstallUnmanaged = true;
|
||||
|
||||
services.flatpak.packages = [
|
||||
"io.gitlab.librewolf-community"
|
||||
"app.grayjay.Grayjay"
|
||||
"com.github.unrud.VideoDownloader"
|
||||
"com.github.tchx84.Flatseal"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,1399 @@
|
||||
#!/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
|
||||
|
||||
|
||||
|
||||
# --- UTILITY FUNCTIONS --->
|
||||
echod() {
|
||||
MESSAGE=${1}
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e ${MESSAGE}
|
||||
fi
|
||||
}
|
||||
|
||||
ssh_to_host() {
|
||||
local COMMAND="${1}"
|
||||
ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
|
||||
}"Invalid IP address format."
|
||||
|
||||
get_valid_input() {
|
||||
local VAR_NAME="${1}"
|
||||
local HEADER="${2}"
|
||||
local PLACEHOLDER="${3}"
|
||||
local REGEX="${4}"
|
||||
local MANDATORY="${5:-true}"
|
||||
local SENSITIVE="${6:-false}"
|
||||
|
||||
if [[ "${MANDATORY}" == "true" ]]; then
|
||||
local PROMPT="(Required) > "
|
||||
elif [[ "${MANDATORY}" == "false" ]]; then
|
||||
local PROMPT="(Optional) > "
|
||||
fi
|
||||
|
||||
while true; do
|
||||
local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}")
|
||||
|
||||
# Handle empty input
|
||||
if [[ -z "${INPUT}" ]]; then
|
||||
if [[ "${MANDATORY}" == true ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ This field is mandatory."
|
||||
continue
|
||||
else
|
||||
INPUT=""
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
# Handle Regex Validation
|
||||
if [[ -n "${REGEX}" ]]; then
|
||||
if [[ "${INPUT}" =~ ${REGEX} ]]; then
|
||||
export "${VAR_NAME}"="${INPUT}"
|
||||
break
|
||||
else
|
||||
gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again."
|
||||
fi
|
||||
else
|
||||
export "${VAR_NAME}"="${INPUT}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
# --- UTILITY FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- GLOBAL FUNCTIONS --->
|
||||
cleanup() {
|
||||
echo -e "\n ✅ Cleaning up..."
|
||||
|
||||
rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/
|
||||
|
||||
if [[ ${WEB_MODE} -eq 1 && -n "${BRIDGE_PID:-}" ]]; then
|
||||
kill ${BRIDGE_PID}
|
||||
fi
|
||||
}
|
||||
|
||||
compatibility_check() {
|
||||
TEST_FAIL=0
|
||||
|
||||
if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then
|
||||
echod "\n ✅ NixOS system detected."
|
||||
else
|
||||
TEST_FAIL=$((TEST_FAIL + 1))
|
||||
echo -e "\n ❌ You are not on a NixOS based system. This is required to continue."
|
||||
fi
|
||||
|
||||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||||
echod "\n ✅ x86_64 system detected."
|
||||
else
|
||||
TEST_FAIL=$((TEST_FAIL + 1))
|
||||
echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue."
|
||||
fi
|
||||
|
||||
if [[ ${TEST_FAIL} -gt 0 ]]; then
|
||||
COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will very likely fail. Continue ?" \
|
||||
"No" \
|
||||
"Yes, I know what I am doing")
|
||||
[[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1
|
||||
[[ "${COMPATIBILITY_OVERRIDE}" != "No" ]] && echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
hierarchy_preparation() {
|
||||
echod "\n 🔄 Preparing the folder hierarchy for the final configuration..."
|
||||
|
||||
if [[ -e config/* ]]; then
|
||||
echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up."
|
||||
OLD_CONFIG_PATH="trash/$(date +"%Y-%m-%d-%Hh%M")/"
|
||||
mkdir -${MKDIR_FLAGS} ${OLD_CONFIG_PATH}
|
||||
mv -${MV_FLAGS} config/ ${OLD_CONFIG_PATH}
|
||||
echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed."
|
||||
fi
|
||||
|
||||
# Script folders
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp
|
||||
[[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web
|
||||
|
||||
# Secrets
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system
|
||||
if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman
|
||||
fi
|
||||
|
||||
echod "\n ✅ Folder hierarchy ready"
|
||||
}
|
||||
|
||||
hardware_detection() {
|
||||
local TMPFILE="/tmp/nixos-installation-hw-detection"
|
||||
|
||||
ssh_to_host 'bash -s' << SSHEND
|
||||
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}")
|
||||
else
|
||||
TARGET_GRAPHICS="false"
|
||||
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 [[ "\$TRANSPORT_PROTOCOL" == "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_TARGET_PASSWORD}" | 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
|
||||
|
||||
echo "# Hardware detection results on \$(date)" > "${TMPFILE}"
|
||||
for var in \
|
||||
TARGET_GRAPHICS \
|
||||
TARGET_GRAPHICS_RENDERER \
|
||||
TARGET_USB_CORAL \
|
||||
TARGET_PCIE_CORAL \
|
||||
TARGET_ZIGBEE_DEVICE \
|
||||
TARGET_INTERFACE \
|
||||
TARGET_TPM \
|
||||
TARGET_TPM_VERSION; do
|
||||
echo "export \${var}=\${!var}" >> "${TMPFILE}"
|
||||
done
|
||||
|
||||
for var in \
|
||||
TARGET_GRAPHICS_BRAND \
|
||||
DISK_DEVPATH \
|
||||
DISK_NAME \
|
||||
DISK_TYPE \
|
||||
DISK_HEALTH \
|
||||
DISK_ID \
|
||||
DISK_SIZE; do
|
||||
declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}"
|
||||
done
|
||||
SSHEND
|
||||
|
||||
scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
|
||||
source "${TMPFILE}"
|
||||
|
||||
local DISK_FLAT_ARRAY=()
|
||||
for i in "${!DISK_NAME[@]}"; do
|
||||
DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}")
|
||||
done
|
||||
|
||||
jq -n \
|
||||
--argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \
|
||||
--argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \
|
||||
--argjson tpu_usb "${TARGET_USB_CORAL:-false}" \
|
||||
--argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \
|
||||
--argjson tpm_enabled "${TARGET_TPM:-false}" \
|
||||
--arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \
|
||||
--arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \
|
||||
--arg interface "${TARGET_INTERFACE:-}" \
|
||||
--argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \
|
||||
'
|
||||
{
|
||||
graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer },
|
||||
tpu: { usb: $tpu_usb, pcie: $tpu_pcie },
|
||||
tpm: { enabled: $tpm_enabled, version: $tpm_version },
|
||||
zigbee: { device: $zigbee_device },
|
||||
network: { interface: $interface },
|
||||
disks: [
|
||||
$ARGS.positional | range(0; length; 6) as $i | {
|
||||
name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5]
|
||||
}
|
||||
]
|
||||
}' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH}
|
||||
|
||||
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then
|
||||
echo -e "\n✅ Hardware configuration generated"
|
||||
else
|
||||
echo -e "\n❌ Failed to generate hardware configuration"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
# --- GLOBAL FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN WEB FUNCTIONS --->
|
||||
launch_configurator() {
|
||||
echo -e "\n 🚀 Launching Numbus Configurator..."
|
||||
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
|
||||
export BRIDGE_PID=$!
|
||||
|
||||
echo -e "\n ➡️ Open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")"
|
||||
xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true
|
||||
}
|
||||
# --- MAIN WEB FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN TUI FUNCTIONS --->
|
||||
preparation() {
|
||||
echo -e "\n ➡️ This script will now guide you through the configuration and gather the necessary information."
|
||||
|
||||
echo ""
|
||||
RAW_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \
|
||||
"Numbus Server : Professional-grade hosting, strictly kept under your roof." \
|
||||
"Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \
|
||||
"Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \
|
||||
"Numbus TV : A premium cinematic experience free from trackers and forced subscriptions.")
|
||||
|
||||
case "${RAW_DEVICE_TYPE}" in
|
||||
"Numbus Server : "* ) DEVICE_TYPE="server" ;;
|
||||
"Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;;
|
||||
"Numbus Computer : "* ) DEVICE_TYPE="computer" ;;
|
||||
"Numbus TV : "* ) DEVICE_TYPE="tv" ;;
|
||||
esac
|
||||
|
||||
RAW_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \
|
||||
"Interactive : You don't already have a configuration." \
|
||||
"Non-interactive : You have a valid configuration hosted on a Git platform.")
|
||||
|
||||
case "${RAW_DEPLOYMENT_MODE}" in
|
||||
"Interactive : "* ) DEPLOYMENT_MODE="interactive" ;;
|
||||
"Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;;
|
||||
esac
|
||||
|
||||
if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then
|
||||
git_url() {
|
||||
IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :")
|
||||
}
|
||||
|
||||
git_url
|
||||
|
||||
until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do
|
||||
echo -e "\n ⚠️ This did not work correctly."
|
||||
|
||||
echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}"
|
||||
read URL
|
||||
|
||||
if [[ "${URL^^}" == "N" ]]; then
|
||||
git_url
|
||||
fi
|
||||
|
||||
echo -e "\n You will be prompted for your credentials again. Make sure that they are correct."
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
gum format -- \
|
||||
"➡️ To continue, you need to start the target device in a NixOS live environment :
|
||||
1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**.
|
||||
2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...)
|
||||
3. Make sure your computer allows booting from USB drives and is in UEFI mode.
|
||||
4. Boot into the NixOS live environment.
|
||||
5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`"
|
||||
|
||||
echo ""
|
||||
gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; }
|
||||
|
||||
# LIVE TARGET SETTINGS
|
||||
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}"
|
||||
user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
|
||||
|
||||
# INTERNATIONALIZATION SETTINGS
|
||||
user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc"
|
||||
user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc"
|
||||
user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc"
|
||||
}
|
||||
|
||||
configuration() {
|
||||
if [[ "${DEVICE_TYPE}" == "server" ]]; then
|
||||
|
||||
# Users & Groups
|
||||
user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve"
|
||||
user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}"
|
||||
user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)."
|
||||
|
||||
echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains"
|
||||
# TRAEFIK SETTINGS
|
||||
user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}"
|
||||
user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
|
||||
|
||||
echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)"
|
||||
# SMTP SETTINGS
|
||||
user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}"
|
||||
user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
|
||||
user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
|
||||
user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number."
|
||||
|
||||
echo -e "\n\n ➡️ This server will connect to your local network and you will configure its IP address\n"
|
||||
# NETWORK SETTINGS
|
||||
user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
|
||||
user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format."
|
||||
user_input "HOME_SERVER_IP" " 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.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
|
||||
elif [[ "${DEVICE_TYPE}" == "backup" ]]; then
|
||||
:
|
||||
elif [[ "${DEVICE_TYPE}" == "computer" ]]; then
|
||||
:
|
||||
elif [[ "${DEVICE_TYPE}" == "tv" ]]; then
|
||||
:
|
||||
fi
|
||||
}
|
||||
|
||||
setup_ssh() {
|
||||
echod "\n ✅ Generating new SSH key for numbus-admin..."
|
||||
|
||||
chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..."
|
||||
fi
|
||||
|
||||
if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ SSH key copied successfully"
|
||||
fi
|
||||
else
|
||||
echo -e "\n ❌ Failed to copy SSH key. Please check the host IP and password."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
if gum confirm "Change the subdomain of ${service} ?"; then
|
||||
SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" )
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
users_and_groups() {
|
||||
|
||||
declare -A ACL_GROUPS
|
||||
declare -A ACL_USERS
|
||||
|
||||
compute_acl_services() {
|
||||
EXCLUDED_SERVICES=( "clamav" ) # Those are the services that don't have a web page or don't support SSO
|
||||
local ALL_SERVICES=("${SELECTED_DNS_SERVICE[@]}" "${SELECTED_WEB_APPLICATIONS[@]}" "${SELECTED_SYSTEM_SERVICES[@]}")
|
||||
|
||||
for i in "${!ALL_SERVICES[@]}"; do
|
||||
for excluded in "${EXCLUDED_SERVICES[@]}"; do
|
||||
if [[ "${ALL_SERVICES[${i}]}" == "${excluded}" ]]; then
|
||||
unset "ALL_SERVICES[${i}]"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
|
||||
show_groups_table() {
|
||||
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then
|
||||
gum style --italic --foreground "#6272a4" -- "No groups configured."
|
||||
return
|
||||
fi
|
||||
|
||||
# We use CSV format with quotes to handle comma-separated services correctly
|
||||
local csv="Group Name,Allowed Services\n"
|
||||
for g in "${!ACL_GROUPS[@]}"; do
|
||||
csv+="\"$g\",\"${ACL_GROUPS[$g]}\"\n"
|
||||
done
|
||||
|
||||
printf "%b" "$csv" | gum table
|
||||
}
|
||||
|
||||
add_group() {
|
||||
if [[ ${#ACL_GROUPS[@]} -ge 10 ]]; then
|
||||
gum style --foreground "#ffb86c" -- "⚠ Maximum of 10 groups reached."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
local group_name
|
||||
get_valid_input group_name "Group Name" "^[a-zA-Z0-9_-]+$" true ""
|
||||
|
||||
if [[ -n "${ACL_GROUPS[$group_name]}" ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ Group already exists."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
gum style --foreground "#50fa7b" -- "Select services for $group_name (Space to select, Enter to confirm):"
|
||||
local chosen_services
|
||||
chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
|
||||
|
||||
ACL_GROUPS["$group_name"]="$chosen_services"
|
||||
gum style --foreground "#50fa7b" -- "✔ Group '$group_name' created."
|
||||
sleep 1
|
||||
}
|
||||
|
||||
edit_group() {
|
||||
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi
|
||||
|
||||
local group_keys=("${!ACL_GROUPS[@]}")
|
||||
gum style -- "Select a group to edit:"
|
||||
local group_name=$(gum choose "${group_keys[@]}")
|
||||
|
||||
if [[ -z "$group_name" ]]; then return; fi
|
||||
if [[ "$group_name" == "admin" ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ The admin group cannot be modified."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
gum style --foreground "#50fa7b" -- "Select NEW services for $group_name:"
|
||||
local chosen_services=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
|
||||
|
||||
ACL_GROUPS["$group_name"]="$chosen_services"
|
||||
gum style --foreground "#50fa7b" -- "✔ Group '$group_name' updated."
|
||||
sleep 1
|
||||
}
|
||||
|
||||
remove_group() {
|
||||
if [[ ${#ACL_GROUPS[@]} -eq 0 ]]; then return; fi
|
||||
|
||||
local group_keys=("${!ACL_GROUPS[@]}")
|
||||
gum style -- "Select a group to REMOVE:"
|
||||
local group_name=$(gum choose "${group_keys[@]}")
|
||||
|
||||
if [[ -z "$group_name" ]]; then return; fi
|
||||
if [[ "$group_name" == "admin" ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ The admin group cannot be removed."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$group_name'?"
|
||||
if gum confirm; then
|
||||
unset ACL_GROUPS["$group_name"]
|
||||
gum style --foreground "#50fa7b" -- "✔ Group deleted."
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
manage_groups_menu() {
|
||||
while true; do
|
||||
clear
|
||||
gum style --border double --margin "1" --padding "0 1" --border-foreground "#8be9fd" -- "Group Management (${#ACL_GROUPS[@]}/10)"
|
||||
show_groups_table
|
||||
|
||||
local action=$(gum choose "Add Group" "Edit Group" "Remove Group" "Back")
|
||||
case "$action" in
|
||||
"Add Group") add_group ;;
|
||||
"Edit Group") edit_group ;;
|
||||
"Remove Group") remove_group ;;
|
||||
"Back"|"") break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_users_table() {
|
||||
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then
|
||||
gum style --italic --foreground "#6272a4" -- "No users configured."
|
||||
return
|
||||
fi
|
||||
|
||||
local csv="Username,Name,Email,Health Alerts,ACL Type,ACL Value\n"
|
||||
for u in "${!ACL_USERS[@]}"; do
|
||||
IFS='|' read -r name email phone health type input <<< "${ACL_USERS[$u]}"
|
||||
csv+="\"$u\",\"$name\",\"$email\",\"$health\",\"$type\",\"$input\"\n"
|
||||
done
|
||||
|
||||
printf "%b" "$csv" | gum table
|
||||
}
|
||||
|
||||
add_user() {
|
||||
if [[ ${#ACL_USERS[@]} -ge 20 ]]; then
|
||||
gum style --foreground "#ffb86c" -- "⚠ Maximum of 20 users reached."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
local name username email phone health_alert acl_type acl_value
|
||||
|
||||
get_valid_input username "Username" "^[a-z0-9_-]+$" true ""
|
||||
if [[ -n "${ACL_USERS[$username]}" ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ Username already exists."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
get_valid_input name "Full Name" "" true ""
|
||||
get_valid_input email "Email Address" "$EMAIL_REGEX" true ""
|
||||
get_valid_input phone "Phone Number (E.164, optional)" "$PHONE_REGEX" false ""
|
||||
|
||||
gum style -- "Inform about server health?"
|
||||
if gum confirm; then health_alert="true"; else health_alert="false"; fi
|
||||
|
||||
gum style -- "How should ACL be managed for $username?"
|
||||
acl_type=$(gum choose "Assign to Group" "Manual Service Selection")
|
||||
|
||||
if [[ "$acl_type" == "Assign to Group" ]]; then
|
||||
acl_type="group"
|
||||
local group_keys=("${!ACL_GROUPS[@]}")
|
||||
acl_value=$(gum choose "${group_keys[@]}")
|
||||
else
|
||||
acl_type="manual"
|
||||
gum style --foreground "#50fa7b" -- "Select services for $username:"
|
||||
acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
|
||||
fi
|
||||
|
||||
ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value"
|
||||
gum style --foreground "#50fa7b" -- "✔ User '$username' created."
|
||||
sleep 1
|
||||
}
|
||||
|
||||
edit_user() {
|
||||
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi
|
||||
|
||||
local user_keys=("${!ACL_USERS[@]}")
|
||||
gum style -- "Select a user to edit:"
|
||||
local username=$(gum choose "${user_keys[@]}")
|
||||
|
||||
if [[ -z "$username" ]]; then return; fi
|
||||
|
||||
# Extract current values
|
||||
IFS='|' read -r curr_name curr_email curr_phone curr_health curr_type curr_val <<< "${ACL_USERS[$username]}"
|
||||
local name email phone health_alert acl_type acl_value
|
||||
|
||||
gum style --foreground "#f1fa8c" -- "Editing User: $username"
|
||||
get_valid_input name "Full Name" "" true "$curr_name"
|
||||
get_valid_input email "Email Address" "$EMAIL_REGEX" true "$curr_email"
|
||||
get_valid_input phone "Phone Number" "$PHONE_REGEX" false "$curr_phone"
|
||||
|
||||
if [[ "$username" == "admin" ]]; then
|
||||
gum style --foreground "#ffb86c" -- "Admin health alerts and ACL settings cannot be changed."
|
||||
health_alert="true"
|
||||
acl_type="group"
|
||||
acl_value="admin"
|
||||
sleep 2
|
||||
else
|
||||
gum style -- "Inform about server health? (Currently: $curr_health)"
|
||||
if gum confirm; then health_alert="true"; else health_alert="false"; fi
|
||||
|
||||
gum style -- "How should ACL be managed? (Currently: $curr_type)"
|
||||
acl_type=$(gum choose "Assign to Group" "Manual Service Selection")
|
||||
|
||||
if [[ "$acl_type" == "Assign to Group" ]]; then
|
||||
acl_type="group"
|
||||
local group_keys=("${!ACL_GROUPS[@]}")
|
||||
acl_value=$(gum choose "${group_keys[@]}")
|
||||
else
|
||||
acl_type="manual"
|
||||
gum style --foreground "#50fa7b" -- "Select services for $username:"
|
||||
acl_value=$(gum choose --no-limit "${ACL_SERVICES[@]}" | paste -sd "," -)
|
||||
fi
|
||||
fi
|
||||
|
||||
ACL_USERS["$username"]="$name|$email|$phone|$health_alert|$acl_type|$acl_value"
|
||||
gum style --foreground "#50fa7b" -- "✔ User '$username' updated."
|
||||
sleep 1
|
||||
}
|
||||
|
||||
remove_user() {
|
||||
if [[ ${#ACL_USERS[@]} -eq 0 ]]; then return; fi
|
||||
|
||||
local user_keys=("${!ACL_USERS[@]}")
|
||||
gum style -- "Select a user to REMOVE:"
|
||||
local username=$(gum choose "${user_keys[@]}")
|
||||
|
||||
if [[ -z "$username" ]]; then return; fi
|
||||
if [[ "$username" == "admin" ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ The admin user cannot be removed."
|
||||
sleep 2; return
|
||||
fi
|
||||
|
||||
gum style --foreground "#ff5555" --bold -- "Are you sure you want to delete '$username'?"
|
||||
if gum confirm; then
|
||||
unset ACL_USERS["$username"]
|
||||
gum style --foreground "#50fa7b" -- "✔ User deleted."
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
manage_users_menu() {
|
||||
while true; do
|
||||
clear
|
||||
gum style --border double --margin "1" --padding "0 1" --border-foreground "#ff79c6" -- "User Management (${#ACL_USERS[@]}/20)"
|
||||
show_users_table
|
||||
|
||||
local action=$(gum choose "Add User" "Edit User" "Remove User" "Back")
|
||||
case "$action" in
|
||||
"Add User") add_user ;;
|
||||
"Edit User") edit_user ;;
|
||||
"Remove User") remove_user ;;
|
||||
"Back"|"") break ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
setup_admin_user() {
|
||||
if [[ -n "${ACL_USERS["admin"]}" ]]; then return; fi
|
||||
|
||||
gum style --border rounded --padding "1 2" --margin "1" --border-foreground "#ff79c6" -- "Initial Setup: Administrator User"
|
||||
|
||||
local name email phone
|
||||
get_valid_input name "Admin Full Name" "" true ""
|
||||
get_valid_input email "Admin Email Address" "$EMAIL_REGEX" true ""
|
||||
get_valid_input phone "Admin Phone Number (optional)" "$PHONE_REGEX" false ""
|
||||
|
||||
ACL_USERS["admin"]="$name|$email|$phone|true|group|admin"
|
||||
gum style --foreground "#50fa7b" -- "✔ Administrator configured."
|
||||
sleep 1
|
||||
}
|
||||
|
||||
export_data() {
|
||||
clear
|
||||
gum style --foreground "#50fa7b" --bold -- "--- Provisioning Data Payload ---"
|
||||
echo "GROUPS:"
|
||||
for group in "${!ACL_GROUPS[@]}"; do
|
||||
echo " $group -> Allowed: ${ACL_GROUPS[$group]}"
|
||||
done
|
||||
echo ""
|
||||
echo "USERS (Name|Email|Phone|Alert|AclType|AclValue):"
|
||||
for user in "${!ACL_USERS[@]}"; do
|
||||
echo " $user -> ${ACL_USERS[$user]}"
|
||||
done
|
||||
}
|
||||
|
||||
compute_acl_services
|
||||
ACL_GROUPS["admin"]=$(printf "%s," "${ACL_SERVICES[@]}" | sed 's/,$//')
|
||||
setup_admin_user
|
||||
|
||||
while true; do
|
||||
clear
|
||||
gum style --border double --margin "1" --padding "1 2" --border-foreground "#bd93f9" -- "Numbus Deployment - Access Management"
|
||||
gum style -- "Current state: ${#ACL_GROUPS[@]}/10 Groups | ${#ACL_USERS[@]}/20 Users"
|
||||
echo ""
|
||||
|
||||
local choice=$(gum choose "1. Manage Groups" "2. Manage Users" "3. Finish & Apply Configuration")
|
||||
|
||||
case "$choice" in
|
||||
"1. Manage Groups") manage_groups_menu ;;
|
||||
"2. Manage Users") manage_users_menu ;;
|
||||
"3. Finish & Apply Configuration")
|
||||
export_data
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
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 = \"${INTERNATIONALIZATION_TIMEZONE}\";" >> ${CONFIGURATION_PATH}
|
||||
echo -e " numbus.locale = \"${LOCALE}\";" >> ${CONFIGURATION_PATH}
|
||||
echo -e " numbus.language = \"${INTERNATIONALIZATION_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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt
|
||||
export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt)
|
||||
|
||||
echo -e "\n ✅ Generating sops-nix configuration files..."
|
||||
envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-1 )\`
|
||||
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 -${FILES_CP_FLAGS} deploy.conf ${EXTRA_FILES_PATH}/var/lib/numbus-server/numbus-server.conf
|
||||
|
||||
local CONFIG_EXPORT_DIR="${EXTRA_FILES_PATH}/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 "${EXTRA_FILES_PATH}/"
|
||||
git -C . add -f "templates/"
|
||||
git -C . add -f "deploy.conf"
|
||||
|
||||
echo -e "\n\n🔄 Deploying to the remote server..."
|
||||
nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos
|
||||
nix run github:nix-community/nixos-anywhere -- \
|
||||
--flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \
|
||||
--extra-files ${EXTRA_FILES_PATH} \
|
||||
--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_PASSWORD="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_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/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_PASSWORD} | 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_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short)
|
||||
|
||||
echo ${LIVE_TARGET_PASSWORD} | 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_PASSWORD | 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 ${EXTRA_FILES_PATH}/etc/nixos#numbus-server
|
||||
}
|
||||
# --- MAIN FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- DEFAULT VARIABLES --->
|
||||
WEBSERVER_PORT=${WEBSERVER_PORT:-8088}
|
||||
|
||||
LIVE_DATA_PATH="/run/numbus/web/live_settings.json"
|
||||
HARDWARE_DATA_PATH="/run/numbus/web/hardware.json"
|
||||
|
||||
BRIDGE_SCRIPT="web/logic/bridge.py"
|
||||
CONFIG_FILE="config/numbus.yaml"
|
||||
|
||||
TARGET_USER="nixos"
|
||||
|
||||
TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")"
|
||||
EXTRA_FILES_PATH="${TMP_FILES_PATH}/config"
|
||||
|
||||
if [[ ${DEBUG-0} -eq 1 ]]; then
|
||||
FILES_CP_FLAGS="vau"
|
||||
FILES_RM_FLAGS="vf"
|
||||
DIR_RM_FLAGS="rvf"
|
||||
MKDIR_FLAGS="pv"
|
||||
MV_FLAGS="vu"
|
||||
else
|
||||
DEBUG=0
|
||||
FILES_CP_FLAGS="au"
|
||||
FILES_RM_FLAGS="f"
|
||||
DIR_RM_FLAGS="rf"
|
||||
MKDIR_FLAGS="p"
|
||||
MV_FLAGS="u"
|
||||
fi
|
||||
|
||||
IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
|
||||
SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
|
||||
DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||
EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
PORT_REGEX='^[0-9]{1,5}$'
|
||||
SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
|
||||
PHONE_REGEX='^\+[1-9][0-9]{7,14}$'
|
||||
|
||||
GUM_INPUT_PADDING="1 1"
|
||||
GUM_INPUT_HEADER_FOREGROUND="212"
|
||||
GUM_INPUT_CURSOR_FOREGROUND="212"
|
||||
GUM_INPUT_TIMEOUT="3600"
|
||||
# --- DEFAULTS VARIABLES ---<
|
||||
|
||||
|
||||
|
||||
# --- PRE MAIN LOGIC --->
|
||||
set -euo pipefail
|
||||
clear
|
||||
|
||||
trap cleanup EXIT
|
||||
compatibility_check
|
||||
# --- PRE MAIN LOGIC ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN LOGIC --->
|
||||
echo """
|
||||
_ ____ ____ ______ __ ______
|
||||
/ |/ / / / / |/ / _ )/ / / / __/
|
||||
/ / /_/ / /|_/ / _ / /_/ /\ \
|
||||
/_/|_/\____/_/ /_/____/\____/___/
|
||||
|
||||
"""
|
||||
|
||||
DEPLOY_MODE=$(gum choose --header "Choose your preferred configuration interface :" "Through my browser (Recommended for beginners)" "Through my terminal (TUI)")
|
||||
|
||||
if [[ "$DEPLOY_MODE" == "Through my terminal (TUI)" ]]; then
|
||||
WEB_MODE=0
|
||||
preparation
|
||||
configuration
|
||||
|
||||
else
|
||||
WEB_MODE=1
|
||||
launch_configurator
|
||||
hierarchy_preparation
|
||||
echod "\n ⏳ Waiting for device credentials from web UI..."
|
||||
while [ ! -f configurator/.discovery_ready ]; do
|
||||
sleep 5
|
||||
done
|
||||
echod "\n ✅ Credentials received."
|
||||
INTERNATIONALIZATION_LANGUAGE=$(jq -r '.language' ${LIVE_DATA_PATH})
|
||||
COUNTRY=$(jq -r '.country' ${LIVE_DATA_PATH})
|
||||
INTERNATIONALIZATION_TIMEZONE=$(jq -r '.timeZone' ${LIVE_DATA_PATH})
|
||||
DEVICE_TYPE=$(jq -r '.device' ${LIVE_DATA_PATH})
|
||||
DEPLOYMENT_MODE=$(jq -r '.deploymentMode' ${LIVE_DATA_PATH})
|
||||
if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then
|
||||
REPLICATION_HARDWARE=$(jq -r '.replicationHardware' ${LIVE_DATA_PATH})
|
||||
REPLICATION_STRATEGY=$(jq -r '.replicationStrategy' ${LIVE_DATA_PATH})
|
||||
REPLICATION_SECRETS=$(jq -r '.replicationSecrets' ${LIVE_DATA_PATH})
|
||||
fi
|
||||
LIVE_IP=$(jq -r '.liveIp' ${LIVE_DATA_PATH})
|
||||
LIVE_PASSWORD=$(jq -r '.livePassword' ${LIVE_DATA_PATH})
|
||||
|
||||
fi
|
||||
|
||||
# --- MAIN LOGIC ---<
|
||||
|
||||
|
||||
# 3. Load Credentials and run Discovery
|
||||
|
||||
|
||||
setup_ssh
|
||||
hardware_detection
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ Discovery complete. Hardware data sent to Configurator."
|
||||
fi
|
||||
|
||||
# 4. Wait for Final Configuration Submission
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ⏳ Waiting for final configuration deployment signal..."
|
||||
fi
|
||||
|
||||
while [ ! -f configurator/.deploy_signal ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 5. Execute Deployment
|
||||
echo -e "\n🚀 Starting deployment sequence..."
|
||||
deploy > deploy-out.log 2> deploy-err.log
|
||||
@@ -0,0 +1,1036 @@
|
||||
#!/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
|
||||
|
||||
|
||||
|
||||
# --- UTILITY FUNCTIONS --->
|
||||
echod() {
|
||||
MESSAGE=${1}
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e ${MESSAGE}
|
||||
fi
|
||||
}
|
||||
|
||||
ssh_to_host() {
|
||||
local COMMAND="${1}"
|
||||
ssh -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}" "${COMMAND}"
|
||||
}
|
||||
|
||||
get_valid_input() {
|
||||
local VAR_NAME="${1}"
|
||||
local HEADER="${2}"
|
||||
local PLACEHOLDER="${3}"
|
||||
local REGEX="${4}"
|
||||
local MANDATORY="${5:-true}"
|
||||
local SENSITIVE="${6:-false}"
|
||||
|
||||
if [[ "${MANDATORY}" == "true" ]]; then
|
||||
local PROMPT="(Required) > "
|
||||
elif [[ "${MANDATORY}" == "false" ]]; then
|
||||
local PROMPT="(Optional) > "
|
||||
fi
|
||||
|
||||
while true; do
|
||||
local INPUT=$(gum input --header "${HEADER}" --prompt "${PROMPT}" --placeholder "${PLACEHOLDER}")
|
||||
|
||||
# Handle empty input
|
||||
if [[ -z "${INPUT}" ]]; then
|
||||
if [[ "${MANDATORY}" == true ]]; then
|
||||
gum style --foreground "#ff0000" -- "✖ This field is mandatory."
|
||||
continue
|
||||
else
|
||||
INPUT=""
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
# Handle Regex Validation
|
||||
if [[ -n "${REGEX}" ]]; then
|
||||
if [[ "${INPUT}" =~ ${REGEX} ]]; then
|
||||
export "${VAR_NAME}"="${INPUT}"
|
||||
break
|
||||
else
|
||||
gum style --foreground "#ff0000" -- "✖ Invalid format. Please try again."
|
||||
fi
|
||||
else
|
||||
export "${VAR_NAME}"="${INPUT}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
# --- UTILITY FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- GLOBAL FUNCTIONS --->
|
||||
cleanup() {
|
||||
echo -e "\n ✅ Cleaning up..."
|
||||
|
||||
rm -${DIR_RM_FLAGS} ${TMP_FILES_PATH}/
|
||||
|
||||
if ps -p ${BRIDGE_PID:-} > /dev/null; then
|
||||
kill ${BRIDGE_PID}
|
||||
fi
|
||||
}
|
||||
|
||||
compatibility_check() {
|
||||
TEST_FAIL=0
|
||||
|
||||
if [[ -r /etc/os-release ]] && grep -qi '^ID=nixos\b' /etc/os-release; then
|
||||
echo -e "\n ✅ NixOS system detected."
|
||||
else
|
||||
TEST_FAIL=$((TEST_FAIL + 1))
|
||||
echo -e "\n ❌ You are not on a NixOS based system. This is required to continue."
|
||||
fi
|
||||
|
||||
if [[ "$(uname -m)" == "x86_64" ]]; then
|
||||
echo -e "\n ✅ x86_64 system detected."
|
||||
else
|
||||
TEST_FAIL=$((TEST_FAIL + 1))
|
||||
echo -e "\n ❌ You are not on a x86_64 based system. This is required to continue."
|
||||
fi
|
||||
|
||||
if [[ ${TEST_FAIL} -gt 0 ]]; then
|
||||
COMPATIBILITY_OVERRIDE=$(gum choose --header "Some compatibility checks failed. The installation will very likely fail. Continue ?" \
|
||||
"No" \
|
||||
"Yes, I know what I am doing")
|
||||
[[ "${COMPATIBILITY_OVERRIDE}" == "No" ]] && exit 1
|
||||
[[ "${COMPATIBILITY_OVERRIDE}" != "No" ]] && echo -e "\n ⚠️ Continuing anyways, this is not supported by Numbus."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
hierarchy_preparation() {
|
||||
echod "\n 🔄 Preparing the folder hierarchy for the final configuration..."
|
||||
|
||||
if [[ -e config/* ]]; then
|
||||
echo " ⚠️ It seems you have already run this script. Previously generated files need to be cleaned up."
|
||||
OLD_CONFIG_PATH="trash/$(date +"%Y-%m-%d-%Hh%M")/"
|
||||
mkdir -${MKDIR_FLAGS} ${OLD_CONFIG_PATH}
|
||||
mv -${MV_FLAGS} config/ ${OLD_CONFIG_PATH}
|
||||
echo " ✅ Your files have been moved to the ${OLD_CONFIG_PATH} directory. You can retrieve them there if needed."
|
||||
fi
|
||||
|
||||
# Script folders
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/config
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/logs
|
||||
mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/tmp
|
||||
[[ ${WEB_MODE} -eq 1 ]] && mkdir -${MKDIR_FLAGS} ${TMP_FILES_PATH}/web
|
||||
|
||||
# Secrets
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/var/lib/sops-nix/
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/disks
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/system
|
||||
if [[ "${DEVICE_TYPE}" == "server" || "${DEVICE_TYPE}" == "backup" ]]; then
|
||||
mkdir -${MKDIR_FLAGS} ${EXTRA_FILES_PATH}/etc/nixos/secrets/podman
|
||||
fi
|
||||
|
||||
echod "\n ✅ Folder hierarchy ready"
|
||||
}
|
||||
|
||||
hardware_detection() {
|
||||
local TMPFILE="/tmp/nixos-installation-hw-detection"
|
||||
|
||||
ssh_to_host 'bash -s' << SSHEND
|
||||
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}")
|
||||
else
|
||||
TARGET_GRAPHICS="false"
|
||||
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 [[ "\$TRANSPORT_PROTOCOL" == "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_TARGET_PASSWORD}" | 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
|
||||
|
||||
echo "# Hardware detection results on \$(date)" > "${TMPFILE}"
|
||||
for var in \
|
||||
TARGET_GRAPHICS \
|
||||
TARGET_GRAPHICS_RENDERER \
|
||||
TARGET_USB_CORAL \
|
||||
TARGET_PCIE_CORAL \
|
||||
TARGET_ZIGBEE_DEVICE \
|
||||
TARGET_INTERFACE \
|
||||
TARGET_TPM \
|
||||
TARGET_TPM_VERSION; do
|
||||
echo "export \${var}=\${!var}" >> "${TMPFILE}"
|
||||
done
|
||||
|
||||
for var in \
|
||||
TARGET_GRAPHICS_BRAND \
|
||||
DISK_DEVPATH \
|
||||
DISK_NAME \
|
||||
DISK_TYPE \
|
||||
DISK_HEALTH \
|
||||
DISK_ID \
|
||||
DISK_SIZE; do
|
||||
declare -p \${var} | sed 's/^declare /declare -g /' >> "${TMPFILE}"
|
||||
done
|
||||
SSHEND
|
||||
|
||||
scp -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}":"${TMPFILE}" "${TMPFILE}" &> /dev/null
|
||||
source "${TMPFILE}"
|
||||
|
||||
local DISK_FLAT_ARRAY=()
|
||||
for i in "${!DISK_NAME[@]}"; do
|
||||
DISK_FLAT_ARRAY+=("${DISK_NAME[$i]}" "${DISK_DEVPATH[$i]}" "${DISK_TYPE[$i]}" "${DISK_HEALTH[$i]}" "${DISK_ID[$i]}" "${DISK_SIZE[$i]}")
|
||||
done
|
||||
|
||||
jq -n \
|
||||
--argjson graphics_enabled "${TARGET_GRAPHICS:-false}" \
|
||||
--argjson graphics_renderer "${TARGET_GRAPHICS_RENDERER:-false}" \
|
||||
--argjson tpu_usb "${TARGET_USB_CORAL:-false}" \
|
||||
--argjson tpu_pcie "${TARGET_PCIE_CORAL:-false}" \
|
||||
--argjson tpm_enabled "${TARGET_TPM:-false}" \
|
||||
--arg tpm_version "${TARGET_TPM_VERSION:-N/A}" \
|
||||
--arg zigbee_device "${TARGET_ZIGBEE_DEVICE:-}" \
|
||||
--arg interface "${TARGET_INTERFACE:-}" \
|
||||
--argjson brands "$(jq -n '$ARGS.positional' --args ${TARGET_GRAPHICS_BRAND[@]:-})" \
|
||||
'
|
||||
{
|
||||
graphics: { enabled: $graphics_enabled, brands: $brands, renderer: $graphics_renderer },
|
||||
tpu: { usb: $tpu_usb, pcie: $tpu_pcie },
|
||||
tpm: { enabled: $tpm_enabled, version: $tpm_version },
|
||||
zigbee: { device: $zigbee_device },
|
||||
network: { interface: $interface },
|
||||
disks: [
|
||||
$ARGS.positional | range(0; length; 6) as $i | {
|
||||
name: .[$i], path: .[$i+1], type: .[$i+2], health: .[$i+3], id: .[$i+4], size: .[$i+5]
|
||||
}
|
||||
]
|
||||
}' --args "${DISK_FLAT_ARRAY[@]:-}" > ${HARDWARE_DATA_PATH}
|
||||
|
||||
if ssh_to_host "sudo nixos-generate-config --no-filesystems --show-hardware-config" > ${EXTRA_FILES_PATH}/etc/nixos/hardware-configuration.nix; then
|
||||
echo -e "\n✅ Hardware configuration generated"
|
||||
else
|
||||
echo -e "\n❌ Failed to generate hardware configuration"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
# --- GLOBAL FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN WEB FUNCTIONS --->
|
||||
launch_gui() {
|
||||
echo -e "\n ➡️ You will now proceed to the configuration of your device through your browser"
|
||||
echo -e "\n 🚀 Launching Numbus Configurator..."
|
||||
|
||||
python3 "${BRIDGE_SCRIPT}" > /dev/null 2>&1 &
|
||||
export BRIDGE_PID=$!
|
||||
xdg-open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || open "http://localhost:${WEBSERVER_PORT}" 2>/dev/null || true
|
||||
|
||||
sleep 5
|
||||
|
||||
echo -e "\n ➡️ If it doesn't automatically, open your browser at: $(gum style --foreground 212 "http://localhost:${WEBSERVER_PORT}")"
|
||||
|
||||
}
|
||||
# --- MAIN WEB FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN TUI FUNCTIONS --->
|
||||
preparation() {
|
||||
echo -e "\n ➡️ This script will now guide you through the configuration and gather the necessary information."
|
||||
|
||||
echo ""
|
||||
RAW_DEVICE_TYPE=$(gum choose --header "Choose the device you want to deploy :" \
|
||||
"Numbus Server : Professional-grade hosting, strictly kept under your roof." \
|
||||
"Numbus Backup Server : Automated, high-efficiency protection for your entire ecosystem." \
|
||||
"Numbus Computer : A modern, privacy-respecting machine built for work, creation, and play — without the corporate bloat." \
|
||||
"Numbus TV : A premium cinematic experience free from trackers and forced subscriptions." \
|
||||
"Numbus Game Console : An unbreakable Steam bigscreen experience.")
|
||||
|
||||
case "${RAW_DEVICE_TYPE}" in
|
||||
"Numbus Server : "* ) DEVICE_TYPE="server" ;;
|
||||
"Numbus Backup Server : "* ) DEVICE_TYPE="backup" ;;
|
||||
"Numbus Computer : "* ) DEVICE_TYPE="computer" ;;
|
||||
"Numbus TV : "* ) DEVICE_TYPE="tv" ;;
|
||||
"Numbus Game Console : "* ) DEVICE_TYPE="console" ;;
|
||||
esac
|
||||
|
||||
RAW_DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \
|
||||
"Interactive : You don't already have a configuration." \
|
||||
"Non-interactive : You have a valid configuration hosted on a Git platform.")
|
||||
|
||||
case "${RAW_DEPLOYMENT_MODE}" in
|
||||
"Interactive : "* ) DEPLOYMENT_MODE="interactive" ;;
|
||||
"Non-interactive : "* ) DEPLOYMENT_MODE="non-interactive" ;;
|
||||
esac
|
||||
|
||||
if [[ "${DEPLOYMENT_MODE}" == "non-interactive" ]]; then
|
||||
git_url() {
|
||||
IMPORTED_CONFIG_URL=$(gum input --placeholder "https://yourgitplatform.tld/your-user/repo-containing-the-configuration" --header "Please provide the URL to the git repository containing your configuration :")
|
||||
}
|
||||
|
||||
git_url
|
||||
|
||||
until git clone "${IMPORTED_CONFIG_URL}" imported_configuration; do
|
||||
echo -e "\n ⚠️ This did not work correctly."
|
||||
|
||||
echo -e "\n Is this URL correct [y/n] ? ${IMPORTED_CONFIG_URL}"
|
||||
read URL
|
||||
|
||||
if [[ "${URL^^}" == "N" ]]; then
|
||||
git_url
|
||||
fi
|
||||
|
||||
echo -e "\n You will be prompted for your credentials again. Make sure that they are correct."
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
gum format -- \
|
||||
"➡️ To continue, you need to start the target device in a NixOS live environment :
|
||||
1. Download the NixOS iso from the **[official website](https://nixos.org/download/)**.
|
||||
2. Flash it to a USB stick. (use a flashing tool like **[Rufus](https://rufus.ie/en/#download)**, **[BalenaEtcher](https://etcher.balena.io/#download-etcher)**, **[Impression](https://flathub.org/en/apps/io.gitlab.adhami3310.Impression)**, ...)
|
||||
3. Make sure your computer allows booting from USB drives and is in UEFI mode.
|
||||
4. Boot into the NixOS live environment.
|
||||
5. Launch a terminal. Set a password using \`passwd\` and find the IP address using \`ip a\`"
|
||||
|
||||
echo ""
|
||||
gum confirm "Is the device ready ?" || { echo "❌ You need to prepare the device. The script cannot continue."; exit 1; }
|
||||
|
||||
# LIVE TARGET SETTINGS
|
||||
user_input "LIVE_TARGET_IP" " Please provide the IP address of the target host :" "For example : 192.168.1.100" "${IP_REGEX}"
|
||||
user_input "LIVE_TARGET_PASSWORD" " Please enter the password for '${TARGET_USER}@${LIVE_TARGET_IP}' :" "${LIVE_TARGET_IP}'s password" "" "" "true"
|
||||
|
||||
# INTERNATIONALIZATION SETTINGS
|
||||
user_input "INTERNATIONALIZATION_TIMEZONE" " Please provide the wanted timezone :" "For example : Europe/Paris, Europe/Berlin, Europe/London, etc"
|
||||
user_input "INTERNATIONALIZATION_LANGUAGE" " Please provide the wanted language :" "For example : French, Deutsch, English, etc"
|
||||
user_input "INTERNATIONALIZATION_COUNTRY" " Please provide your country :" "For example : France, Germany, Great-Britain, etc"
|
||||
}
|
||||
|
||||
configuration() {
|
||||
if [[ "${DEVICE_TYPE}" == "server" ]]; then
|
||||
|
||||
# Users & Groups
|
||||
user_input "SERVER_OWNER_NAME" " Please provide the name of the owner of this server :" "For example : Steve"
|
||||
user_input "SERVER_ADMIN_EMAIL" " Please provide a valid ADMIN email address (ACME, system failures notifications, etc) :" "For example : myemail@mydomain.mytld" "${EMAIL_REGEX}"
|
||||
user_input "AUTHORIZED_SSH_PUBLIC_KEY" " Please provide the SSH public key of an authorized device (or a comma-separated list) :" "For example : ssh-ed25519 AAAAC3Nzam0uYewNAbxL8Fci8 user@your-pc or ssh-* * *, ssh-* * *, etc" "${SSH_KEY_REGEX}" "Invalid SSH key format (must start with ssh-...)."
|
||||
|
||||
echo -e "\n\n ➡️ You will access your services via a domain name (e.g. cloud.mydomain.com) and containers need credentials to create those subdomains"
|
||||
# TRAEFIK SETTINGS
|
||||
user_input "DOMAIN_NAME" " Please provide the domain name (FQDN) your home server will use :" "For example : yourdomain.com" "${DOMAIN_REGEX}"
|
||||
user_input "CLOUDFLARE_DNS_API_TOKEN" " Please provide a cloudflare API token with DNS zone permission :" "For example : bA7hdvCOuXGytlNKohi3ZGtlVpf5CHpLuCMiJrE" "" "" "true"
|
||||
|
||||
echo -e "\n\n ➡️ Some services will be able to send you emails. For that you need an email that supports sending emails (like Gmail for example)"
|
||||
# SMTP SETTINGS
|
||||
user_input "SMTP_SERVER_USERNAME" " Please provide a valid sender email address :" "For example : myemail@gmail.com" "${EMAIL_REGEX}"
|
||||
user_input "SMTP_SERVER_PASSWORD" " Please provide the password of this email address :" "abcd efgh ijkl mnop" "" "" "true"
|
||||
user_input "SMTP_SERVER_HOST" " Please provide the SMTP server endpoint :" "For Gmail : smtp.gmail.com" "${DOMAIN_REGEX}" "Invalid domain name format."
|
||||
user_input "SMTP_SERVER_PORT" " Please provide the smtp TLS port :" "For Gmail : 587" "${PORT_REGEX}" "Invalid port number."
|
||||
|
||||
echo -e "\n\n ➡️ This server will connect to your local network and you will configure its IP address\n"
|
||||
# NETWORK SETTINGS
|
||||
user_input "NETWORK_SUBNET" " Please provide your network subnet :" "For example 192.168.1.0/24" "${SUBNET_REGEX}" "Invalid subnet format (e.g. 192.168.1.1/24)."
|
||||
user_input "NETWORK_ROUTER_IP" " Please provide the ip address of your router :" "Most likely 192.168.1.1 or 192.168.1.254" "${IP_REGEX}" "Invalid IP address format."
|
||||
user_input "HOME_SERVER_IP" " 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.) :" "For example 192.168.1.5" "${IP_REGEX}" "Invalid IP address format."
|
||||
elif [[ "${DEVICE_TYPE}" == "backup" ]]; then
|
||||
:
|
||||
elif [[ "${DEVICE_TYPE}" == "computer" ]]; then
|
||||
:
|
||||
elif [[ "${DEVICE_TYPE}" == "tv" ]]; then
|
||||
:
|
||||
fi
|
||||
}
|
||||
|
||||
setup_ssh() {
|
||||
echod "\n ✅ Generating new SSH key for numbus-admin..."
|
||||
|
||||
chmod 700 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/
|
||||
ssh-keygen -t "ed25519" -C "numbus-admin@numbus-server" -f "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" -N "" -q
|
||||
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ➡️ Copying SSH key to target host '${TARGET_USER}@${LIVE_TARGET_IP}'..."
|
||||
fi
|
||||
|
||||
if sshpass -p "${LIVE_TARGET_PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i "${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519" "${TARGET_USER}@${LIVE_TARGET_IP}"; then
|
||||
if [[ ${DEBUG} -eq 1 ]]; then
|
||||
echo -e "\n ✅ SSH key copied successfully"
|
||||
fi
|
||||
else
|
||||
echo -e "\n ❌ Failed to copy SSH key. Please check the host IP and password."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
if gum confirm "Change the subdomain of ${service} ?"; then
|
||||
SELECTED_WEB_APPLICATIONS_SUBDOMAIN+=( "$(gum input --placeholder "${service}" --header "Please provide the desired subdomain for ${service}:")" )
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
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() {
|
||||
echod "\n 📝 Generating structured settings.json..."
|
||||
|
||||
# Create a temporary JSON file with all the collected variables
|
||||
# This file will be read by the Nix configuration using builtins.fromJSON
|
||||
jq -n \
|
||||
--arg tz "$INTERNATIONALIZATION_TIMEZONE" \
|
||||
--arg lang "$INTERNATIONALIZATION_LANGUAGE" \
|
||||
--arg owner "$SERVER_OWNER_NAME" \
|
||||
--arg ip "$HOME_SERVER_IP" \
|
||||
--arg iface "$TARGET_INTERFACE" \
|
||||
--arg router "$NETWORK_ROUTER_IP" \
|
||||
--arg domain "$DOMAIN_NAME" \
|
||||
--argjson cockpit_enabled "true" \
|
||||
--arg dns "${SELECTED_DNS_SERVICE[0]}" \
|
||||
--argjson apps "$(printf '%s\n' "${SELECTED_WEB_APPLICATIONS[@]}" | jq -R . | jq -s .)" \
|
||||
'{
|
||||
system: {
|
||||
timeZone: $tz,
|
||||
language: $lang,
|
||||
owner: $owner
|
||||
},
|
||||
network: {
|
||||
ipAddress: $ip,
|
||||
interface: $iface,
|
||||
routerIp: $router
|
||||
},
|
||||
services: {
|
||||
domain: $domain,
|
||||
dnsProvider: $dns,
|
||||
enabledApps: $apps,
|
||||
managementConsole: $cockpit_enabled
|
||||
}
|
||||
}' > "${EXTRA_FILES_PATH}/etc/nixos/settings.json"
|
||||
|
||||
echo -e "{\n numbus.settings = builtins.fromJSON (builtins.readFile ./settings.json);\n}" > "${CONFIGURATION_PATH}"
|
||||
|
||||
# Ensure the settings file is writable by the management service
|
||||
# and that the directory is prepared for local git tracking
|
||||
chmod 664 "${EXTRA_FILES_PATH}/etc/nixos/settings.json"
|
||||
}
|
||||
|
||||
# The existing network_config_generation and services_config_generation functions
|
||||
# are now redundant as the logic is centralized in the JSON export.
|
||||
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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/boot-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S mkdir -p /etc/secrets/disks/
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/boot-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/content-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/content-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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" > "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
chmod 600 "${EXTRA_FILES_PATH}/etc/secrets/disks/parity-${i}"
|
||||
ssh_to_host 'bash -s' << EOF
|
||||
echo "$LIVE_TARGET_PASSWORD" | sudo -S bash -c "printf '%s' '$PASS' > /etc/secrets/disks/parity-${i}"
|
||||
echo "$LIVE_TARGET_PASSWORD" | 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 ${EXTRA_FILES_PATH}/home/numbus-admin/.ssh/id_ed25519 > ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt
|
||||
export SOPS_PUBLIC_KEY=$(age-keygen -y ${EXTRA_FILES_PATH}/var/lib/sops-nix/key.txt)
|
||||
|
||||
echo -e "\n ✅ Generating sops-nix configuration files..."
|
||||
envsubst < templates/nix-config/sops-nix/.sops.yaml > ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/etc/secrets/disks/boot-1 )\`
|
||||
$( [[ -n "${BOOT_DISKS_ID_LIST[1]:-}" ]] && echo "* **Disk 2 secret key :** \`$( cat ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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 ${EXTRA_FILES_PATH}/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
|
||||
}
|
||||
|
||||
deploy() {
|
||||
git -C . add -f "${EXTRA_FILES_PATH}/"
|
||||
git -C . add -f "templates/"
|
||||
git -C . add -f "deploy.conf"
|
||||
|
||||
# Initialize a git repo in the configuration to be deployed
|
||||
# This allows the Management UI on the appliance to commit changes
|
||||
# and provide a local history/rollback UI to the user.
|
||||
if [ ! -d "${EXTRA_FILES_PATH}/etc/nixos/.git" ]; then
|
||||
git -C "${EXTRA_FILES_PATH}/etc/nixos" init -q
|
||||
git -C "${EXTRA_FILES_PATH}/etc/nixos" add .
|
||||
git -C "${EXTRA_FILES_PATH}/etc/nixos" commit -m "Initial bootstrap via Numbus Deploy" -q
|
||||
fi
|
||||
|
||||
echo -e "\n\n🔄 Deploying to the remote server..."
|
||||
nix flake update --flake ./${EXTRA_FILES_PATH}/etc/nixos
|
||||
nix run github:nix-community/nixos-anywhere -- \
|
||||
--flake ${EXTRA_FILES_PATH}/etc/nixos#numbus-server \
|
||||
--extra-files ${EXTRA_FILES_PATH} \
|
||||
--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_PASSWORD="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_PASSWORD}" ssh -i "${EXTRA_FILES_PATH}/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_PASSWORD} | 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_PASSWORD} | sudo -S systemd-analyze pcrs 15 --json=short)
|
||||
|
||||
echo ${LIVE_TARGET_PASSWORD} | 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_PASSWORD | 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 ${EXTRA_FILES_PATH}/etc/nixos#numbus-server
|
||||
}
|
||||
# --- MAIN FUNCTIONS ---<
|
||||
|
||||
|
||||
|
||||
# --- DEFAULT VARIABLES --->
|
||||
WEBSERVER_PORT=${WEBSERVER_PORT:-8088}
|
||||
|
||||
LIVE_DATA_PATH="/run/user/$(id -u)/numbus/web/live_settings.json"
|
||||
HARDWARE_DATA_PATH="/run/user/$(id -u)/numbus/web/hardware.json"
|
||||
|
||||
CONFIG_FILE="../config/numbus.yaml"
|
||||
|
||||
TARGET_USER="nixos"
|
||||
|
||||
TMP_FILES_PATH="/run/user/$(id -u)/numbus-$(date +"%Y-%m-%d-%Hh%M")"
|
||||
EXTRA_FILES_PATH="${TMP_FILES_PATH}/config"
|
||||
|
||||
if [[ ${DEBUG-0} -eq 1 ]]; then
|
||||
FILES_CP_FLAGS="vau"
|
||||
FILES_RM_FLAGS="vf"
|
||||
DIR_RM_FLAGS="rvf"
|
||||
MKDIR_FLAGS="pv"
|
||||
MV_FLAGS="vu"
|
||||
else
|
||||
DEBUG=0
|
||||
FILES_CP_FLAGS="au"
|
||||
FILES_RM_FLAGS="f"
|
||||
DIR_RM_FLAGS="rf"
|
||||
MKDIR_FLAGS="p"
|
||||
MV_FLAGS="u"
|
||||
fi
|
||||
|
||||
IP_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
|
||||
SUBNET_REGEX='^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
|
||||
DOMAIN_REGEX='^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||
EMAIL_REGEX='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
PORT_REGEX='^[0-9]{1,5}$'
|
||||
SSH_KEY_REGEX='^ssh-[a-z0-9]+ [A-Za-z0-9+/]+.*'
|
||||
PHONE_REGEX='^\+[1-9][0-9]{7,14}$'
|
||||
|
||||
GUM_INPUT_PADDING="1 1"
|
||||
GUM_INPUT_HEADER_FOREGROUND="212"
|
||||
GUM_INPUT_CURSOR_FOREGROUND="212"
|
||||
GUM_INPUT_TIMEOUT="3600"
|
||||
# --- DEFAULTS VARIABLES ---<
|
||||
|
||||
|
||||
|
||||
# --- PRE MAIN LOGIC --->
|
||||
set -euo pipefail
|
||||
clear
|
||||
|
||||
trap cleanup EXIT
|
||||
compatibility_check
|
||||
# --- PRE MAIN LOGIC ---<
|
||||
|
||||
|
||||
|
||||
# --- MAIN LOGIC --->
|
||||
echo """
|
||||
_ ____ ____ ______ __ ______
|
||||
/ |/ / / / / |/ / _ )/ / / / __/
|
||||
/ / /_/ / /|_/ / _ / /_/ /\ \
|
||||
/_/|_/\____/_/ /_/____/\____/___/
|
||||
"""
|
||||
|
||||
DEPLOYMENT_STRATEGY=$(gum choose --header "Choose your preferred deployment strategy :" \
|
||||
"I don't have a configuration" \
|
||||
"I have a valid configuration hosted on a Git platform")
|
||||
|
||||
if [[ "${DEPLOYMENT_STRATEGY}" == "I don't have a configuration" ]]; then
|
||||
BRIDGE_SCRIPT="../web/logic/interactive.py"
|
||||
launch_gui
|
||||
else
|
||||
DEPLOYMENT_MODE=$(gum choose --header "Choose your preferred deployment mode :" \
|
||||
"Through my web browser (Recommended for beginners)" \
|
||||
"Through my terminal (TUI)")
|
||||
if [[ "${DEPLOYMENT_MODE}" == "Through my web browser (Recommended for beginners)" ]]; then
|
||||
BRIDGE_SCRIPT="../web/logic/non-interactive.py"
|
||||
launch_gui
|
||||
else
|
||||
launch_tui
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,6 @@
|
||||
# Terminology for the variables used
|
||||
|
||||
|Variable|Meaning|Possible values|
|
||||
|-|-------|-|
|
||||
|DEPLOYMENT_STRATEGY|Either deploy the machine with a config you already have or let the script guide you through the config options|**interactive** or **non-interactive**|
|
||||
|DEPLOYMENT_MODE|Either configure the machine through your terminal (TUI) or through a slick web UI (GUI)|**TUI** (only available for **non-interactive** strategy) or **GUI**|
|
||||
@@ -17,8 +17,4 @@
|
||||
# Secrets
|
||||
sops.secrets."authorizedSshPublicKeys" = { owner = "numbus-admin"; path = "/home/numbus-admin/.ssh/authorized_keys"; mode = "0600"; };
|
||||
sops.secrets."smtpPassword" = { owner = "numbus-admin"; mode = "0600"; };
|
||||
sops.secrets."cloudflareDnsApiToken" = { owner = "numbus-admin"; mode = "0600"; };
|
||||
|
||||
# # TPM2 PCR check
|
||||
# systemIdentity.enable = true;
|
||||
# systemIdentity.pcr15 = "PCR_HASH";
|
||||
sops.secrets."cloudflareDnsApiToken" = { owner = "numbus-admin"; mode = "0600"; };
|
||||
@@ -3,7 +3,7 @@
|
||||
# Core Nixpkgs
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
# Numbus server configuration
|
||||
numbus.url = "git+https://gittea.dev/numbus/numbus-server-module";
|
||||
numbus.url = "git+https://gittea.dev/numbus/numbus-backup-server";
|
||||
numbus.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Disk-partitioning helper
|
||||
disko.url = "github:nix-community/disko";
|
||||
@@ -0,0 +1,18 @@
|
||||
{ modulesPath, config, pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/installer/scan/not-detected.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
];
|
||||
|
||||
# System
|
||||
system.stateVersion = "25.11";
|
||||
|
||||
# Secrets management
|
||||
sops.defaultSopsFile = ./secrets/secrets.yaml;
|
||||
sops.age.sshKeyPaths = [ "/home/numbus-admin/.ssh/id_ed25519" ];
|
||||
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||
# Secrets
|
||||
sops.secrets."authorizedSshPublicKeys" = { owner = "numbus-admin"; path = "/home/numbus-admin/.ssh/authorized_keys"; mode = "0600"; };
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
inputs = {
|
||||
# Core Nixpkgs
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
# Numbus server configuration
|
||||
numbus.url = "git+https://gittea.dev/numbus/numbus-computer";
|
||||
numbus.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Disk-partitioning helper
|
||||
disko.url = "github:nix-community/disko";
|
||||
disko.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Secrets handling
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Power savings
|
||||
autoaspm.url = "git+https://git.notthebe.ee/notthebee/AutoASPM";
|
||||
autoaspm.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, numbus, disko, sops-nix, autoaspm, ... }@inputs: let
|
||||
# System definition
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
numbus-server = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
# Numbus server configuration
|
||||
numbus.nixosModules.numbus
|
||||
# Disk-partitioning helper
|
||||
disko.nixosModules.disko
|
||||
# Secrets handling
|
||||
sops-nix.nixosModules.sops
|
||||
# Power savings
|
||||
autoaspm.nixosModules.autoaspm
|
||||
# Core host configuration
|
||||
./configuration.nix
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# .sops.yaml
|
||||
|
||||
keys:
|
||||
- &primary $SOPS_PUBLIC_KEY
|
||||
creation_rules:
|
||||
- path_regex: secrets/secrets.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *primary
|
||||
@@ -0,0 +1,2 @@
|
||||
authorizedSshPublicKeys: |
|
||||
$SSH_KEYS_FORMATTED
|
||||
@@ -0,0 +1,9 @@
|
||||
# .sops.yaml
|
||||
|
||||
keys:
|
||||
- &primary $SOPS_PUBLIC_KEY
|
||||
creation_rules:
|
||||
- path_regex: secrets/(disks|podman|system)/.*\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *primary
|
||||
@@ -0,0 +1,8 @@
|
||||
# This file is reserved for ADVANCED USERS ONLY.
|
||||
# Editing could compromise system stability and is not supported by numbus.
|
||||
# Do NOT set options already managed by numbus. i.e. config.numbus.* and other options (networking, storage, etc.)
|
||||
# Please use the dedicated script for those options : https://gittea.dev/numbus/numbus.
|
||||
|
||||
{ config }:
|
||||
|
||||
{}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Do NOT edit this file manually.
|
||||
# Please use the dedicated script : https://gittea.dev/numbus/numbus.
|
||||
# This could compromise system stability and is not supported by numbus.
|
||||
|
||||
{
|
||||
inputs = {
|
||||
# Core Nixpkgs
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
# Numbus server configuration
|
||||
numbus-server.url = "git+https://git.numbus.eu/numbus/numbus-server";
|
||||
numbus-server.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Disk-partitioning helper
|
||||
disko.url = "github:nix-community/disko";
|
||||
disko.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Secrets handling
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Power savings
|
||||
autoaspm.url = "git+https://git.notthebe.ee/notthebee/AutoASPM";
|
||||
autoaspm.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, numbus-server, disko, sops-nix, autoaspm, ... }@inputs: let
|
||||
# System definition
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
numbus-server = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
# Numbus server configuration
|
||||
numbus-server.nixosModules.numbus-server
|
||||
# Disk-partitioning helper
|
||||
disko.nixosModules.disko
|
||||
# Secrets handling
|
||||
sops-nix.nixosModules.sops
|
||||
# Power savings
|
||||
autoaspm.nixosModules.autoaspm
|
||||
# Core host configuration
|
||||
./hardware-configuration.nix
|
||||
./numbus-generated.nix
|
||||
./custom-configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Do NOT edit this file manually.
|
||||
# Please use the dedicated script : https://gittea.dev/numbus/numbus.
|
||||
# This could compromise system stability and is not supported by numbus.
|
||||
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
@@ -0,0 +1,2 @@
|
||||
disks:
|
||||
boot: "whoever blabber senior liqueur riverboat filing wronged seventeen grandma moonshine"
|
||||
@@ -0,0 +1,3 @@
|
||||
disks:
|
||||
content-1: "nemeses starship tackle anime cardinal scarcity marmalade divorcee gondola kindle"
|
||||
content-2: "chihuahua smolder cardiac mooned ovary suing bless nuptials driver slighting"
|
||||
@@ -0,0 +1,2 @@
|
||||
disks:
|
||||
parity-1: "armed despise atrophy province strategy lustrous provoking jurist ramble phoenix"
|
||||
@@ -0,0 +1,3 @@
|
||||
# CLOUDFLARE
|
||||
traefik:
|
||||
cloudflareDnsApiToken: ""
|
||||
@@ -0,0 +1,3 @@
|
||||
# SMTP
|
||||
mail:
|
||||
smtpPassword: "$SMTP_SERVER_PASSWORD"
|
||||
@@ -0,0 +1,3 @@
|
||||
# SSH
|
||||
ssh:
|
||||
authorizedPublicKeys: |
|
||||
@@ -0,0 +1,18 @@
|
||||
{ modulesPath, config, pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/installer/scan/not-detected.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
];
|
||||
|
||||
# System
|
||||
system.stateVersion = "25.11";
|
||||
|
||||
# Secrets management
|
||||
sops.defaultSopsFile = ./secrets/secrets.yaml;
|
||||
sops.age.sshKeyPaths = [ "/home/numbus-admin/.ssh/id_ed25519" ];
|
||||
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||
# Secrets
|
||||
sops.secrets."authorizedSshPublicKeys" = { owner = "numbus-admin"; path = "/home/numbus-admin/.ssh/authorized_keys"; mode = "0600"; };
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
inputs = {
|
||||
# Core Nixpkgs
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
# Numbus server configuration
|
||||
numbus.url = "git+https://gittea.dev/numbus/numbus-tv";
|
||||
numbus.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Disk-partitioning helper
|
||||
disko.url = "github:nix-community/disko";
|
||||
disko.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Secrets handling
|
||||
sops-nix.url = "github:Mic92/sops-nix";
|
||||
sops-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
# Power savings
|
||||
autoaspm.url = "git+https://git.notthebe.ee/notthebee/AutoASPM";
|
||||
autoaspm.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, numbus, disko, sops-nix, autoaspm, ... }@inputs: let
|
||||
# System definition
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
numbus-server = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
# Numbus server configuration
|
||||
numbus.nixosModules.numbus
|
||||
# Disk-partitioning helper
|
||||
disko.nixosModules.disko
|
||||
# Secrets handling
|
||||
sops-nix.nixosModules.sops
|
||||
# Power savings
|
||||
autoaspm.nixosModules.autoaspm
|
||||
# Core host configuration
|
||||
./configuration.nix
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# .sops.yaml
|
||||
|
||||
keys:
|
||||
- &primary $SOPS_PUBLIC_KEY
|
||||
creation_rules:
|
||||
- path_regex: secrets/secrets.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *primary
|
||||
@@ -0,0 +1,2 @@
|
||||
authorizedSshPublicKeys: |
|
||||
$SSH_KEYS_FORMATTED
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Numbus Configurator</title>
|
||||
<!-- Tailwind CSS for modern styling -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alpine.js for lightweight reactivity -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<!-- JS-YAML to convert JS objects to YAML strings -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
|
||||
<!-- Material Design Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
<link rel="icon" href="./media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<style>
|
||||
@keyframes pulse-slow {
|
||||
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 20px rgba(14, 165, 233, 0.2)); }
|
||||
50% { transform: scale(1.03); filter: drop-shadow(0 0 40px rgba(192, 38, 211, 0.4)); }
|
||||
}
|
||||
.animate-pulse-slow { animation: pulse-slow 6s infinite ease-in-out; }
|
||||
.text-shadow-glow { text-shadow: 0 0 15px rgba(56, 189, 248, 0.4); }
|
||||
</style>
|
||||
|
||||
<body class="bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<!-- Welcome screen -->
|
||||
<div class="p-12 justify-center flex flex-col items-center">
|
||||
<img class="w-64 h-64 auto drop-shadow-2xl animate-pulse-slow" src="./media/logo.png" alt="Numbus Logo">
|
||||
<h1 class="p-8 text-center text-6xl font-extrabold tracking-tight">Welcome to <span class="text-sky-400 animate-pulse-slow text-shadow-glow">NUMBUS</span></h1>
|
||||
<p class="text-center text-2xl p-2">Transform your device into a <strong>secure, reliable and private</strong> appliance <br> using the power of open-soure software.</p>
|
||||
<p class="text-center text-2xl p-2 pb-10">You will be <strong>guided</strong> through the configuration process.</p>
|
||||
|
||||
<div class="bg-amber-500/20 border border-amber-500/30 p-4 rounded-xl flex gap-5 text-left items-center max-w-3xl mx-auto">
|
||||
<span class="bg-amber-500 rounded-full p-1 px-2 shrink-0">
|
||||
<i class="mdi mdi-shield-lock text-slate-900 text-2xl"></i>
|
||||
</span>
|
||||
<p class="text-xl text-amber-200/90 italic"><strong>Privacy First:</strong> No data entered here ever leaves your device. <br> This configurator runs entirely locally in your browser and is fully private.
|
||||
</div>
|
||||
|
||||
<span class="animate-pulse-slow drop-shadow-2xl p-12">
|
||||
<a class="text-shadow-glow px-10 py-4 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-full text-xl font-bold transition-all transform hover:scale-105 shadow-lg shadow-fuchsia-600/20" href="./pages/preparation.html">Get Started</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
### Variables -->
|
||||
SECRET_PATH = "/run/user/{}/numbus".format(os.getuid()) if os.path.exists("/run/user/{}".format(os.getuid())) else "../secrets"
|
||||
os.makedirs(SECRET_PATH, exist_ok=True)
|
||||
LOGS_DIR = "../web/logs/"
|
||||
PAGES_DIR = "../web/pages/"
|
||||
CONFIG_DIR = "../web/config/"
|
||||
SIGNALS_DIR = "../web/signal/"
|
||||
### <-- Variables
|
||||
|
||||
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# Route for logs: /logs?type=out or /logs?type=err
|
||||
if self.path.startswith('/logs'):
|
||||
log_type = "out" if "type=err" not in self.path else "err"
|
||||
log_path = os.path.join(LOGS_DIR, f'deploy-{log_type}.log')
|
||||
|
||||
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(log_path):
|
||||
with open(log_path, 'r') as f:
|
||||
# Read last 50 lines for better context during errors
|
||||
self.wfile.write("".join(f.readlines()[-50:]).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':
|
||||
# Store secrets in memory-backed filesystem
|
||||
with open(os.path.join(SECRET_PATH, "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(os.path.join(SIGNALS_DIR, ".discovery_ready"), "w") as f: f.write("1")
|
||||
|
||||
elif self.path == '/deploy':
|
||||
with open(os.path.join(CONFIG_DIR, "numbus.yaml"), "wb") as f:
|
||||
f.write(post_data)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
with open(os.path.join(SIGNALS_DIR, ".deploy_signal"), "w") as f: f.write("1")
|
||||
|
||||
os.chdir(PAGES_DIR)
|
||||
http.server.HTTPServer(('localhost', 8088), BridgeHandler).serve_forever()
|
||||
@@ -0,0 +1,50 @@
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Use a memory-backed path for temporary secrets if available, else local
|
||||
SECRET_PATH = "/run/user/{}/numbus".format(os.getuid()) if os.path.exists("/run/user/{}".format(os.getuid())) else "."
|
||||
os.makedirs(SECRET_PATH, exist_ok=True)
|
||||
|
||||
class BridgeHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# Route for logs: /logs?type=out or /logs?type=err
|
||||
if self.path.startswith('/logs'):
|
||||
log_type = "out" if "type=err" not in self.path else "err"
|
||||
log_file = f'deploy-{log_type}.log'
|
||||
|
||||
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(log_file):
|
||||
with open(log_file, 'r') as f:
|
||||
# Read last 50 lines for better context during errors
|
||||
self.wfile.write("".join(f.readlines()[-50:]).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':
|
||||
# Store secrets in memory-backed filesystem
|
||||
with open(os.path.join(SECRET_PATH, "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("../numbus.yaml", "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', 8088), BridgeHandler).serve_forever()
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-backup-server-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 382.85714,316.05715 a 77.142857,77.142857 0 0 0 -77.14285,77.14286 H 280 l 34.28572,34.28571 34.28571,-34.28571 h -25.71429 a 60,60 0 0 1 60,-60 60,60 0 0 1 60,60 60,60 0 0 1 -60,59.99998 c -12.85714,0 -24.94285,-4.28571 -34.8,-11.14285 L 335.71428,454.4 c 13.20001,9.94285 29.48572,15.94285 47.14286,15.94285 A 77.142857,77.142857 0 0 0 460,393.20001 77.142857,77.142857 0 0 0 382.85714,316.05715 M 400,393.20001 a 17.142857,17.142857 0 0 0 -17.14286,-17.14286 17.142857,17.142857 0 0 0 -17.14285,17.14286 17.142857,17.142857 0 0 0 17.14285,17.14285 A 17.142857,17.142857 0 0 0 400,393.20001 Z"
|
||||
id="path1-5"
|
||||
style="fill:#ffffff;stroke-width:8.57143" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-computer-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 310,348.2 h 120 v 75 H 310 m 120,15 a 15,15 0 0 0 15,-15 v -75 c 0,-8.325 -6.75,-15 -15,-15 H 310 c -8.325,0 -15,6.675 -15,15 v 75 a 15,15 0 0 0 15,15 h -30 v 15 h 180 v -15 z"
|
||||
id="path1-2"
|
||||
style="stroke-width:7.5;fill:#ffffff" /></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-server-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 377.25,436.7 h 7.25 a 7.25,7.25 0 0 1 7.25,7.25 h 50.75 v 14.5 h -50.75 a 7.25,7.25 0 0 1 -7.25,7.25 h -29 a 7.25,7.25 0 0 1 -7.25,-7.25 H 297.5 v -14.5 h 50.75 a 7.25,7.25 0 0 1 7.25,-7.25 h 7.25 V 422.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,378.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,422.2 h -50.75 v 14.5 M 312,320.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,364.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,320.7 m 36.25,29 h 7.25 v -14.5 h -7.25 v 14.5 m 0,58 h 7.25 v -14.5 h -7.25 v 14.5 m -29,-72.5 v 14.5 h 14.5 v -14.5 h -14.5 m 0,58 v 14.5 h 14.5 v -14.5 z"
|
||||
id="path1-6"
|
||||
style="fill:#ffffff;stroke-width:7.25" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-dark-template.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.2326894"
|
||||
inkscape:cx="261.21747"
|
||||
inkscape:cy="258.78377"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-tv-dark.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#000000"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:33.8362;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 318.5733,405.71938 v -38.9116 H 299.28665 280 V 354.28838 341.769 h 54.65141 54.65141 l 9.69089,29.09912 c 5.32999,16.00451 9.79503,29.09911 9.92233,29.09911 0.12729,0 4.59234,-13.0946 9.92232,-29.09911 L 428.52925,341.769 H 444.269 c 8.65686,0 15.73581,0.0761 15.731,0.16918 -0.005,0.0931 -8.63713,23.16087 -19.18293,51.26182 l -19.17419,51.09264 H 408.9696 396.29631 l -14.33924,-38.57326 -14.33925,-38.57324 -12.00286,-0.18479 -12.00285,-0.1848 v 38.92722 38.92723 h -12.5194 -12.51941 z"
|
||||
id="path2" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-backup-server-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 382.85714,316.05715 a 77.142857,77.142857 0 0 0 -77.14285,77.14286 H 280 l 34.28572,34.28571 34.28571,-34.28571 h -25.71429 a 60,60 0 0 1 60,-60 60,60 0 0 1 60,60 60,60 0 0 1 -60,59.99998 c -12.85714,0 -24.94285,-4.28571 -34.8,-11.14285 L 335.71428,454.4 c 13.20001,9.94285 29.48572,15.94285 47.14286,15.94285 A 77.142857,77.142857 0 0 0 460,393.20001 77.142857,77.142857 0 0 0 382.85714,316.05715 M 400,393.20001 a 17.142857,17.142857 0 0 0 -17.14286,-17.14286 17.142857,17.142857 0 0 0 -17.14285,17.14286 17.142857,17.142857 0 0 0 17.14285,17.14285 A 17.142857,17.142857 0 0 0 400,393.20001 Z"
|
||||
id="path1-5"
|
||||
style="fill:#000000;stroke-width:8.57143" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-computer-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 310,348.2 h 120 v 75 H 310 m 120,15 a 15,15 0 0 0 15,-15 v -75 c 0,-8.325 -6.75,-15 -15,-15 H 310 c -8.325,0 -15,6.675 -15,15 v 75 a 15,15 0 0 0 15,15 h -30 v 15 h 180 v -15 z"
|
||||
id="path1-2"
|
||||
style="stroke-width:7.5;fill:#000000" /></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-server-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
d="m 377.25,436.7 h 7.25 a 7.25,7.25 0 0 1 7.25,7.25 h 50.75 v 14.5 h -50.75 a 7.25,7.25 0 0 1 -7.25,7.25 h -29 a 7.25,7.25 0 0 1 -7.25,-7.25 H 297.5 v -14.5 h 50.75 a 7.25,7.25 0 0 1 7.25,-7.25 h 7.25 V 422.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,378.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,422.2 h -50.75 v 14.5 M 312,320.7 h 116 a 7.25,7.25 0 0 1 7.25,7.25 v 29 A 7.25,7.25 0 0 1 428,364.2 H 312 a 7.25,7.25 0 0 1 -7.25,-7.25 v -29 A 7.25,7.25 0 0 1 312,320.7 m 36.25,29 h 7.25 v -14.5 h -7.25 v 14.5 m 0,58 h 7.25 v -14.5 h -7.25 v 14.5 m -29,-72.5 v 14.5 h 14.5 v -14.5 h -14.5 m 0,58 v 14.5 h 14.5 v -14.5 z"
|
||||
id="path1-6"
|
||||
style="fill:#000000;stroke-width:7.25" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-dark-template.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="1.2326894"
|
||||
inkscape:cx="261.21747"
|
||||
inkscape:cy="258.78377"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="numbus-tv-light.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.96303857"
|
||||
inkscape:cx="255.44148"
|
||||
inkscape:cy="219.09818"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" /><path
|
||||
d="M201.1 166.7 76.5 397.9l-29.1-52.8 33.6-62-66.8-.2L0 256.5l14.6-27 95.1.3 34.2-63zm9.7 184.8h249.4L432 404.9l-66.9-.2 33.2 62-14.2 26.5H355L307.7 405l-68.1-.1zM356 250.2 231.3 19l57.3-.6 33.3 62.1 33.5-61.8h28.5l14.5 27-47.8 87.9 33.9 63.2zm-199.8 11.7 124.7 231.2-57.3.6-33.3-62.2-33.5 61.8h-28.5l-14.6-27 47.8-87.8-33.9-63.2zM301.1 160H51.6l28.2-53.4 66.9.2-33.2-62 14.2-26.5h29.1l47.3 88.2 68.1.1zm9.8 185.4 124.7-231.2 29.1 52.8-33.7 61.9 66.8.2 14.2 26.5-14.6 27-95.1-.3-34.2 63z"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;fill:#ffffff"
|
||||
id="path1" /><rect
|
||||
style="opacity:0.88;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:55.0675;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect1"
|
||||
width="184.93251"
|
||||
height="144.93251"
|
||||
x="277.53375"
|
||||
y="320.73373" /><path
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:33.8362;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 318.5733,405.71938 v -38.9116 H 299.28665 280 V 354.28838 341.769 h 54.65141 54.65141 l 9.69089,29.09912 c 5.32999,16.00451 9.79503,29.09911 9.92233,29.09911 0.12729,0 4.59234,-13.0946 9.92232,-29.09911 L 428.52925,341.769 H 444.269 c 8.65686,0 15.73581,0.0761 15.731,0.16918 -0.005,0.0931 -8.63713,23.16087 -19.18293,51.26182 l -19.17419,51.09264 H 408.9696 396.29631 l -14.33924,-38.57326 -14.33925,-38.57324 -12.00286,-0.18479 -12.00285,-0.1848 v 38.92722 38.92723 h -12.5194 -12.51941 z"
|
||||
id="path2" /></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Numbus Configurator</title>
|
||||
<!-- Tailwind CSS for modern styling -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alpine.js for lightweight reactivity -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<!-- JS-YAML to convert JS objects to YAML strings -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
|
||||
<!-- Material Design Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="../media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
|
||||
<body x-data="setupNavigation()" class="p-4 bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<script>
|
||||
function setupNavigation() {
|
||||
return {
|
||||
step: 1,
|
||||
goToPrevStep() {
|
||||
this.step--;
|
||||
},
|
||||
goToNextStep() {
|
||||
this.step++;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-[#1e293b] border border-slate-700 rounded-2xl relative">
|
||||
<div class="mx-auto relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center">
|
||||
<a href="https://numbus.eu"><img class="w-auto h-10 pr-5 pl-8" src="../media/logo.png" aria-label="The numbus logo"></a>
|
||||
<a class="font-bold text-2xl tracking-tight bg-gradient-to-r from-sky-400 to-fuchsia-500 bg-clip-text text-transparent uppercase" href="https://numbus.eu">NUMBUS</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<h1 class="sm:text-2xl sm:pr-20 lg:pr-0 text-xl text-white font-bold flex items-center">Step 1 - Preparation</h1>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center my-auto">
|
||||
<button class="h-auto mdi mdi-menu text-slate-100 text-2xl sm:hidden pr-8" aria-label="Menu with links"></button>
|
||||
<button class="h-auto mdi mdi-brightness-2 text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="Change theme"></button>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-source-repository text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="See the source code on Gitea"></a>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-text-box-search text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-8" aria-label="See the full documentation"></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="pt-10 w-full"></div>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="bg-[#1e293b] border border-slate-700 rounded-3xl h-[calc(100vh-10rem)] max-w-[60vw] mx-auto relative">
|
||||
<!-- Device Type -->
|
||||
<div x-show="step === 1">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Device Type</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select the <strong>device type</strong> for your new Numbus machine that matches <strong>your needs</strong>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<button @click="" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-server-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Server</h1>
|
||||
<p class="text-sm transition-colors">Your own Cloud at Home.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-backup-server-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Backup Server</h1>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-computer-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus Computer</h1>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-tv-light.svg" alt="Numbus Server icon">
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Numbus TV</h1>
|
||||
<p class="text-sm transition-colors">Your TV, your way. No spying on you.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Mode -->
|
||||
<div x-show="step === 2">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Deployment Mode</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <strong>preferred</strong> deployment mode. Non-interactive <strong>requires</strong> a ready-to-go configuration hosted on a <strong>git platform</strong>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 grid grid-cols-2 gap-4">
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<i class="mdi mdi-gesture-tap text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Interactive</h1>
|
||||
<p class="text-sm transition-colors">We will guide you through the setup process.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left bg-slate-900 border-slate-700 hover:bg-fuchsia-600/20 hover:border-fuchsia-500 focus:bg-fuchsia-600/20 focus:border-fuchsia-500">
|
||||
<i class="mdi mdi-cog-clockwise text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<h1 class="font-bold text-2xl mb-1">Non-interactive</h1>
|
||||
<p class="text-sm transition-colors">You already have a ready-to-go configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Setup -->
|
||||
<div x-show="step === 3">
|
||||
<h1 class="p-5 pt-6 text-4xl font-bold text-sky-400">Live Setup</h1>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[98%] mx-auto"></div>
|
||||
<p class="p-5 pb-10 text-xl text-slate-300">Provide the <strong>necessary information</strong> to connect to the device. It needs to be in a <strong>NixOS live environment</strong>.</p>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="p-6 space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Live Target IP Address</label>
|
||||
<input class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40" type="text" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="p-6 space-y-2">
|
||||
<label class="text-sm font-bold text-slate-400 uppercase tracking-widest">Temporary Password</label>
|
||||
<input class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40" type="password" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="bg-[#1e293b] border-t bottom-0 w-full rounded-3xl absolute border-slate-700 p-6 flex items-center justify-between">
|
||||
<div x-show="step === 1" class="px-8 py-3"></div>
|
||||
<button @click="goToPrevStep()" x-show="step > 1" class="px-8 py-3 text-slate-400 hover:text-white font-bold transition-all">Back</button>
|
||||
<button @click="goToNextStep()" class="px-10 py-3 bg-fuchsia-600 hover:bg-fuchsia-500 rounded-xl font-bold transition-all shadow-lg shadow-fuchsia-600/20">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,430 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Numbus Configurator</title>
|
||||
<!-- Tailwind CSS for modern styling -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Alpine.js for lightweight reactivity -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<!-- JS-YAML to convert JS objects to YAML strings -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
|
||||
<!-- Material Design Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="../media/favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body x-data="numbusPreparation()" class="p-4 bg-[#0f172a] text-slate-100 min-h-screen font-sans selection:bg-fuchsia-500/30">
|
||||
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function numbusPreparation() {
|
||||
return {
|
||||
step: 1,
|
||||
formData: {
|
||||
1: { language: 'French', country: 'France', timeZone: 'Europe/Paris' },
|
||||
2: { deviceType: '' },
|
||||
3: { deploymentMode: '' },
|
||||
4: { replicationHardware: '', replicationStrategy: '', replicationSecrets: '' },
|
||||
5: { liveIp: '', livePassword: '' },
|
||||
},
|
||||
isStepValid() {
|
||||
const currentStepData = this.formData[this.step];
|
||||
if (!currentStepData) return true;
|
||||
return Object.values(currentStepData).every(value => !!value);
|
||||
},
|
||||
goToPrevStep() {
|
||||
if (this.step === 5 && this.formData[3].deploymentMode === 'interactive') {
|
||||
this.step--;
|
||||
}
|
||||
this.step--;
|
||||
},
|
||||
goToNextStep() {
|
||||
if (this.step === 3 && this.formData[3].deploymentMode === 'interactive') {
|
||||
this.step++;
|
||||
}
|
||||
this.step++;
|
||||
},
|
||||
startDiscovery() {
|
||||
console.log("Discovery started with:", JSON.parse(JSON.stringify(this.formData)));
|
||||
// Add your bridge communication logic here
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-[#1e293b] border border-slate-700 rounded-2xl relative">
|
||||
<div class="mx-auto relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center">
|
||||
<a href="https://numbus.eu"><img class="w-auto h-10 pr-5 pl-8" src="../media/logo.png" aria-label="The numbus logo"></a>
|
||||
<a class="font-bold text-2xl tracking-tight bg-gradient-to-r from-sky-400 to-fuchsia-500 bg-clip-text text-transparent uppercase" href="https://numbus.eu">NUMBUS</a>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<h1 class="sm:text-2xl sm:pr-20 lg:pr-0 text-xl text-slate-200 font-bold flex items-center">Step 1 - Preparation</h1>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center my-auto">
|
||||
<button class="h-auto mdi mdi-menu text-slate-100 text-2xl sm:hidden pr-8" aria-label="Menu with links"></button>
|
||||
<button class="h-auto mdi mdi-brightness-2 text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="Change theme"></button>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-source-repository text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-5" aria-label="See the source code on Gitea"></a>
|
||||
<a href="https://gittea.dev/numbus" class="h-auto mdi mdi-text-box-search text-slate-100 text-2xl hidden sm:block sm:text-3xl pr-8" aria-label="See the full documentation"></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="pt-10 w-full"></div>
|
||||
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="bg-[#1e293b] border border-slate-700 rounded-3xl h-[calc(100vh-10rem)] max-w-[60vw] mx-auto relative">
|
||||
|
||||
<!-- Step 1: Language & Region -->
|
||||
<div x-show="step === 1" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Language</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-200">Set your regional preferences to ensure <b>correct</b> time and language display.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">System Language</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the primary language for the operating system and management interfaces.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].language" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none transition-all appearance-none cursor-pointer">
|
||||
<option value="FR">French</option>
|
||||
<option value="EN">English</option>
|
||||
<option value="DE">German</option>
|
||||
<option value="IT">Italian</option>
|
||||
<option value="ES">Spanish</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">Country</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the country where you are located to help define the device's locale.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].country" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
|
||||
<option value="fr_FR">France</option>
|
||||
<option value="de_DE">Germany</option>
|
||||
<option value="it_IT">Italy</option>
|
||||
<option value="en_GB">United Kingdom</option>
|
||||
<option value="en_US">United States</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2 relative" x-data="{ infoBubbleOpen: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg font-semibold text-slate-300">Time zone</label>
|
||||
<button class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl shadow-slate-900 z-50">
|
||||
<p class="text-slate-200 text-lg italic">Select the time zone in which the device is located. <br> Don't know your time zone ? Find it at <a target="_blank" href="https://en.wikipedia.org/wiki/List_of_UTC_offsets" class="text-sky-400 underline font-semibold">Wikipedia</a>.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<select x-model="formData[1].timeZone" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none appearance-none cursor-pointer">
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Type -->
|
||||
<div x-show="step === 2" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Device Type</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select the <b>device type</b> for your new Numbus machine that matches <b>your needs</b>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 pb-10 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[2].deviceType = 'server'" :class="formData[2].deviceType === 'server' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-server-light.svg" alt="Numbus Server icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center gap-2 flex">
|
||||
<span class="font-bold text-2xl">Numbus Server</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A versatile home cloud solution for hosting containers, media, and automated services.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Your own Cloud at Home.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'backup-server'" :class="formData[2].deviceType === 'backup-server' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-backup-server-light.svg" alt="Numbus Backup Server icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus Backup Server</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">An all-in-one backup solution for all your Numbus devices, with monitoring tools.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Backup all Numbus devices and monitor your servers.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'computer'" :class="formData[2].deviceType === 'computer' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-computer-light.svg" alt="Numbus Computer icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus Computer</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A polished workstation powered by leading open-source software.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">A workstation powered by leading open-source software.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[2].deviceType = 'tv'" :class="formData[2].deviceType === 'tv' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<img class="w-12 h-12 flex-shrink-0" src="../media/light/numbus-tv-light.svg" alt="Numbus TV icon">
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Numbus TV</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">A computer -and all its accompanying advantages- but with a slick, familiar TV interface.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">Your TV, your way. No spying on you.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deployment Mode -->
|
||||
<div x-show="step === 3" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Deployment Mode</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <b>preferred</b> deployment mode. Non-interactive <b>requires</b> a ready-to-go configuration hosted on a <b>git platform</b>.</p>
|
||||
<div class="pl-5 pr-5 pt-10 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[3].deploymentMode = 'interactive'" :class="formData[3].deploymentMode === 'interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-gesture-tap text-5xl flex-shrink-0"></i>
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Interactive</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">If this is your first time setting up the Numbus device you chose, follow this option.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">We will guide you through the setup process.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[3].deploymentMode = 'non-interactive'" :class="formData[3].deploymentMode === 'non-interactive' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-cog-clockwise text-5xl flex-shrink-0"></i>
|
||||
<div x-data="{ infoBubbleOpen: false }">
|
||||
<div class="items-center flex gap-2">
|
||||
<span class="font-bold text-2xl mb-1">Non-interactive</span>
|
||||
<span class="relative" @mouseenter="infoBubbleOpen = true" @mouseleave="infoBubbleOpen = false">
|
||||
<i class="mdi mdi-information text-lg text-fuchsia-600 cursor-help transition-colors duration-300 hover:text-fuchsia-500"></i>
|
||||
<div
|
||||
x-show="infoBubbleOpen"
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-90"
|
||||
x-cloak class="absolute left-full top-1/2 -translate-y-1/2 ml-3 w-80 p-5 bg-slate-800 rounded-xl border border-slate-600 shadow-2xl z-50 text-sm leading-relaxed">
|
||||
<p class="text-slate-300 text-lg italic">This option is used for mass devices deployments. It requires a first run in interactive mode.</p>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm transition-colors">You already have a ready-to-go configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Replication Mode -->
|
||||
<div class="max-h-[calc(100vh-17rem)] overflow-y-auto" x-show="step === 4" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Replication Mode</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 text-xl text-slate-300">Select your <b>preferred</b> replication mode. It compares the current deployment to the old one.</p>
|
||||
<h3 class="text-xl font-semibold text-slate-300 text-center pb-3">Hardware</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationHardware = 'exact_same'" :class="formData[4].replicationHardware === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-check-network text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same hardware</span>
|
||||
<p class="text-sm transition-colors">The hardware listed in the configuration is exactly the same as the hardware on the machine you want to deploy.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationHardware = 'different'" :class="formData[4].replicationHardware === 'different' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-close-network text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Different hardware</span>
|
||||
<p class="text-sm transition-colors">The hardware listed in the configuration is different compared to the hardware on the machine you want to deploy.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-center text-slate-300 pb-3 pt-5">Strategy</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationStrategy = 'exact_same'" :class="formData[4].replicationStrategy === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-file-check text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same configuration</span>
|
||||
<p class="text-sm transition-colors">Re-deploy the exact same configuration.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationStrategy = 'with_tweaks'" :class="formData[4].replicationStrategy=== 'with_tweaks' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-file-cog text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">With tweaks</span>
|
||||
<p class="text-sm transition-colors">Use the configuration as a base and tweak the desired settings.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-center text-slate-300 pt-5 pb-3">Secrets</h3>
|
||||
<div class="pl-5 pr-5 grid grid-cols-2 gap-4">
|
||||
<button @click="formData[4].replicationSecrets = 'exact_same'" :class="formData[4].replicationSecrets === 'exact_same' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-key-star text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">Same secrets</span>
|
||||
<p class="text-sm transition-colors">Re-use the secrets in the repository.</p>
|
||||
</div>
|
||||
</button>
|
||||
<button @click="formData[4].replicationSecrets = 'regenerate'" :class="formData[4].replicationSecrets === 'regenerate' ? 'bg-fuchsia-600/20 border-fuchsia-500' : 'bg-slate-900 border-slate-700'" class="flex items-center gap-4 p-6 border rounded-2xl transition-all text-left hover:bg-fuchsia-600/20 hover:border-fuchsia-500">
|
||||
<i class="mdi mdi-key-remove text-5xl flex-shrink-0"></i>
|
||||
<div>
|
||||
<span class="font-bold text-2xl mb-1">New secrets</span>
|
||||
<p class="text-sm transition-colors">Provide and auto-generate new secrets.</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Setup -->
|
||||
<div x-show="step === 5" x-cloak class="pl-3 pr-3">
|
||||
<h2 class="p-5 pt-6 text-4xl font-bold text-sky-400">Live Setup</h2>
|
||||
<div class="p-0.5 bg-slate-700 rounded-3xl w-[97%] mx-auto"></div>
|
||||
<p class="p-5 pb-10 text-xl text-slate-300">Provide the <b>necessary information</b> to connect to the device. It needs to be in a <b>NixOS live environment</b>.</p>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="p-6 space-y-2">
|
||||
<span class="text-base font-bold text-center text-slate-300 uppercase">Live Target IP Address</span>
|
||||
<input x-model="formData[5].liveIp" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40" type="text" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="p-6 space-y-2">
|
||||
<span class="text-base font-bold text-center text-slate-300 uppercase">Temporary Password</span>
|
||||
<input x-model="formData[5].livePassword" class="w-full bg-slate-900 border border-slate-700 rounded-xl p-4 focus:ring-2 focus:ring-fuchsia-500 outline-none placeholder:text-slate-500/40" type="password" placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="bg-[#1e293b] border-t bottom-0 w-full rounded-3xl absolute border-slate-700 p-6 flex items-center justify-between">
|
||||
<div x-show="step === 1" class="px-8 py-3"></div>
|
||||
<button @click="goToPrevStep()" x-show="step > 1" x-cloak class="px-8 py-3 text-slate-400 font-bold rounded-xl hover:text-white transition-colors duration-200">Back</button>
|
||||
|
||||
<button @click="goToNextStep()"
|
||||
x-show="step != 5"
|
||||
:disabled="!isStepValid()"
|
||||
class="px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">Continue</button>
|
||||
|
||||
<button
|
||||
@click="startDiscovery()"
|
||||
x-show="step === 5"
|
||||
x-cloak
|
||||
:disabled="!isStepValid()"
|
||||
class="px-10 py-3 text-white font-bold rounded-xl scale-100 transition duration-200 ease-in bg-gradient-to-r from-fuchsia-500 via-fuchsia-600 to-fuchsia-700 hover:scale-105 hover:bg-gradient-to-br shadow-lg shadow-fuchsia-500/30 dark:shadow-lg dark:shadow-fuchsia-800/80 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100">Start Discovery
|
||||
<svg aria-hidden="true" class="w-4 h-4 text-neutral-tertiary animate-spin fill-brand me-2" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||