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,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 ];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user