nixos/hosts/FredOS-Mediaserver.nix
rope d2a4e5cebe Mediaserver: cap nix build parallelism and memory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:21:32 +01:00

165 lines
7.3 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 = "12G";
MemoryMax = "16G";
OOMPolicy = "continue"; # a killed build shouldn't stop the daemon
};
# 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
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;
};
};
};
}