nixos/hosts/FredOS-Mediaserver.nix
rope d4657c2bf9 Mediaserver: accept Eee tunnel, eee-update
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:13:07 +01:00

200 lines
9.1 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ config, pkgs, lib, ... }:
{
config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {
# Create symlink from home to storage
systemd.tmpfiles.rules = [
"L+ /home/fred/storage - - - - /mnt/storage"
];
# Default max-jobs = 56 × cores = all let a from-source build thrash 31G
# into a 10h livelock, taking routing down with it. Cap parallelism and
# fence builds in a cgroup so the kernel OOM-kills a compiler, not the LAN.
nix.settings.max-jobs = 8;
nix.settings.cores = 8;
nix.daemonCPUSchedPolicy = "batch";
systemd.services.nix-daemon.serviceConfig = {
MemoryHigh = "8G";
MemoryMax = "10G";
OOMPolicy = "continue"; # a killed build shouldn't stop the daemon
};
# System-wide backstop for whatever else runs away. oomd was running but
# monitoring nothing; now it kills the worst cgroup once memory stalls
# hit 80% for 30s (start of a thrash, not 10h in).
systemd.oomd.enableRootSlice = true;
systemd.oomd.enableSystemSlice = true;
# Keep 1G truly free (default ~88M) so kernel networking and the watchdog
# can still allocate while userspace is squeezed.
boot.kernel.sysctl."vm.min_free_kbytes" = 1048576;
# FredOS-Eee dials in and holds localhost:2222 -> its sshd open (see
# hosts/FredOS-Eee.nix). Its key may open exactly that forward, nothing else.
users.users.fred.openssh.authorizedKeys.keys = [
''restrict,port-forwarding,permitlisten="localhost:2222",command="${pkgs.coreutils}/bin/false" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOwVhdOMnFbOu9zS4jjrGyIPE00lJNc0IIKX/u04WwN5 eee-tunnel''
];
# Reap dead sessions in ~3min so a dropped tunnel's port frees up for the
# reconnect (default is TCP keepalive: 2h).
services.openssh.settings.ClientAliveInterval = 60;
services.openssh.settings.ClientAliveCountMax = 3;
programs.ssh.extraConfig = ''
Host eee
HostName localhost
Port 2222
HostKeyAlias FredOS-Eee
StrictHostKeyChecking accept-new
'';
# Basic system packages
environment.systemPackages = with pkgs; [
mergerfs
wget
util-linux
# This host is the router (services/router.nix) but shipped without any
# way to see a packet, so "is that nftables rule actually matching?" was
# unanswerable. Counters tell you a rule fired; these tell you what it
# fired on.
tcpdump
conntrack-tools
javaPackages.compiler.temurin-bin.jre-25
unzip
screen
# The Eee can't evaluate the flake in 1GB: build here, push over its
# tunnel, switch. The out-link keeps the closure from the weekly GC.
(pkgs.writeShellScriptBin "eee-update" ''
set -euo pipefail
sys=$(nix build --refresh --print-out-paths --out-link ~/eee-system \
git+https://forg.gregersen.it/rope/nixos#nixosConfigurations.FredOS-Eee.config.system.build.toplevel)
nix copy --no-check-sigs --to ssh-ng://root@eee "$sys"
ssh root@eee "nix-env -p /nix/var/nix/profiles/system --set $sys && $sys/bin/switch-to-configuration switch"
'')
yt-dlp
# Audible library -> /mnt/storage/torrents/audiobooks for Jellyfin.
# audible-cli pulls the .aaxc + voucher, AAXtoMP3 decrypts to m4b.
audible-cli
aaxtomp3
ghostty.terminfo
usbutils
lm_sensors
(pkgs.writeShellScriptBin "transcode-hevc" ''
export PATH="${pkgs.jellyfin-ffmpeg}/bin:${pkgs.coreutils}/bin:${pkgs.findutils}/bin:${pkgs.gnugrep}/bin:${pkgs.gawk}/bin:${pkgs.bc}/bin:${pkgs.curl}/bin:$PATH"
exec ${pkgs.bash}/bin/bash ${../scripts/transcode-hevc.sh} "$@"
'')
# Stats stream for the quickshell server monitor on the desktops:
# `ssh mediaserver qs-stats` = top's 2s batch stream, interleaved with
# one @STAT line per tick (hottest coretemp across both sockets, WAN
# byte rates from eno1). Everything rides one SSH connection.
(pkgs.writeShellScriptBin "qs-stats" ''
# Single writer: the loop re-emits top's lines itself and injects a
# @STAT line at each frame header. top writing the pipe directly in
# parallel raced the injected lines (pipe writes aren't line-atomic)
# and spliced @STAT mid-frame, where the client parser never saw it.
C=${pkgs.coreutils}/bin
export LC_ALL=C
prev_rx=""
${pkgs.procps}/bin/top -b -d 2 -w 512 | while IFS= read -r line; do
printf '%s\n' "$line"
case $line in
"top - "*)
t=0
for h in /sys/class/hwmon/*; do
[ "$($C/cat "$h/name" 2>/dev/null)" = coretemp ] || continue
for f in "$h"/temp*_input; do
v=$($C/cat "$f" 2>/dev/null || echo 0)
[ "$v" -gt "$t" ] && t=$v
done
done
read -r rx tx < <(${pkgs.gawk}/bin/awk '$1 == "eno1:" {print $2, $10}' /proc/net/dev)
if [ -n "$prev_rx" ]; then
printf '@STAT temp=%s rxbps=%s txbps=%s\n' "$((t / 1000))" "$(( (rx - prev_rx) / 2 ))" "$(( (tx - prev_tx) / 2 ))"
fi
prev_rx=$rx
prev_tx=$tx
;;
esac
done
'')
# Instant-answer backend for the quickshell launcher on the desktops:
# `printf '%s' "question" | ssh mediaserver qs-ask`.
#
# Runs through Codex CLI using fred's ChatGPT subscription. Sign in on
# the server as fred with `codex login` once; no API key is needed.
# The launcher shows Wikipedia first and replaces it when this answers.
#
# Reads the question from stdin — passing it as an ssh argv element would
# send it through the remote shell for a second round of word splitting.
#
# Luna at low reasoning effort is enough for a short fact. The CLI runs
# read-only, without web search, shell access, user config, exec rules,
# or session persistence.
#
# STDOUT is the launcher's channel: the answer, or nothing at all if the
# call fails or the model doesn't know — silence means "keep Wikipedia".
# Reasons go to STDERR and the journal (`journalctl -t qs-ask`).
(pkgs.writeShellScriptBin "qs-ask" ''
# Every line carries this script's pid, because the launcher fires a
# fresh call per debounce and concurrent runs otherwise interleave with
# no way to pair a question with its outcome. The pid goes in the
# message text rather than through logger's --id: --id sets SYSLOG_PID,
# which journalctl does not display — it shows the pid of the logger
# process itself, a different number on every line.
log() {
printf 'qs-ask: %s\n' "$*" >&2
${pkgs.util-linux}/bin/logger -t qs-ask -- "[$$] $*"
}
q=$(${pkgs.coreutils}/bin/cat)
[ -z "$q" ] && { log "no question on stdin"; exit 0; }
log "ask: $q"
# Keep repository instructions and project files out of this invocation.
work="$HOME/.cache/qs-ask"
${pkgs.coreutils}/bin/mkdir -p "$work" || { log "cannot create $work"; exit 0; }
cd "$work" || { log "cannot enter $work"; exit 0; }
ans=$(printf 'Answer the question in one or two short sentences, under 240 characters. Lead with the specific fact asked for, including units. The query is often bare keywords rather than a question ("shadow hunter knife classic wow"); treat that as a request for the key facts about that thing. No preamble, caveats, markdown, or follow-up offers. Do not use tools. Reply with exactly UNKNOWN only when you genuinely do not know, or the answer depends on live data you do not have.\n\nQuestion: %s\n' "$q" | \
${pkgs.coreutils}/bin/timeout 30s ${pkgs.codex}/bin/codex exec \
--model gpt-5.6-luna \
--config model_reasoning_effort=low \
--config web_search=disabled \
--disable shell_tool \
--sandbox read-only \
--skip-git-repo-check \
--ephemeral \
--ignore-user-config \
--ignore-rules \
- \
2>/dev/null) \
|| { log "codex exited non-zero (check 'codex login status' as fred)"; exit 0; }
ans=$(printf '%s' "$ans" | ${pkgs.coreutils}/bin/tr -d '\r' | ${pkgs.gnused}/bin/sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
case "$ans" in
"") log "empty answer"; exit 0 ;;
UNKNOWN*) log "model replied UNKNOWN"; exit 0 ;;
esac
log "ok: $ans"
printf '%s' "$ans"
'')
];
# Basic networking
networking.useDHCP = lib.mkForce false;
# Allow fred to act as a remote Nix builder (trusted users can import
# unsigned store paths sent by the build client).
nix.settings.trusted-users = [ "root" "fred" ];
# services/deployment.nix owns the 05:15 update and conditional reboot,
# using the same health-checked transaction as manual and router UI applies.
# WAN exposure is controlled by nftables in services/router.nix +
# ports.toml (networking.firewall is disabled on this host).
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "no";
PasswordAuthentication = false;
};
};
};
}