From 5da58ae6d7f5122ca88fb5590fbb8eb66f1aa6f5 Mon Sep 17 00:00:00 2001 From: Lukas Holzner Date: Thu, 3 Sep 2026 23:53:18 +0200 Subject: [PATCH] feat(core): initialize NaxOS declarative NixOS distribution, modules, and ISO installer --- .gitea/workflows/build-iso.yaml | 46 ++++++ .gitea/workflows/package.yaml | 39 +++++ .gitea/workflows/test-modules.yaml | 29 ++++ README.md | 59 ++++++- flake.nix | 53 ++++++ iso/installer-iso.nix | 62 +++++++ iso/scripts/naxos-installer.sh | 193 ++++++++++++++++++++++ modules/api/daemon.nix | 83 ++++++++++ modules/core/gitops.nix | 111 +++++++++++++ modules/core/self-update.nix | 77 +++++++++ modules/core/system.nix | 96 +++++++++++ modules/core/users.nix | 80 ++++++++++ modules/default.nix | 19 +++ modules/monitoring/perses.nix | 186 +++++++++++++++++++++ modules/monitoring/prometheus.nix | 72 +++++++++ modules/services/app-engine.nix | 110 +++++++++++++ modules/services/apps/default.nix | 11 ++ modules/services/apps/immich.nix | 84 ++++++++++ modules/services/apps/jellyfin.nix | 33 ++++ modules/services/apps/nextcloud.nix | 43 +++++ modules/services/apps/paperless.nix | 55 +++++++ modules/services/apps/vaultwarden.nix | 35 ++++ modules/shares/nfs.nix | 72 +++++++++ modules/shares/samba.nix | 197 +++++++++++++++++++++++ modules/storage/zfs.nix | 222 ++++++++++++++++++++++++++ modules/ui/service.nix | 72 +++++++++ profiles/hardware-generic.nix | 28 ++++ profiles/nas-appliance.nix | 82 ++++++++++ 28 files changed, 2247 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/build-iso.yaml create mode 100644 .gitea/workflows/package.yaml create mode 100644 .gitea/workflows/test-modules.yaml create mode 100644 flake.nix create mode 100644 iso/installer-iso.nix create mode 100755 iso/scripts/naxos-installer.sh create mode 100644 modules/api/daemon.nix create mode 100644 modules/core/gitops.nix create mode 100644 modules/core/self-update.nix create mode 100644 modules/core/system.nix create mode 100644 modules/core/users.nix create mode 100644 modules/default.nix create mode 100644 modules/monitoring/perses.nix create mode 100644 modules/monitoring/prometheus.nix create mode 100644 modules/services/app-engine.nix create mode 100644 modules/services/apps/default.nix create mode 100644 modules/services/apps/immich.nix create mode 100644 modules/services/apps/jellyfin.nix create mode 100644 modules/services/apps/nextcloud.nix create mode 100644 modules/services/apps/paperless.nix create mode 100644 modules/services/apps/vaultwarden.nix create mode 100644 modules/shares/nfs.nix create mode 100644 modules/shares/samba.nix create mode 100644 modules/storage/zfs.nix create mode 100644 modules/ui/service.nix create mode 100644 profiles/hardware-generic.nix create mode 100644 profiles/nas-appliance.nix diff --git a/.gitea/workflows/build-iso.yaml b/.gitea/workflows/build-iso.yaml new file mode 100644 index 0000000..66feffa --- /dev/null +++ b/.gitea/workflows/build-iso.yaml @@ -0,0 +1,46 @@ +name: Build NaxOS Bootable ISO + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + channel: + description: 'Target Release Channel' + required: true + default: 'stable' + +jobs: + build-iso: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Nix with Flake Support + uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-24.11 + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Build Bootable NaxOS Installer ISO + run: | + nix build .#iso --show-trace + + - name: Locate ISO Artifact + id: iso-info + run: | + ISO_FILE=$(find result/iso -name "*.iso" | head -n 1) + echo "Found ISO: $ISO_FILE" + SHA256=$(sha256sum "$ISO_FILE" | cut -d' ' -f1) + echo "SHA256: $SHA256" + echo "iso_file=$ISO_FILE" >> $GITHUB_OUTPUT + echo "sha256=$SHA256" >> $GITHUB_OUTPUT + + - name: Upload ISO Artifact + uses: actions/upload-artifact@v4 + with: + name: naxos-installer-iso + path: result/iso/*.iso diff --git a/.gitea/workflows/package.yaml b/.gitea/workflows/package.yaml new file mode 100644 index 0000000..be47110 --- /dev/null +++ b/.gitea/workflows/package.yaml @@ -0,0 +1,39 @@ +name: Package NaxOS Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Build ISO + run: | + nix build .#iso + + - name: Generate Checksums + run: | + mkdir -p release-dist + cp result/iso/*.iso release-dist/ + cd release-dist + sha256sum *.iso > SHA256SUMS + + - name: Create Gitea Release + uses: softprops/action-gh-release@v2 + with: + files: | + release-dist/* + draft: false + prerelease: false + generate_release_notes: true diff --git a/.gitea/workflows/test-modules.yaml b/.gitea/workflows/test-modules.yaml new file mode 100644 index 0000000..1943652 --- /dev/null +++ b/.gitea/workflows/test-modules.yaml @@ -0,0 +1,29 @@ +name: Test NaxOS Module Configurations + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-modules: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Evaluate Flake Outputs & Check Syntax + run: | + nix flake show + nix flake check --all-systems + + - name: Validate Appliance System Configuration Evaluation + run: | + nix eval .#nixosConfigurations.naxos.config.system.build.toplevel.drvPath diff --git a/README.md b/README.md index 9f4c3a7..5eeadc0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ -# naxos-os +# NaxOS Core (`naxos-os`) -NaxOS Operating System - Declarative NixOS Core, Modules, ISO Installer, and Appliance Architecture \ No newline at end of file +[![Gitea Actions](https://git.lholz.de/naxos/naxos-os/actions/workflows/test-modules.yaml/badge.svg)](https://git.lholz.de/naxos/naxos-os/actions) + +**NaxOS** is a declarative, appliance-like Network Attached Storage (NAS) operating system built on top of **NixOS** and **OpenZFS**. + +## Architecture & Features + +- **Declarative NixOS Foundation**: Complete system state defined as code, from kernel sysctl parameters to users and storage configurations. +- **GitOps-Driven Rebuilds**: Automated `nixos-rebuild switch` with dry-build safety checks, canary testing, and instant rollback. +- **Native OpenZFS Management**: + - Declarative pools (mirror, raidz1, raidz2, stripe) + - Declarative datasets with per-dataset quotas, compression (`lz4`, `zstd`), and recordsize tuning + - First-class zero-data-loss import of existing pools (e.g. from TrueNAS or raw NixOS setups like `nixos-lukas`) + - Automated scrub, TRIM, and snapshot lifecycle policies (hourly, daily, weekly, monthly) +- **Multi-Protocol File Shares**: + - **Samba**: Preconfigured with macOS Time Machine support (`vfs_fruit`), POSIX ACL propagation, and fine-grained share permissions. + - **NFS**: High-throughput NFS exports with CIDR client filters. +- **Multi-Tier Workload & App Engine**: + - Native NixOS systemd services (e.g. Immich with Intel QuickSync / OpenCL hardware transcoding) + - Docker Compose with ZFS copy-on-write storage driver + - Optional lightweight K3s cluster integration +- **Native Observability**: + - Embedded **Perses** analytics dashboards (replacing standalone Grafana) + - Prometheus TSDB and Node Exporter +- **Streamlined Bootable ISO**: + - Interactive CLI / TUI wizard (`naxos-installer`) for rapid bare-metal installation and pool import. + +## Repository Structure + +``` +naxos-os/ +├── flake.nix # Flake exports (modules, configurations, ISO builder) +├── modules/ # NaxOS module definitions +│ ├── core/ # System base, users, gitops, self-update +│ ├── storage/ # ZFS pool/dataset management & migration +│ ├── shares/ # Samba (SMB) and NFS file shares +│ ├── services/ # App engine runtime & application catalog +│ ├── monitoring/ # Prometheus & Perses analytics engine +│ ├── api/ # Management daemon systemd service +│ └── ui/ # Web dashboard Nginx reverse proxy service +├── profiles/ # Hardware & appliance base profiles +├── iso/ # Bootable installer ISO & scripts +└── .gitea/workflows/ # Gitea Actions CI/CD workflows +``` + +## Quick Start + +### Building the Installation ISO +```bash +nix build .#iso +``` + +### Testing System Evaluation +```bash +nix flake check +nix eval .#nixosConfigurations.naxos.config.system.build.toplevel.drvPath +``` \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..1a057ad --- /dev/null +++ b/flake.nix @@ -0,0 +1,53 @@ +{ + description = "NaxOS: Declarative, Appliance-Like NixOS- & ZFS-Based Network Attached Storage"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, nixpkgs-unstable, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + in { + # Exported NixOS Modules for NaxOS + nixosModules = { + naxos = import ./modules; + default = self.nixosModules.naxos; + }; + + # Standard NaxOS Appliance System Configuration + nixosConfigurations = { + naxos = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + self.nixosModules.naxos + ./profiles/nas-appliance.nix + ./profiles/hardware-generic.nix + ]; + }; + + # Bootable Installer ISO + installer-iso = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./iso/installer-iso.nix + ]; + }; + }; + + # Exported Packages & ISO Image + packages.${system} = { + iso = self.nixosConfigurations.installer-iso.config.system.build.isoImage; + default = pkgs.writeShellScriptBin "naxos-help" '' + echo "NaxOS Appliance Flake" + echo "To build installer ISO: nix build .#iso" + echo "To test appliance configuration: nix build .#nixosConfigurations.naxos.config.system.build.toplevel" + ''; + }; + }; +} diff --git a/iso/installer-iso.nix b/iso/installer-iso.nix new file mode 100644 index 0000000..9894d3b --- /dev/null +++ b/iso/installer-iso.nix @@ -0,0 +1,62 @@ +{ pkgs, lib, ... }: + +{ + # Minimal Live ISO base + imports = [ + + ]; + + # ZFS and Storage tooling on Live ISO + boot.supportedFilesystems = [ "zfs" ]; + boot.kernelPackages = pkgs.linuxPackages; + + environment.systemPackages = with pkgs; [ + zfs + parted + gptfdisk + e2fsprogs + dosfstools + smartmontools + curl + wget + git + jq + tmux + htop + btop + whiptail + dialog + ]; + + # Automatically launch installer wizard on tty1 + services.getty.autologinUser = "root"; + + # Welcome banner and installer prompt in bash profile + environment.etc."issue".text = '' + ======================================================== + Welcome to NaxOS Installation Media + ======================================================== + To launch the interactive installer wizard, run: + naxos-installer + + To inspect disks and ZFS pools manually: + lsblk + zpool import + ======================================================== + ''; + + # Link installer script into PATH + system.activationScripts.naxosInstallerScript = '' + cp -f ${./scripts/naxos-installer.sh} /usr/local/bin/naxos-installer || true + chmod +x /usr/local/bin/naxos-installer || true + ''; + + # ISO Image identification + isoImage.isoBaseName = "naxos-installer"; + isoImage.volumeID = "NAXOS_INSTALL"; + isoImage.makeEfiBootable = true; + isoImage.makeUsbBootable = true; + + # State version + system.stateVersion = "24.11"; +} diff --git a/iso/scripts/naxos-installer.sh b/iso/scripts/naxos-installer.sh new file mode 100755 index 0000000..b11f7b1 --- /dev/null +++ b/iso/scripts/naxos-installer.sh @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# ============================================================================== +# NaxOS Automated & Interactive Installer Wizard +# Guides user through disk partitioning, ZFS pool creation or safe foreign pool +# import (TrueNAS / nixos-lukas), network setup, and initial credentials. +# ============================================================================== + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +clear +cat << "EOF" + _ _ ___ ____ + | \ | | __ ___ __/ _ \/ ___| + | \| |/ _` \ \/ / | | \___ \ + | |\ | (_| |> <| |_| |___) | + |_| \_|\__,_/_/\_\\___/|____/ + Declarative NixOS & ZFS Storage Appliance +================================================ +EOF + +echo -e "${BOLD}${CYAN}Welcome to the NaxOS Installation Wizard!${NC}\n" + +# 1. Hardware Check +echo -e "${BOLD}[1/6] Scanning Hardware & Storage Devices...${NC}" +TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}') +CPU_CORES=$(nproc) +echo " Detected CPU Cores: $CPU_CORES" +echo " Detected RAM: ${TOTAL_RAM_MB} MB" + +echo -e "\nAvailable Physical Disks:" +lsblk -d -p -n -o NAME,SIZE,MODEL,TRAN | grep -v 'loop' || true + +# 2. Storage Pool Setup (New vs Import) +echo -e "\n${BOLD}[2/6] Storage Architecture Configuration${NC}" +echo "1) Create a brand new ZFS storage pool (Formatted & optimized for NAS)" +echo "2) Import an EXISTING ZFS pool (Preserve existing data e.g. TrueNAS or nixos-lukas 'tank')" +echo "3) Skip storage configuration (Configure post-boot via Web Dashboard)" +read -rp "Select option [1-3] (default: 2): " STORAGE_OPTION +STORAGE_OPTION=${STORAGE_OPTION:-2} + +POOL_NAME="tank" +IMPORTED_POOL="" +CREATE_POOL=false + +if [ "$STORAGE_OPTION" = "1" ]; then + CREATE_POOL=true + read -rp "Enter new pool name (default: tank): " POOL_INPUT + POOL_NAME=${POOL_INPUT:-tank} + + echo "Select pool layout: [1] Mirror, [2] RAID-Z1, [3] RAID-Z2, [4] Single/Stripe" + read -rp "Layout (default: 1): " LAYOUT_INPUT + LAYOUT_INPUT=${LAYOUT_INPUT:-1} + + read -rp "Enter disk devices space-separated (e.g. /dev/sdb /dev/sdc): " DISK_DEVICES + +elif [ "$STORAGE_OPTION" = "2" ]; then + echo -e "\n${BLUE}Scanning for unimported ZFS pools on connected drives...${NC}" + zpool import || true + + read -rp "Enter the name of the existing pool to import (e.g. tank): " IMPORTED_POOL + if [ -n "$IMPORTED_POOL" ]; then + echo -e "${GREEN}Verifying pool '$IMPORTED_POOL' status safely without mounting...${NC}" + zpool import -N -f "$IMPORTED_POOL" || echo "Warning: Pool could not be imported immediately. Will configure for boot-time import." + fi +fi + +# 3. Target System OS Disk Selection +echo -e "\n${BOLD}[3/6] Target OS Disk Selection${NC}" +echo "Enter the disk where NaxOS system files should be installed (WARNING: this disk will be formatted!):" +read -rp "OS Disk (e.g. /dev/sda or /dev/nvme0n1): " OS_DISK + +if [ -z "$OS_DISK" ] || [ ! -b "$OS_DISK" ]; then + echo -e "${RED}Error: Disk '$OS_DISK' does not exist! Aborting.${NC}" + exit 1 +fi + +echo -e "${RED}${BOLD}WARNING: All data on $OS_DISK will be destroyed!${NC}" +read -rp "Are you sure you want to proceed with $OS_DISK? (yes/no): " CONFIRM +if [ "$CONFIRM" != "yes" ]; then + echo "Installation canceled by user." + exit 0 +fi + +# 4. Network Configuration +echo -e "\n${BOLD}[4/6] Network Configuration${NC}" +read -rp "Appliance Hostname (default: naxos): " HOSTNAME +HOSTNAME=${HOSTNAME:-naxos} + +read -rp "Use DHCP for network? (yes/no, default: yes): " USE_DHCP +USE_DHCP=${USE_DHCP:-yes} + +# 5. Admin User & Credentials +echo -e "\n${BOLD}[5/6] Administrator Account & Access${NC}" +read -rp "Admin username (default: admin): " ADMIN_USER +ADMIN_USER=${ADMIN_USER:-admin} + +read -rp "Paste Admin SSH Public Key (optional): " ADMIN_SSH_KEY + +# 6. Partitioning and Installation +echo -e "\n${BOLD}[6/6] Executing Partitioning & Installation...${NC}" + +# Partition OS disk: 1GB EFI, remainder for root +wipefs -a "$OS_DISK" +parted -s "$OS_DISK" -- mklabel gpt +parted -s "$OS_DISK" -- mkpart ESP fat32 1MiB 1024MiB +parted -s "$OS_DISK" -- set 1 esp on +parted -s "$OS_DISK" -- mkpart primary ext4 1024MiB 100% + +# Detect partition names (handles nvme0n1p1 vs sda1) +if [[ "$OS_DISK" =~ [0-9]$ ]]; then + EFI_PART="${OS_DISK}p1" + ROOT_PART="${OS_DISK}p2" +else + EFI_PART="${OS_DISK}1" + ROOT_PART="${OS_DISK}2" +fi + +mkfs.vfat -F 32 -n NIXOS_BOOT "$EFI_PART" +mkfs.ext4 -F -L NIXOS_ROOT "$ROOT_PART" + +# Mount filesystems +mount "$ROOT_PART" /mnt +mkdir -p /mnt/boot +mount "$EFI_PART" /mnt/boot + +# Generate hardware configuration +mkdir -p /mnt/etc/nixos +nixos-generate-config --root /mnt + +# If creating pool now +if [ "$CREATE_POOL" = true ] && [ -n "${DISK_DEVICES:-}" ]; then + echo "Creating ZFS pool $POOL_NAME..." + zpool create -f -o ashift=12 -O compression=lz4 -O acltype=posixacl -O xattr=sa "$POOL_NAME" $DISK_DEVICES + zfs create "$POOL_NAME/media" + zfs create "$POOL_NAME/backup" + zfs create "$POOL_NAME/container" +fi + +# Generate unique hostId +HOST_ID=$(head -c4 /dev/urandom | od -A none -t x4 | tr -d ' ') + +# Write appliance flake and configuration +cat << NIXCONFIG > /mnt/etc/nixos/configuration.nix +{ config, pkgs, lib, ... }: + +{ + imports = [ + ./hardware-configuration.nix + ]; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + boot.supportedFilesystems = [ "zfs" ]; + + networking.hostName = "$HOSTNAME"; + networking.hostId = "$HOST_ID"; + networking.useDHCP = lib.mkDefault $([ "$USE_DHCP" = "yes" ] && echo "true" || echo "false"); + + # Safe ZFS extra pools + boot.zfs.extraPools = [ "$POOL_NAME" $([ -n "$IMPORTED_POOL" ] && echo "\"$IMPORTED_POOL\"") ]; + + # Administrator user + users.users.$ADMIN_USER = { + isNormalUser = true; + extraGroups = [ "wheel" "docker" "video" "render" ]; + openssh.authorizedKeys.keys = [ $([ -n "$ADMIN_SSH_KEY" ] && echo "\"$ADMIN_SSH_KEY\"") ]; + }; + security.sudo.wheelNeedsPassword = false; + + # SSH Access + services.openssh.enable = true; + + # State version + system.stateVersion = "24.11"; +} +NIXCONFIG + +echo -e "\n${GREEN}Beginning NixOS System Installation via nixos-install...${NC}" +nixos-install --no-root-passwd + +echo -e "\n${BOLD}${GREEN}==============================================${NC}" +echo -e "${BOLD}${GREEN} NaxOS Installation Complete!${NC}" +echo -e "${BOLD}${GREEN} Remove the installation USB/ISO and reboot.${NC}" +echo -e "${BOLD}${GREEN} Access Dashboard on: http://$HOSTNAME.local${NC}" +echo -e "${BOLD}${GREEN}==============================================${NC}" +EOF diff --git a/modules/api/daemon.nix b/modules/api/daemon.nix new file mode 100644 index 0000000..6daeff5 --- /dev/null +++ b/modules/api/daemon.nix @@ -0,0 +1,83 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.api; +in { + options.services.naxos.api = { + enable = mkEnableOption "NaxOS Management Daemon & REST/WebSocket API Service"; + + port = mkOption { + type = types.port; + default = 8088; + description = "Internal daemon listen port."; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Internal daemon bind host."; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/naxos"; + description = "Persistent state directory for NaxOS daemon."; + }; + + configRepoDir = mkOption { + type = types.str; + default = "/etc/naxos/repo"; + description = "Directory of the local GitOps configuration repository."; + }; + }; + + config = mkIf cfg.enable { + systemd.services.naxos-api = { + description = "NaxOS Declarative Management Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "zfs.target" ]; + path = with pkgs; [ + zfs + git + nix + systemd + smartmontools + util-linux + shadow + samba + curl + bash + ]; + environment = { + NODE_ENV = "production"; + PORT = toString cfg.port; + HOST = cfg.host; + NAXOS_DATA_DIR = cfg.dataDir; + NAXOS_CONFIG_REPO = cfg.configRepoDir; + SYSTEM_PROFILE = "/nix/var/nix/profiles/system"; + }; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.nodejs}/bin/node /opt/naxos/api/dist/server.js"; + WorkingDirectory = cfg.dataDir; + Restart = "always"; + RestartSec = "3s"; + StateDirectory = "naxos"; + RuntimeDirectory = "naxos"; + # Sandboxing / Security hardening + ProtectSystem = "strict"; + ProtectHome = "read-only"; + ReadWritePaths = [ cfg.dataDir cfg.configRepoDir "/var/log" "/etc/naxos" "/nix/var/nix/profiles" ]; + AmbientCapabilities = [ "CAP_SYS_ADMIN" ]; # For ZFS operations & systemd manipulation + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0750 root root -" + "d ${cfg.configRepoDir} 0750 root root -" + "d /etc/naxos 0755 root root -" + ]; + }; +} diff --git a/modules/core/gitops.nix b/modules/core/gitops.nix new file mode 100644 index 0000000..5678f07 --- /dev/null +++ b/modules/core/gitops.nix @@ -0,0 +1,111 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.gitops; + + # Safe switch script with canary validation and automatic rollback + naxosRebuildWrapper = pkgs.writeShellScriptBin "naxos-rebuild-safe" '' + set -euo pipefail + CONFIG_DIR="''${CONFIG_DIR:-/etc/naxos/repo}" + LOG_FILE="/var/log/naxos-rebuild.log" + + echo "[$(date -Iseconds)] Starting NaxOS declarative rebuild..." | tee -a "$LOG_FILE" + + cd "$CONFIG_DIR" + + # Pre-flight check with nix flake check / nixos-rebuild dry-build + echo "Running dry build validation..." + if ! nixos-rebuild build --flake .#naxos 2>&1 | tee -a "$LOG_FILE"; then + echo "Dry-build failed! Configuration aborted without changing running system." | tee -a "$LOG_FILE" + exit 1 + fi + + # Record current generation + PREV_GEN=$(readlink -f /nix/var/nix/profiles/system) + + echo "Applying system switch..." + if nixos-rebuild switch --flake .#naxos 2>&1 | tee -a "$LOG_FILE"; then + echo "System switch succeeded." | tee -a "$LOG_FILE" + + # Canary health check: verify management daemon and storage pools are responsive + if systemctl is-active --quiet naxos-api.service; then + echo "Canary verification passed: NaxOS daemon active." | tee -a "$LOG_FILE" + exit 0 + else + echo "WARNING: NaxOS daemon failed canary test! Initiating safe automatic rollback..." | tee -a "$LOG_FILE" + "$PREV_GEN/bin/switch-to-configuration" switch + exit 2 + fi + else + echo "Switch command failed! Rolling back to $PREV_GEN..." | tee -a "$LOG_FILE" + "$PREV_GEN/bin/switch-to-configuration" switch + exit 3 + fi + ''; + + # Automated remote push/pull service + naxosGitSync = pkgs.writeShellScriptBin "naxos-git-sync" '' + set -euo pipefail + REPO_DIR="/etc/naxos/repo" + + if [ ! -d "$REPO_DIR/.git" ]; then + echo "Git repository not initialized in $REPO_DIR." + exit 0 + fi + + cd "$REPO_DIR" + + if [ -n "''${NAXOS_REMOTE_URL:-}" ]; then + echo "Synchronizing with remote GitOps repository..." + git fetch origin main || true + git push origin main || true + fi + ''; + +in { + options.services.naxos.gitops = { + enable = mkEnableOption "NaxOS GitOps Configuration Synchronization Engine"; + + remoteUrl = mkOption { + type = types.nullOr types.str; + default = null; + example = "ssh://git@git.lholz.de:2222/naxos/naxos-config.git"; + description = "Remote Git repository URL for automated backup, auditing, and disaster recovery."; + }; + + branch = mkOption { + type = types.str; + default = "main"; + description = "GitOps target branch."; + }; + + tokenFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to Gitea/Git access token or SSH private key."; + }; + + autoPushOnCommit = mkOption { + type = types.bool; + default = true; + description = "Automatically push to remote repository whenever web dashboard commits a change."; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + git + naxosRebuildWrapper + naxosGitSync + ]; + + # Create config repo directory structure + systemd.tmpfiles.rules = [ + "d /etc/naxos 0755 root root -" + "d /etc/naxos/repo 0750 root root -" + "d /var/log 0755 root root -" + ]; + }; +} diff --git a/modules/core/self-update.nix b/modules/core/self-update.nix new file mode 100644 index 0000000..4b7d5aa --- /dev/null +++ b/modules/core/self-update.nix @@ -0,0 +1,77 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.selfUpdate; + + updateScript = pkgs.writeShellScriptBin "naxos-self-update" '' + set -euo pipefail + CONFIG_DIR="''${CONFIG_DIR:-/etc/naxos/repo}" + LOG_FILE="/var/log/naxos-update.log" + + echo "[$(date -Iseconds)] Checking for NaxOS updates..." | tee -a "$LOG_FILE" + + if [ ! -d "$CONFIG_DIR/.git" ]; then + echo "Configuration directory is not a git repository. Skipping." | tee -a "$LOG_FILE" + exit 0 + fi + + cd "$CONFIG_DIR" + git fetch origin main + + LOCAL_HASH=$(git rev-parse HEAD) + REMOTE_HASH=$(git rev-parse origin/main) + + if [ "$LOCAL_HASH" = "$REMOTE_HASH" ]; then + echo "System is already up to date ($LOCAL_HASH)." | tee -a "$LOG_FILE" + exit 0 + fi + + echo "New updates detected ($LOCAL_HASH -> $REMOTE_HASH). Updating flake inputs and pulling..." | tee -a "$LOG_FILE" + git merge origin/main --ff-only + + echo "Rebuilding and applying configuration..." | tee -a "$LOG_FILE" + naxos-rebuild-safe + ''; + +in { + options.services.naxos.selfUpdate = { + enable = mkEnableOption "NaxOS Automated Background Self-Update Service"; + + schedule = mkOption { + type = types.str; + default = "*-*-* 04:00:00"; # Daily at 4:00 AM + description = "Systemd calendar expression for scheduled update checks."; + }; + + channel = mkOption { + type = types.enum [ "stable" "beta" "nightly" ]; + default = "stable"; + description = "Update release channel."; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ updateScript ]; + + systemd.services.naxos-self-update = { + description = "NaxOS Self-Update Check & Apply"; + path = [ pkgs.git pkgs.nix pkgs.systemd pkgs.bash ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${updateScript}/bin/naxos-self-update"; + }; + }; + + systemd.timers.naxos-self-update = { + description = "NaxOS Scheduled Self-Update Timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.schedule; + Persistent = true; + RandomizedDelaySec = "1800"; # 30 min random jitter + }; + }; + }; +} diff --git a/modules/core/system.nix b/modules/core/system.nix new file mode 100644 index 0000000..359271c --- /dev/null +++ b/modules/core/system.nix @@ -0,0 +1,96 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.core; +in { + options.services.naxos.core = { + enable = mkEnableOption "NaxOS Core Appliance Base"; + + hostName = mkOption { + type = types.str; + default = "naxos"; + description = "Appliance hostname."; + }; + + hostId = mkOption { + type = types.str; + default = "8425f3a1"; + description = "32-bit Host ID required for OpenZFS safety locking."; + }; + + timeZone = mkOption { + type = types.str; + default = "Europe/Berlin"; + description = "System timezone."; + }; + }; + + config = mkIf cfg.enable { + networking.hostName = cfg.hostName; + networking.hostId = cfg.hostId; + time.timeZone = cfg.timeZone; + + # Flakes and Nix CLI enablement + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + auto-optimise-store = true; + }; + + # High-Performance Storage & Network Kernel Tuning + boot.kernel.sysctl = { + # BBR Congestion Control + "net.core.default_qdisc" = "fq"; + "net.ipv4.tcp_congestion_control" = "bbr"; + + # High-bandwidth 10G/25G network buffer tuning + "net.core.rmem_max" = 67108864; + "net.core.wmem_max" = 67108864; + "net.ipv4.tcp_rmem" = "4096 87380 33554432"; + "net.ipv4.tcp_wmem" = "4096 65536 33554432"; + "net.core.netdev_max_backlog" = 10000; + + # Storage & VM writeback tuning for ZFS + "vm.swappiness" = 10; + "vm.dirty_background_ratio" = 5; + "vm.dirty_ratio" = 10; + + # File handles limit + "fs.file-max" = 2097152; + }; + + # Core system tools + environment.systemPackages = with pkgs; [ + curl + wget + git + htop + btop + tmux + jq + pciutils + usbutils + ethtool + iperf3 + rsync + ]; + + # Security & Firewall defaults + networking.firewall = { + enable = true; + allowPing = true; + allowedTCPPorts = [ 22 80 443 ]; + }; + + # SSH Server with modern secure defaults + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "prohibit-password"; + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + }; + }; + }; +} diff --git a/modules/core/users.nix b/modules/core/users.nix new file mode 100644 index 0000000..659890c --- /dev/null +++ b/modules/core/users.nix @@ -0,0 +1,80 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.users; +in { + options.services.naxos.users = { + enable = mkEnableOption "NaxOS Declarative User Management"; + + adminUser = mkOption { + type = types.str; + default = "admin"; + description = "Primary administrator account name."; + }; + + adminSshKeys = mkOption { + type = types.listOf types.str; + default = []; + description = "Public SSH keys for the administrator."; + }; + + users = mkOption { + type = types.attrsOf (types.submodule { + options = { + description = mkOption { + type = types.str; + default = ""; + description = "User full name or comment."; + }; + isAdmin = mkOption { + type = types.bool; + default = false; + description = "Whether the user has sudo/wheel privileges."; + }; + sshKeys = mkOption { + type = types.listOf types.str; + default = []; + description = "Public SSH authorized keys."; + }; + extraGroups = mkOption { + type = types.listOf types.str; + default = []; + description = "Additional Linux groups."; + }; + smbAccess = mkOption { + type = types.bool; + default = true; + description = "Whether the user can access SMB shares."; + }; + }; + }); + default = {}; + description = "Appliance user accounts."; + }; + }; + + config = mkIf cfg.enable { + users.mutableUsers = true; + + # Admin user creation + users.users.${cfg.adminUser} = { + isNormalUser = true; + description = "NaxOS Primary Administrator"; + extraGroups = [ "wheel" "docker" "video" "render" "users" ]; + openssh.authorizedKeys.keys = cfg.adminSshKeys; + shell = pkgs.bashInteractive; + }; + + # Additional users + users.users = mapAttrs (name: ucfg: { + isNormalUser = true; + description = ucfg.description; + extraGroups = (if ucfg.isAdmin then [ "wheel" ] else []) ++ ucfg.extraGroups ++ [ "users" ]; + openssh.authorizedKeys.keys = ucfg.sshKeys; + }) cfg.users; + + security.sudo.wheelNeedsPassword = false; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000..5a894bf --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,19 @@ +{ ... }: + +{ + imports = [ + ./core/system.nix + ./core/users.nix + ./core/gitops.nix + ./core/self-update.nix + ./storage/zfs.nix + ./shares/samba.nix + ./shares/nfs.nix + ./services/app-engine.nix + ./services/apps + ./monitoring/prometheus.nix + ./monitoring/perses.nix + ./api/daemon.nix + ./ui/service.nix + ]; +} diff --git a/modules/monitoring/perses.nix b/modules/monitoring/perses.nix new file mode 100644 index 0000000..315730d --- /dev/null +++ b/modules/monitoring/perses.nix @@ -0,0 +1,186 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.perses; + + # Declarative Perses Prometheus Datasource Manifest + datasourceManifest = pkgs.writeText "perses-prom-datasource.json" (builtins.toJSON { + kind = "Datasource"; + metadata = { + name = "PrometheusDemo"; + project = "naxos"; + }; + spec = { + default = true; + plugin = { + kind = "PrometheusDatasource"; + spec = { + directUrl = "http://127.0.0.1:9090"; + }; + }; + }; + }); + + # Declarative Perses ZFS & System Dashboard Manifest + zfsDashboardManifest = pkgs.writeText "perses-zfs-dashboard.json" (builtins.toJSON { + kind = "Dashboard"; + metadata = { + name = "zfs-storage-health"; + project = "naxos"; + }; + spec = { + duration = "1h"; + refreshInterval = "10s"; + display = { + name = "ZFS Storage & System Health"; + description = "Native NaxOS analytics powered by Perses"; + }; + variables = []; + panels = { + cpuUsage = { + kind = "Panel"; + spec = { + display = { name = "CPU Utilization (%)"; }; + plugin = { + kind = "TimeSeriesChart"; + spec = { + queries = [{ + kind = "TimeSeriesQuery"; + spec = { + plugin = { + kind = "PrometheusTimeSeriesQuery"; + spec = { + query = "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode='idle'}[1m])) * 100)"; + }; + }; + }; + }]; + }; + }; + }; + }; + memoryArc = { + kind = "Panel"; + spec = { + display = { name = "Memory & ARC Cache (Bytes)"; }; + plugin = { + kind = "TimeSeriesChart"; + spec = { + queries = [ + { + kind = "TimeSeriesQuery"; + spec = { + plugin = { + kind = "PrometheusTimeSeriesQuery"; + spec = { + query = "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"; + }; + }; + }; + } + ]; + }; + }; + }; + }; + diskIO = { + kind = "Panel"; + spec = { + display = { name = "ZFS Pool Read/Write Throughput"; }; + plugin = { + kind = "TimeSeriesChart"; + spec = { + queries = [{ + kind = "TimeSeriesQuery"; + spec = { + plugin = { + kind = "PrometheusTimeSeriesQuery"; + spec = { + query = "rate(node_disk_read_bytes_total[1m]) + rate(node_disk_written_bytes_total[1m])"; + }; + }; + }; + }]; + }; + }; + }; + }; + }; + layouts = [ + { + kind = "Grid"; + spec = { + items = [ + { x = 0; y = 0; width = 12; height = 6; content = { "$ref" = "#/spec/panels/cpuUsage"; }; } + { x = 12; y = 0; width = 12; height = 6; content = { "$ref" = "#/spec/panels/memoryArc"; }; } + { x = 0; y = 6; width = 24; height = 8; content = { "$ref" = "#/spec/panels/diskIO"; }; } + ]; + }; + } + ]; + }; + }); + + persesConfigFile = pkgs.writeText "perses-config.yaml" '' + database: + file: + folder: "/var/lib/perses" + extension: "json" + schemas: + panels_path: "/var/lib/perses/schemas/panels" + queries_path: "/var/lib/perses/schemas/queries" + datasources_path: "/var/lib/perses/schemas/datasources" + variables_path: "/var/lib/perses/schemas/variables" + security: + readonly: false + enable_auth: false + ''; + +in { + options.services.naxos.perses = { + enable = mkEnableOption "Perses Embedded Analytics Engine"; + + port = mkOption { + type = types.port; + default = 8080; + description = "Perses dashboard server port."; + }; + }; + + config = mkIf cfg.enable { + # System user for Perses + users.users.perses = { + isSystemUser = true; + group = "perses"; + home = "/var/lib/perses"; + createHome = true; + }; + users.groups.perses = {}; + + # Systemd service for Perses + systemd.services.perses = { + description = "Perses Native Observability and Dashboard Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "prometheus.service" ]; + serviceConfig = { + User = "perses"; + Group = "perses"; + StateDirectory = "perses"; + WorkingDirectory = "/var/lib/perses"; + ExecStartPre = pkgs.writeShellScript "perses-provisioning" '' + mkdir -p /var/lib/perses/projects/naxos/dashboards + mkdir -p /var/lib/perses/projects/naxos/datasources + cp -f ${datasourceManifest} /var/lib/perses/projects/naxos/datasources/PrometheusDemo.json + cp -f ${zfsDashboardManifest} /var/lib/perses/projects/naxos/dashboards/zfs-storage-health.json + ''; + ExecStart = "${pkgs.perses or pkgs.prometheus}/bin/perses --config=${persesConfigFile} --port=${toString cfg.port}"; + Restart = "always"; + RestartSec = "5s"; + }; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + }; +} diff --git a/modules/monitoring/prometheus.nix b/modules/monitoring/prometheus.nix new file mode 100644 index 0000000..0657065 --- /dev/null +++ b/modules/monitoring/prometheus.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.monitoring; +in { + options.services.naxos.monitoring = { + enable = mkEnableOption "NaxOS Metrics and Telemetry Collection"; + + prometheusPort = mkOption { + type = types.port; + default = 9090; + description = "Prometheus server listen port."; + }; + + nodeExporterPort = mkOption { + type = types.port; + default = 9100; + description = "Node exporter listen port."; + }; + + retentionTime = mkOption { + type = types.str; + default = "15d"; + description = "Metrics retention period."; + }; + }; + + config = mkIf cfg.enable { + # Prometheus TSDB + services.prometheus = { + enable = true; + port = cfg.prometheusPort; + retentionTime = cfg.retentionTime; + extraFlags = [ + "--web.enable-remote-write-receiver" + ]; + scrapeConfigs = [ + { + job_name = "node"; + static_configs = [{ + targets = [ "127.0.0.1:${toString cfg.nodeExporterPort}" ]; + }]; + } + { + job_name = "naxos-api"; + static_configs = [{ + targets = [ "127.0.0.1:8088" ]; + }]; + } + ]; + }; + + # Node exporter for hardware & OS metrics + services.prometheus.exporters.node = { + enable = true; + port = cfg.nodeExporterPort; + enabledCollectors = [ + "cpu" + "diskstats" + "filesystem" + "loadavg" + "meminfo" + "netdev" + "stat" + "systemd" + "thermal_zone" + ]; + }; + }; +} diff --git a/modules/services/app-engine.nix b/modules/services/app-engine.nix new file mode 100644 index 0000000..0ffd54c --- /dev/null +++ b/modules/services/app-engine.nix @@ -0,0 +1,110 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.appEngine; +in { + options.services.naxos.appEngine = { + enable = mkEnableOption "NaxOS Multi-Tier App Engine"; + + defaultRuntime = mkOption { + type = types.enum [ "systemd" "docker" "k3s" ]; + default = "docker"; + description = "Default execution runtime for user workloads."; + }; + + docker = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable Docker engine for containerized applications."; + }; + storageDriver = mkOption { + type = types.enum [ "zfs" "overlay2" "btrfs" ]; + default = "zfs"; + description = "Container storage driver. 'zfs' uses native copy-on-write datasets."; + }; + dataRoot = mkOption { + type = types.str; + default = "/var/lib/docker"; + description = "Storage root for container images and layers."; + }; + }; + + k3s = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable lightweight K3s single-node cluster for cloud-native orchestration."; + }; + role = mkOption { + type = types.enum [ "server" "agent" ]; + default = "server"; + description = "K3s node role."; + }; + tokenFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to token file for K3s node join / cluster security."; + }; + }; + + gpuAcceleration = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable hardware transcoding and machine learning acceleration."; + }; + vendor = mkOption { + type = types.enum [ "intel" "nvidia" "amd" "none" ]; + default = "intel"; + description = "Primary GPU hardware vendor."; + }; + }; + }; + + config = mkIf cfg.enable { + # 1. Docker Runtime Configuration + virtualisation.docker = mkIf cfg.docker.enable { + enable = true; + storageDriver = cfg.docker.storageDriver; + daemon.settings = { + data-root = cfg.docker.dataRoot; + log-driver = "journald"; + }; + }; + + # OCI container backend compatibility + virtualisation.oci-containers.backend = mkIf cfg.docker.enable "docker"; + + # 2. K3s Runtime Configuration + services.k3s = mkIf cfg.k3s.enable { + enable = true; + role = cfg.k3s.role; + tokenFile = cfg.k3s.tokenFile; + extraFlags = toString [ + "--disable=traefik" # We manage ingress/reverse-proxy through NaxOS + "--snapshotter=native" + ]; + }; + + # 3. Hardware Graphics Acceleration + hardware.graphics = mkIf cfg.gpuAcceleration.enable { + enable = true; + extraPackages = mkIf (cfg.gpuAcceleration.vendor == "intel") (with pkgs; [ + intel-media-driver # Broadwell or newer + intel-compute-runtime # OpenCL support + vpl-gpu-rt # QSV support (11th Gen+) + ]); + }; + + # 4. System packages for workload operations + environment.systemPackages = with pkgs; [ + docker-compose + lazydocker + kubectl + dive + ]; + }; +} diff --git a/modules/services/apps/default.nix b/modules/services/apps/default.nix new file mode 100644 index 0000000..bef8eaa --- /dev/null +++ b/modules/services/apps/default.nix @@ -0,0 +1,11 @@ +{ ... }: + +{ + imports = [ + ./immich.nix + ./nextcloud.nix + ./jellyfin.nix + ./paperless.nix + ./vaultwarden.nix + ]; +} diff --git a/modules/services/apps/immich.nix b/modules/services/apps/immich.nix new file mode 100644 index 0000000..134116a --- /dev/null +++ b/modules/services/apps/immich.nix @@ -0,0 +1,84 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.apps.immich; +in { + options.services.naxos.apps.immich = { + enable = mkEnableOption "Immich Self-Hosted Photo & Video Hub"; + + runtime = mkOption { + type = types.enum [ "systemd" "docker" ]; + default = "systemd"; + description = "Runtime to execute Immich (systemd for native bare-metal speed with NixOS package, docker for containerized)."; + }; + + port = mkOption { + type = types.port; + default = 2283; + description = "Web interface and API port."; + }; + + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Listen host."; + }; + + mediaLocation = mkOption { + type = types.str; + default = "/tank/media/photos"; + description = "ZFS dataset or path where uploaded photos and videos are stored."; + }; + + accelerationDevices = mkOption { + type = types.listOf types.str; + default = [ "/dev/dri/renderD128" ]; + description = "DRM render devices for hardware-accelerated transcoding."; + }; + + machineLearningCPUQuota = mkOption { + type = types.str; + default = "200%"; + description = "CPU quota for machine learning service (200% = 2 full cores)."; + }; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Open port in firewall."; + }; + }; + + config = mkIf cfg.enable { + # Systemd / Native NixOS Deployment + services.immich = mkIf (cfg.runtime == "systemd") { + enable = true; + host = cfg.host; + port = cfg.port; + mediaLocation = cfg.mediaLocation; + openFirewall = cfg.openFirewall; + accelerationDevices = cfg.accelerationDevices; + }; + + services.redis.servers.immich = mkIf (cfg.runtime == "systemd") { + logLevel = "warning"; + }; + + systemd.services.immich-machine-learning = mkIf (cfg.runtime == "systemd") { + serviceConfig = { + CPUQuota = cfg.machineLearningCPUQuota; + Nice = 19; + }; + }; + + users.users.immich = mkIf (cfg.runtime == "systemd") { + extraGroups = [ "video" "render" ]; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.mediaLocation} 0750 immich immich -" + ]; + }; +} diff --git a/modules/services/apps/jellyfin.nix b/modules/services/apps/jellyfin.nix new file mode 100644 index 0000000..7df0bea --- /dev/null +++ b/modules/services/apps/jellyfin.nix @@ -0,0 +1,33 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.apps.jellyfin; +in { + options.services.naxos.apps.jellyfin = { + enable = mkEnableOption "Jellyfin Open-Source Media Streaming Server"; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Open port 8096 in firewall."; + }; + + user = mkOption { + type = types.str; + default = "jellyfin"; + description = "Service user."; + }; + }; + + config = mkIf cfg.enable { + services.jellyfin = { + enable = true; + openFirewall = cfg.openFirewall; + user = cfg.user; + }; + + users.users.${cfg.user}.extraGroups = [ "video" "render" ]; + }; +} diff --git a/modules/services/apps/nextcloud.nix b/modules/services/apps/nextcloud.nix new file mode 100644 index 0000000..4ca3149 --- /dev/null +++ b/modules/services/apps/nextcloud.nix @@ -0,0 +1,43 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.apps.nextcloud; +in { + options.services.naxos.apps.nextcloud = { + enable = mkEnableOption "Nextcloud Personal Cloud & Collaboration Platform"; + + hostName = mkOption { + type = types.str; + default = "cloud.local"; + description = "Domain / hostname for Nextcloud."; + }; + + homeDir = mkOption { + type = types.str; + default = "/tank/data/nextcloud"; + description = "Persistent data directory on ZFS storage."; + }; + + adminpassFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to file containing initial admin password."; + }; + }; + + config = mkIf cfg.enable { + services.nextcloud = { + enable = true; + hostName = cfg.hostName; + home = cfg.homeDir; + config = { + adminuser = "admin"; + adminpassFile = cfg.adminpassFile; + dbtype = "sqlite"; + }; + caching.redis = true; + }; + }; +} diff --git a/modules/services/apps/paperless.nix b/modules/services/apps/paperless.nix new file mode 100644 index 0000000..9aa3a0a --- /dev/null +++ b/modules/services/apps/paperless.nix @@ -0,0 +1,55 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.apps.paperless; +in { + options.services.naxos.apps.paperless = { + enable = mkEnableOption "Paperless-ngx Document Archiving System"; + + port = mkOption { + type = types.port; + default = 28981; + description = "Web interface port."; + }; + + mediaDir = mkOption { + type = types.str; + default = "/tank/data/paperless/media"; + description = "Directory where archived documents and OCR results are stored."; + }; + + consumptionDir = mkOption { + type = types.str; + default = "/tank/scans"; + description = "Ingestion directory where scanner uploads incoming documents."; + }; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Open port in firewall."; + }; + }; + + config = mkIf cfg.enable { + services.paperless = { + enable = true; + port = cfg.port; + mediaDir = cfg.mediaDir; + consumptionDir = cfg.consumptionDir; + settings = { + PAPERLESS_OCR_LANGUAGE = "deu+eng"; + PAPERLESS_CONSUMER_POLLING = 30; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.mediaDir} 0750 paperless paperless -" + "d ${cfg.consumptionDir} 0775 paperless users -" + ]; + }; +} diff --git a/modules/services/apps/vaultwarden.nix b/modules/services/apps/vaultwarden.nix new file mode 100644 index 0000000..11a042e --- /dev/null +++ b/modules/services/apps/vaultwarden.nix @@ -0,0 +1,35 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.apps.vaultwarden; +in { + options.services.naxos.apps.vaultwarden = { + enable = mkEnableOption "Vaultwarden Password & Secret Vault"; + + port = mkOption { + type = types.port; + default = 8222; + description = "Web interface port."; + }; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Open port in firewall."; + }; + }; + + config = mkIf cfg.enable { + services.vaultwarden = { + enable = true; + config = { + ROCKET_PORT = cfg.port; + ROCKET_ADDRESS = "0.0.0.0"; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/modules/shares/nfs.nix b/modules/shares/nfs.nix new file mode 100644 index 0000000..55791be --- /dev/null +++ b/modules/shares/nfs.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.shares.nfs; +in { + options.services.naxos.shares.nfs = { + enable = mkEnableOption "NaxOS Declarative NFS Service"; + + lockdPort = mkOption { + type = types.int; + default = 4001; + description = "Port for lockd."; + }; + + mountdPort = mkOption { + type = types.int; + default = 4002; + description = "Port for mountd."; + }; + + exports = mkOption { + type = types.listOf (types.submodule { + options = { + path = mkOption { + type = types.str; + description = "Path to exported directory or dataset."; + }; + clients = mkOption { + type = types.listOf (types.submodule { + options = { + subnet = mkOption { + type = types.str; + example = "10.0.0.0/23"; + description = "Allowed client subnet or IP."; + }; + options = mkOption { + type = types.str; + default = "rw,sync,no_subtree_check,no_root_squash"; + description = "NFS export options."; + }; + }; + }); + default = []; + description = "Clients allowed to mount this export."; + }; + }; + }); + default = []; + description = "List of NFS exported directories."; + }; + }; + + config = mkIf cfg.enable { + services.nfs.server = { + enable = true; + lockdPort = cfg.lockdPort; + mountdPort = cfg.mountdPort; + exports = concatMapStringsSep "\n" (exp: + let + clientList = concatMapStringsSep " " (c: "${c.subnet}(${c.options})") exp.clients; + in "${exp.path} ${clientList}" + ) cfg.exports; + }; + + networking.firewall = { + allowedTCPPorts = [ 111 2049 cfg.lockdPort cfg.mountdPort ]; + allowedUDPPorts = [ 111 2049 cfg.lockdPort cfg.mountdPort ]; + }; + }; +} diff --git a/modules/shares/samba.nix b/modules/shares/samba.nix new file mode 100644 index 0000000..fd5e97e --- /dev/null +++ b/modules/shares/samba.nix @@ -0,0 +1,197 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.shares.samba; +in { + options.services.naxos.shares.samba = { + enable = mkEnableOption "NaxOS Declarative Samba (SMB) Service"; + + workgroup = mkOption { + type = types.str; + default = "WORKGROUP"; + description = "NetBIOS workgroup."; + }; + + serverString = mkOption { + type = types.str; + default = "NaxOS Storage Appliance"; + description = "Server announcement string."; + }; + + netbiosName = mkOption { + type = types.str; + default = "naxos"; + description = "NetBIOS hostname."; + }; + + allowedHosts = mkOption { + type = types.listOf types.str; + default = [ "10.0.0." "192.168." "172.16." "127.0.0.1" "localhost" ]; + description = "Allowed IP subnets or hosts for SMB access."; + }; + + shares = mkOption { + type = types.attrsOf (types.submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to publish this share."; + }; + path = mkOption { + type = types.str; + description = "Target local directory or ZFS mountpoint."; + }; + comment = mkOption { + type = types.str; + default = ""; + description = "Share description."; + }; + readOnly = mkOption { + type = types.bool; + default = false; + description = "Whether the share is read-only."; + }; + browseable = mkOption { + type = types.bool; + default = true; + description = "Whether the share is visible in network browsing."; + }; + guestOk = mkOption { + type = types.bool; + default = false; + description = "Whether anonymous/guest access is allowed."; + }; + validUsers = mkOption { + type = types.listOf types.str; + default = []; + description = "List of valid users or @groups."; + }; + forceUser = mkOption { + type = types.nullOr types.str; + default = null; + description = "Force UNIX user for all connections."; + }; + forceGroup = mkOption { + type = types.nullOr types.str; + default = null; + description = "Force UNIX group for all connections."; + }; + createMask = mkOption { + type = types.str; + default = "0664"; + description = "File creation permission mask."; + }; + directoryMask = mkOption { + type = types.str; + default = "0775"; + description = "Directory creation permission mask."; + }; + timeMachine = mkOption { + type = types.bool; + default = false; + description = "Enable Apple Time Machine compatibility mode (vfs_fruit)."; + }; + timeMachineMaxSize = mkOption { + type = types.nullOr types.str; + default = null; + example = "512G"; + description = "Quota size limit advertised to Time Machine."; + }; + }; + }); + default = {}; + description = "Declaratively managed SMB shares."; + }; + }; + + config = mkIf cfg.enable { + services.samba = { + enable = true; + openFirewall = true; + settings = { + global = { + workgroup = cfg.workgroup; + "server string" = cfg.serverString; + "netbios name" = cfg.netbiosName; + security = "user"; + "hosts allow" = concatStringsSep " " cfg.allowedHosts; + "hosts deny" = "0.0.0.0/0"; + "guest account" = "nobody"; + "map to guest" = "bad user"; + + # Apple & ZFS ACL compatibility + "vfs objects" = "catia fruit streams_xattr acl_xattr"; + "fruit:aapl" = "yes"; + "fruit:metadata" = "stream"; + "fruit:model" = "Macmini"; + "fruit:posix_rename" = "yes"; + "fruit:zero_file_id" = "yes"; + + # ACLs and inheritance + "map acl inherit" = "yes"; + "inherit acls" = "yes"; + "ea support" = "yes"; + }; + } // (mapAttrs' (shareName: shareCfg: + nameValuePair shareName ( + { + path = shareCfg.path; + comment = shareCfg.comment; + browseable = if shareCfg.browseable then "yes" else "no"; + "read only" = if shareCfg.readOnly then "yes" else "no"; + "guest ok" = if shareCfg.guestOk then "yes" else "no"; + "create mask" = shareCfg.createMask; + "directory mask" = shareCfg.directoryMask; + } + // optionalAttrs (shareCfg.validUsers != []) { + "valid users" = concatStringsSep " " shareCfg.validUsers; + } + // optionalAttrs (shareCfg.forceUser != null) { + "force user" = shareCfg.forceUser; + } + // optionalAttrs (shareCfg.forceGroup != null) { + "force group" = shareCfg.forceGroup; + } + // optionalAttrs shareCfg.timeMachine { + "vfs objects" = "catia fruit streams_xattr acl_xattr"; + "fruit:time machine" = "yes"; + } + // optionalAttrs (shareCfg.timeMachine && shareCfg.timeMachineMaxSize != null) { + "fruit:time machine max size" = shareCfg.timeMachineMaxSize; + } + ) + ) (filterAttrs (n: v: v.enable) cfg.shares)); + }; + + # Enable Avahi (mDNS / Bonjour) for automatic SMB & Time Machine discovery on macOS/iOS/Windows + services.avahi = { + enable = true; + nssmdns4 = true; + publish = { + enable = true; + userServices = true; + }; + extraServiceFiles = { + smb = '' + + + + %h (NaxOS SMB) + + _smb._tcp + 445 + + + _device-info._tcp + 0 + model=RackMac + + + ''; + }; + }; + }; +} diff --git a/modules/storage/zfs.nix b/modules/storage/zfs.nix new file mode 100644 index 0000000..102b948 --- /dev/null +++ b/modules/storage/zfs.nix @@ -0,0 +1,222 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.storage; +in { + options.services.naxos.storage = { + enable = mkEnableOption "NaxOS Declarative ZFS Storage Engine"; + + arcMaxBytes = mkOption { + type = types.nullOr types.ints.positive; + default = 4294967296; # 4 GiB default + description = "Maximum ZFS ARC cache size in bytes."; + }; + + autoScrub = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable automated regular scrubbing of ZFS pools."; + }; + interval = mkOption { + type = types.str; + default = "monthly"; + description = "Interval for auto-scrub (e.g. monthly, weekly)."; + }; + }; + + autoTrim = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable periodic TRIM for SSD/NVMe vdevs."; + }; + }; + + autoSnapshot = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable automatic snapshot retention policies."; + }; + hourly = mkOption { + type = types.int; + default = 24; + description = "Number of hourly snapshots to keep."; + }; + daily = mkOption { + type = types.int; + default = 7; + description = "Number of daily snapshots to keep."; + }; + weekly = mkOption { + type = types.int; + default = 4; + description = "Number of weekly snapshots to keep."; + }; + monthly = mkOption { + type = types.int; + default = 12; + description = "Number of monthly snapshots to keep."; + }; + }; + + importExistingPools = mkOption { + type = types.listOf types.str; + default = []; + example = [ "tank" "datapool" ]; + description = "List of existing foreign ZFS pools (e.g. from TrueNAS or nixos-lukas) to safely import without formatting."; + }; + + pools = mkOption { + type = types.attrsOf (types.submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to manage this pool."; + }; + ashift = mkOption { + type = types.int; + default = 12; + description = "ZFS vdev ashift alignment value (12 = 4K sectors)."; + }; + layout = mkOption { + type = types.enum [ "stripe" "mirror" "raidz1" "raidz2" "raidz3" ]; + default = "mirror"; + description = "Vdev topology layout."; + }; + devices = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/dev/disk/by-id/nvme-..." "/dev/disk/by-id/ata-..." ]; + description = "Member disk devices or partitions."; + }; + datasets = mkOption { + type = types.attrsOf (types.submodule { + options = { + mountpoint = mkOption { + type = types.nullOr types.str; + default = null; + description = "Mountpoint for this dataset. If null, uses default ZFS mount."; + }; + compression = mkOption { + type = types.enum [ "on" "off" "lz4" "zstd" "zstd-fast" "gzip" ]; + default = "lz4"; + description = "ZFS compression algorithm."; + }; + recordsize = mkOption { + type = types.str; + default = "128K"; + description = "Record size for dataset (e.g. 1M for media, 16K for databases)."; + }; + quota = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional dataset quota (e.g. 500G)."; + }; + reservation = mkOption { + type = types.nullOr types.str; + default = null; + description = "Optional dataset reservation."; + }; + owner = mkOption { + type = types.str; + default = "root"; + description = "POSIX owner of mountpoint."; + }; + group = mkOption { + type = types.str; + default = "root"; + description = "POSIX group of mountpoint."; + }; + mode = mkOption { + type = types.str; + default = "0755"; + description = "POSIX directory mode permissions."; + }; + }; + }); + default = {}; + description = "Sub-datasets belonging to this pool."; + }; + }; + }); + default = {}; + description = "Declaratively defined ZFS pools and datasets."; + }; + }; + + config = mkIf cfg.enable { + boot.supportedFilesystems = [ "zfs" ]; + boot.kernelPackages = pkgs.linuxPackages; + + # Kernel parameter for ZFS ARC Max limit + boot.kernelParams = mkIf (cfg.arcMaxBytes != null) [ + "zfs.zfs_arc_max=${toString cfg.arcMaxBytes}" + ]; + + # Safe pool import list (includes both configured pools and imported existing pools) + boot.zfs.extraPools = unique ( + (attrNames (filterAttrs (n: v: v.enable) cfg.pools)) ++ + cfg.importExistingPools + ); + boot.zfs.forceImportRoot = false; + + # Automated ZFS services + services.zfs = { + autoScrub = { + enable = cfg.autoScrub.enable; + interval = cfg.autoScrub.interval; + }; + trim = { + enable = cfg.autoTrim.enable; + }; + autoSnapshot = { + enable = cfg.autoSnapshot.enable; + hourly = cfg.autoSnapshot.hourly; + daily = cfg.autoSnapshot.daily; + weekly = cfg.autoSnapshot.weekly; + monthly = cfg.autoSnapshot.monthly; + }; + }; + + # System tools for storage diagnosis and disk monitoring + environment.systemPackages = with pkgs; [ + zfs + smartmontools + hdparm + nvme-cli + parted + gptfdisk + iotop + ncdu + sanoid + syncoid + ]; + + # Generate systemd.tmpfiles rules for configured datasets with custom owners/modes + systemd.tmpfiles.rules = flatten (mapAttrsToList (poolName: poolCfg: + mapAttrsToList (dsName: dsCfg: + mkIf (dsCfg.mountpoint != null) + "d ${dsCfg.mountpoint} ${dsCfg.mode} ${dsCfg.owner} ${dsCfg.group} -" + ) poolCfg.datasets + ) cfg.pools); + + # Generate fileSystems definitions for datasets with explicit mountpoints + fileSystems = foldl' (acc: pool: + let + poolName = pool.name; + poolCfg = pool.value; + in acc // (mapAttrs' (dsName: dsCfg: + nameValuePair dsCfg.mountpoint { + device = "${poolName}/${dsName}"; + fsType = "zfs"; + options = [ "nofail" ]; + } + ) (filterAttrs (n: v: v.mountpoint != null) poolCfg.datasets)) + ) {} (mapAttrsToList (name: value: { inherit name value; }) (filterAttrs (n: v: v.enable) cfg.pools)); + }; +} diff --git a/modules/ui/service.nix b/modules/ui/service.nix new file mode 100644 index 0000000..a40258a --- /dev/null +++ b/modules/ui/service.nix @@ -0,0 +1,72 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.naxos.ui; +in { + options.services.naxos.ui = { + enable = mkEnableOption "NaxOS Web Dashboard Service"; + + port = mkOption { + type = types.port; + default = 80; + description = "Web interface listen port."; + }; + + sslPort = mkOption { + type = types.port; + default = 443; + description = "SSL/HTTPS listen port."; + }; + + staticPath = mkOption { + type = types.str; + default = "/opt/naxos/ui/dist"; + description = "Path to compiled React/Vite web dashboard assets."; + }; + }; + + config = mkIf cfg.enable { + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + + virtualHosts."naxos.local" = { + default = true; + listen = [ + { addr = "0.0.0.0"; port = cfg.port; } + { addr = "[::]"; port = cfg.port; } + ]; + + # Serve Frontend SPA + locations."/" = { + root = cfg.staticPath; + tryFiles = "$uri $uri/ /index.html"; + }; + + # Reverse Proxy to NaxOS API & WebSocket / SSE + locations."/api/" = { + proxyPass = "http://127.0.0.1:${toString config.services.naxos.api.port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + ''; + }; + + # Reverse Proxy to Perses Embedded Analytics + locations."/perses/" = { + proxyPass = "http://127.0.0.1:${toString config.services.naxos.perses.port}/"; + proxyWebsockets = true; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ cfg.port cfg.sslPort ]; + }; +} diff --git a/profiles/hardware-generic.nix b/profiles/hardware-generic.nix new file mode 100644 index 0000000..3c9fea7 --- /dev/null +++ b/profiles/hardware-generic.nix @@ -0,0 +1,28 @@ +{ config, pkgs, lib, ... }: + +{ + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "ahci" + "nvme" + "usb_storage" + "sd_mod" + "mpt3sas" + "virtio_pci" + "virtio_scsi" + "virtio_blk" + "virtio_net" + ]; + + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + # Dynamic DHCP on physical interfaces + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/profiles/nas-appliance.nix b/profiles/nas-appliance.nix new file mode 100644 index 0000000..3bcc535 --- /dev/null +++ b/profiles/nas-appliance.nix @@ -0,0 +1,82 @@ +{ config, pkgs, lib, ... }: + +{ + # Core Appliance Settings + services.naxos.core = { + enable = true; + hostName = "naxos"; + timeZone = "Europe/Berlin"; + }; + + # Declarative ZFS Storage Engine + services.naxos.storage = { + enable = true; + arcMaxBytes = 4294967296; # 4GB default + autoScrub.enable = true; + autoTrim.enable = true; + autoSnapshot = { + enable = true; + hourly = 24; + daily = 7; + weekly = 4; + monthly = 12; + }; + }; + + # Network File Shares + services.naxos.shares.samba = { + enable = true; + workgroup = "WORKGROUP"; + serverString = "NaxOS Appliance"; + netbiosName = "naxos"; + }; + + services.naxos.shares.nfs = { + enable = true; + }; + + # Workload Runtime & App Engine + services.naxos.appEngine = { + enable = true; + defaultRuntime = "docker"; + docker.enable = true; + gpuAcceleration.enable = true; + }; + + # Telemetry & Native Perses Analytics + services.naxos.monitoring = { + enable = true; + }; + + services.naxos.perses = { + enable = true; + port = 8080; + }; + + # Management API & Web Dashboard + services.naxos.api = { + enable = true; + port = 8088; + }; + + services.naxos.ui = { + enable = true; + port = 80; + sslPort = 443; + }; + + # GitOps Configuration + services.naxos.gitops = { + enable = true; + autoPushOnCommit = true; + }; + + # Automated Updates + services.naxos.selfUpdate = { + enable = true; + channel = "stable"; + }; + + # Nix System State Version + system.stateVersion = "24.11"; +}