182 lines
7.0 KiB
Nix
182 lines
7.0 KiB
Nix
# dropsonde2 analysis machine (dixie)
|
|
#
|
|
# dixie is the fleet's analysis tier: Postgres/Timescale for the ADS-B/AIS buckets
|
|
# + labels + the eventual signal feed, DuckDB-over-Parquet for the 100-300 GB
|
|
# historical scans, and a sentiment encoder. The collectors stay on hyperborea
|
|
# (Pi 5, /srv/dropsonde2); this box just does the heavy lifting.
|
|
#
|
|
# The 5.5 TB SATA HDD (sda, WDC WD6003FZBX, serial V7G47PKH) is mounted by label
|
|
# as /srv/cold — cold Parquet + a local restore copy. /srv/analysis is a bind
|
|
# mount off the NVMe root (hot Parquet). Both are plain fileSystems, not disko:
|
|
# the disko root layout on the PM9A1 is untouched.
|
|
#
|
|
# The HDD wipe is the only destructive step and is authorized for sda only. It is
|
|
# NOT declarative — it runs once as an imperative step during deploy (see below).
|
|
#
|
|
# HDD wipe (authorized for sda = WDC WD6003FZBX-00K5WB0, serial V7G47PKH only):
|
|
# wipefs -a /dev/sda2 /dev/sda1 /dev/sda # partitions first, then the disk
|
|
# printf 'label: gpt\n,,L\n' | sfdisk /dev/sda # GPT + one Linux span
|
|
# udevadm settle
|
|
# mkfs.xfs -L ds2-cold /dev/sda1
|
|
# One GPT partition, XFS, label ds2-cold. sdb (stick) and nvme0n1 (root) untouched.
|
|
#
|
|
# Password: generated on dixie as root, never committed:
|
|
# umask 077; openssl rand -base64 32 > /root/dropsonde-pg
|
|
# The dropsonde-pg-setup service reads it via a systemd LoadCredential. Local
|
|
# analysis uses the unix socket as the dropsonde user (peer auth, no password).
|
|
# The only remote client is the future ADS-B collector on hyperborea; when that
|
|
# is built, copy /root/dropsonde-pg over ssh to
|
|
# /srv/dropsonde2/.env.pg (mode 600). Nobody needs to know the value.
|
|
|
|
{ inputs, pkgs, lib, config, ... }:
|
|
|
|
let
|
|
tailnet = "100.114.166.41"; # dixie's tailscale0
|
|
dropsondeRange = "100.64.0.0/10"; # tailnet CGNAT range
|
|
in
|
|
{
|
|
config = {
|
|
# --- dropsonde system user ------------------------------------------------
|
|
users.users.dropsonde = {
|
|
isSystemUser = true;
|
|
group = "dropsonde";
|
|
home = "/home/dropsonde";
|
|
createHome = true;
|
|
shell = pkgs.bash;
|
|
};
|
|
users.groups.dropsonde = {};
|
|
|
|
# --- /srv/cold (5.5 TB HDD, by label) -------------------------------------
|
|
# The wipe+format of sda happens once during deploy; this entry just mounts
|
|
# by label with nofail so a missing disk never blocks boot.
|
|
# Label is ds2-cold (8 chars; XFS caps labels at 12). Not "dropsonde-cold":
|
|
# hyperborea's NVMe is already labelled "dropsonde", and duplicate labels
|
|
# across hosts is a footgun.
|
|
fileSystems."/srv/cold" = {
|
|
device = "/dev/disk/by-label/ds2-cold";
|
|
fsType = "xfs";
|
|
options = [ "noatime" "nofail" "x-systemd.device-timeout=30s" ];
|
|
};
|
|
|
|
# --- /srv/analysis (hot Parquet on the NVMe root) -------------------------
|
|
# Bind mount off the NVMe so hot Parquet lives on the fast disk, not the
|
|
# spinning HDD. tmpfiles provisions the bind source directory.
|
|
fileSystems."/srv/analysis" = {
|
|
device = "/var/lib/dropsonde-analysis";
|
|
fsType = "none";
|
|
options = [ "bind" "nofail" ];
|
|
};
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/dropsonde-analysis 0755 dropsonde dropsonde"
|
|
];
|
|
|
|
# --- analysis.slice: bounded, best-effort, below llama-server -------------
|
|
# CPUQuota 600% = 6 of 16 threads; MemoryMax 40G leaves ~24G for llama.
|
|
# Launch dropsonde analysis under this slice with:
|
|
# systemd-run --slice=analysis.slice --property=Slice=analysis.slice <cmd>
|
|
systemd.slices.analysis = {
|
|
sliceConfig = {
|
|
CPUQuota = "600%";
|
|
MemoryMax = "40G";
|
|
Nice = 10;
|
|
IOWeight = 50;
|
|
};
|
|
};
|
|
|
|
# --- llama.slice: keep the helper tier above default CPUWeight ------------
|
|
# The router's three resident models (5.6 + 2.5 + 0.7 GB) share the 12 GB
|
|
# card; analysis must not starve them. Default CPUWeight is 1000; 2000 gives
|
|
# it roughly double the fair-share. Wired into the llama-server service below.
|
|
systemd.slices.llama = {
|
|
sliceConfig = {
|
|
CPUWeight = 2000;
|
|
};
|
|
};
|
|
|
|
# --- analysis packages ----------------------------------------------------
|
|
environment.systemPackages = with pkgs; [
|
|
duckdb
|
|
(python3.withPackages (p: with p; [ duckdb pyarrow polars ]))
|
|
];
|
|
|
|
# --- Postgres 17 + Timescale + PostGIS + h3-pg ----------------------------
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_17;
|
|
|
|
# Listen on all interfaces; the firewall opens 5432 on tailscale0 only, so
|
|
# nothing else is exposed. Binding to the tailnet IP directly would race
|
|
# tailscale0 coming up after postgres at boot and leave the service down
|
|
# until a manual restart.
|
|
enableTCPIP = true;
|
|
|
|
settings = {
|
|
listen_addresses = "*";
|
|
shared_preload_libraries = "timescaledb";
|
|
shared_buffers = "8GB";
|
|
work_mem = "256MB";
|
|
maintenance_work_mem = "2GB";
|
|
max_wal_size = "4GB";
|
|
};
|
|
|
|
# Extensions installed into the pg17 package. All three exist for 17.11 in
|
|
# the pinned nixpkgs (verified: timescaledb 2.27.1, postgis 3.6.4, h3-pg
|
|
# 4.2.3).
|
|
extensions = [
|
|
pkgs.postgresql17Packages.timescaledb
|
|
pkgs.postgresql17Packages.postgis
|
|
pkgs.postgresql17Packages."h3-pg"
|
|
];
|
|
|
|
ensureDatabases = [ "dropsonde" ];
|
|
ensureUsers = [
|
|
{
|
|
name = "dropsonde";
|
|
ensureDBOwnership = true; # owns the dropsonde db, so it can CREATE EXTENSION
|
|
}
|
|
];
|
|
|
|
# Allow the dropsonde role to connect over TCP from the tailnet with scram.
|
|
# The default localhost md5 lines stay for local admin access.
|
|
authentication = lib.mkAfter ''
|
|
host dropsonde dropsonde ${dropsondeRange} scram-sha-256
|
|
'';
|
|
};
|
|
|
|
# Create the extensions in the dropsonde db once postgres is up and the db +
|
|
# role exist. Idempotent (IF NOT EXISTS) so redeploys are safe. The password
|
|
# is generated on dixie (see note) and read out of git via a systemd
|
|
# LoadCredential; psql interpolates it as a bound variable so it never shows
|
|
# in the process list or journal.
|
|
systemd.services.dropsonde-pg-setup = {
|
|
description = "dropsonde2: set dropsonde role password + create extensions";
|
|
wants = [ "postgresql-setup.service" ];
|
|
after = [ "postgresql-setup.service" "postgresql.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
Restart = "on-failure";
|
|
RestartSec = "10s";
|
|
LoadCredential = [ "dropsonde-pg-password:/root/dropsonde-pg" ];
|
|
ExecStart = ''${pkgs.coreutils}/bin/bash <<'BASH'
|
|
set -euo pipefail
|
|
pw=$(cat "$CREDENTIALS_DIRECTORY/dropsonde-pg-password")
|
|
psql -v ON_ERROR_STOP=1 -v pw="$pw" -d postgres <<'SQL'
|
|
ALTER ROLE dropsonde WITH LOGIN PASSWORD :'pw';
|
|
SQL
|
|
psql -v ON_ERROR_STOP=1 -d dropsonde <<'SQL'
|
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
CREATE EXTENSION IF NOT EXISTS postgis;
|
|
CREATE EXTENSION IF NOT EXISTS h3;
|
|
CREATE EXTENSION IF NOT EXISTS h3_postgis CASCADE;
|
|
SQL
|
|
BASH
|
|
'';
|
|
};
|
|
wantedBy = [ "multi-user.target" ];
|
|
};
|
|
};
|
|
}
|