feat(core): initialize NaxOS declarative NixOS distribution, modules, and ISO installer
Test NaxOS Module Configurations / test-modules (push) Failing after 6m10s
Test NaxOS Module Configurations / test-modules (push) Failing after 6m10s
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,58 @@
|
||||
# naxos-os
|
||||
# NaxOS Core (`naxos-os`)
|
||||
|
||||
NaxOS Operating System - Declarative NixOS Core, Modules, ISO Installer, and Appliance Architecture
|
||||
[](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
|
||||
```
|
||||
@@ -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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# Minimal Live ISO base
|
||||
imports = [
|
||||
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
|
||||
];
|
||||
|
||||
# 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";
|
||||
}
|
||||
Executable
+193
@@ -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
|
||||
@@ -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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./immich.nix
|
||||
./nextcloud.nix
|
||||
./jellyfin.nix
|
||||
./paperless.nix
|
||||
./vaultwarden.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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 = ''
|
||||
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
||||
<service-group>
|
||||
<name replace-wildcards="yes">%h (NaxOS SMB)</name>
|
||||
<service>
|
||||
<type>_smb._tcp</type>
|
||||
<port>445</port>
|
||||
</service>
|
||||
<service>
|
||||
<type>_device-info._tcp</type>
|
||||
<port>0</port>
|
||||
<txt-record>model=RackMac</txt-record>
|
||||
</service>
|
||||
</service-group>
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user