fedora-csb-system-manager
1{
2 libx,
3 globals,
4 lib,
5 pkgs,
6 config,
7 ...
8}:
9let
10 # Service defaults for media/homelab services
11 serviceDefaults = libx.mkServiceDefaults { };
12
13 # Common rsync configuration for rhea backups
14 rheaBackupDefaults = {
15 source = {
16 host = "rhea.sbr.pm";
17 user = "vincent";
18 };
19 destination = "/neo";
20 delete = true; # Mirror mode: delete files in destination that don't exist in source
21 user = "vincent";
22 group = "users";
23 rsyncArgs = [
24 "--exclude=.Trash-*"
25 "--exclude=lost+found"
26 ];
27 sshArgs = [
28 "-o StrictHostKeyChecking=accept-new"
29 ];
30 };
31
32 # Exportarr services configuration (data-driven approach)
33 exportarrServices = {
34 lidarr = {
35 port = 9709;
36 servicePort = 8686;
37 };
38 };
39in
40{
41 imports = [
42 ../common/services/samba.nix
43 ../common/services/homepage.nix
44 ../common/services/prometheus-exporters-node.nix
45 ../../modules/audible-sync
46 ../../modules/music-playlist-dl
47 ../../modules/harmonia
48 ../../modules/xmpp-research-bot
49 ./xmpp.nix
50 ];
51
52 users.users.vincent.linger = true;
53
54 # Age secrets for homepage widgets (API keys for *arr services on rhea)
55 age.secrets = {
56 "exportarr-sonarr-apikey" = {
57 file = ../../secrets/rhea/exportarr-sonarr-apikey.age;
58 mode = "440";
59 group = "homepage";
60 };
61 "exportarr-radarr-apikey" = {
62 file = ../../secrets/rhea/exportarr-radarr-apikey.age;
63 mode = "440";
64 group = "homepage";
65 };
66 "exportarr-lidarr-apikey" = {
67 file = ../../secrets/rhea/exportarr-lidarr-apikey.age;
68 mode = "440";
69 group = "homepage";
70 };
71 "restic-aix-password" = {
72 file = ../../secrets/aion/restic-aix-password.age;
73 mode = "400";
74 owner = "vincent";
75 group = "users";
76 };
77 "ntfy-token" = {
78 file = ../../secrets/sakhalin/ntfy-token.age;
79 mode = "400";
80 owner = "vincent";
81 group = "users";
82 };
83 "harmonia-aion-signing-key" = {
84 file = ../../secrets/harmonia/aion-signing-key.age;
85 mode = "440";
86 owner = "root";
87 group = "root";
88 };
89 # TODO: Uncomment after creating secrets with agenix
90 # "xmpp-research-bot-password" = {
91 # file = ../../secrets/aion/xmpp-research-bot-password.age;
92 # mode = "400";
93 # owner = "vincent";
94 # group = "users";
95 # };
96 # "anthropic-api-key" = {
97 # file = ../../secrets/aion/anthropic-api-key.age;
98 # mode = "400";
99 # owner = "vincent";
100 # group = "users";
101 # };
102 };
103
104 services = {
105 # Binary cache server (aarch64-linux)
106 harmonia-cache = {
107 enable = true;
108 signKeyPath = config.age.secrets."harmonia-aion-signing-key".path;
109 port = 5000;
110 workers = 4;
111 priority = 30;
112
113 # Nightly cache pre-population
114 builder = {
115 enable = true;
116 systems = [
117 "aion" # Self
118 "athena" # RPi4
119 "demeter" # RPi4
120 "aix" # RPi4
121 "rhea" # Media server
122 ];
123 schedule = "02:30"; # 2:30 AM daily (offset from aomi)
124 notification = {
125 enable = true;
126 tokenFile = config.age.secrets."ntfy-token".path;
127 };
128 };
129 };
130
131 wireguard = {
132 enable = true;
133 ips = libx.wg-ips globals.machines.aion.net.vpn.ips;
134 endpoint = "${globals.net.vpn.endpoint}";
135 endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
136 };
137
138 audible-sync = {
139 enable = true; # enable one migration dayrs
140 user = "vincent";
141 outputDir = "/neo/audiobooks";
142 tempDir = "/neo/audiobooks/zz_import"; # Keep AAX files for reuse
143 quality = "best";
144 format = "m4b";
145 schedule = "daily"; # Run daily at 3 AM
146 notification = {
147 enable = true;
148 ntfyUrl = "https://ntfy.sbr.pm";
149 topic = "homelab";
150 tokenFile = config.age.secrets."ntfy-token".path;
151 };
152 };
153
154 audiobookshelf = serviceDefaults // {
155 enable = true;
156 port = 13378;
157 host = "0.0.0.0";
158 };
159
160 lidarr = serviceDefaults // {
161 enable = true;
162 settings.server.port = exportarrServices.lidarr.servicePort;
163 };
164
165 rsync-replica = {
166 enable = true;
167 jobs = {
168 rhea-daily = rheaBackupDefaults // {
169 source = rheaBackupDefaults.source // {
170 paths = [
171 "/neo/documents"
172 "/neo/ebooks"
173 ];
174 };
175 schedule = "daily";
176 };
177 rhea-hourly = rheaBackupDefaults // {
178 source = rheaBackupDefaults.source // {
179 paths = [
180 "/neo/pictures"
181 "/neo/videos"
182 ];
183 };
184 schedule = "hourly";
185 };
186 };
187 };
188
189 # Restic backup to aix (off-site backup)
190 # Note: Photos are already rsync'd to aix daily via aix's pull job
191 # This backup focuses on critical versioned data only
192 restic.backups.aix-critical = {
193 user = "vincent";
194 repository = "sftp:vincent@aix.sbr.pm:/data/backup/restic/aion";
195
196 # Use password-based encryption
197 passwordFile = config.age.secrets."restic-aix-password".path;
198
199 paths = [
200 "/neo/pictures/photos/backups" # Immich database dumps only (~100MB, versioned)
201 "/home/vincent/desktop/org" # Org files (<1GB)
202 "/neo/documents" # Personal docs rsynced from rhea (~113GB)
203 "/var/lib/lidarr" # Lidarr database and config (~4.6GB)
204 "/var/lib/audiobookshelf" # Audiobookshelf database and config (~30MB)
205 ];
206
207 # Backup schedule - weekly for large dataset
208 timerConfig = {
209 OnCalendar = "weekly";
210 Persistent = true;
211 RandomizedDelaySec = "1h"; # Avoid VPN congestion
212 };
213
214 # Retention policy
215 pruneOpts = [
216 "--keep-daily 7" # Last 7 days
217 "--keep-weekly 4" # Last 4 weeks
218 "--keep-monthly 12" # Last 12 months
219 "--keep-yearly 3" # Last 3 years
220 ];
221
222 # Backup options
223 extraBackupArgs = [
224 "--exclude-caches"
225 "--exclude='*.Trash-*'"
226 "--exclude='lost+found'"
227 "--exclude='.sync-conflict-*'" # Syncthing conflicts
228 "--verbose"
229 ];
230
231 # Check repository integrity after backup
232 checkOpts = [
233 "--read-data-subset=5%" # Verify 5% of data each run
234 ];
235
236 # Backup monitoring with ntfy.sh
237 backupPrepareCommand = ''
238 ${pkgs.curl}/bin/curl \
239 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
240 config.age.secrets."ntfy-token".path
241 })" \
242 -H "Title: Restic Backup Starting (aion)" \
243 -d "Starting backup to aix (critical data only)" \
244 https://ntfy.sbr.pm/backups
245 '';
246
247 backupCleanupCommand = ''
248 ${pkgs.curl}/bin/curl \
249 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
250 config.age.secrets."ntfy-token".path
251 })" \
252 -H "Title: Restic Backup Complete (aion)" \
253 -H "Tags: white_check_mark" \
254 -d "Backup to aix completed successfully" \
255 https://ntfy.sbr.pm/backups || \
256 ${pkgs.curl}/bin/curl \
257 -H "Authorization: Bearer $(${pkgs.coreutils}/bin/tr -d '\n' < ${
258 config.age.secrets."ntfy-token".path
259 })" \
260 -H "Title: Restic Backup Failed (aion)" \
261 -H "Tags: x,warning" \
262 -H "Priority: high" \
263 -d "Backup to aix failed! Check logs: journalctl -u restic-backups-aix-critical.service" \
264 https://ntfy.sbr.pm/backups
265 '';
266 };
267
268 # Prometheus exporter for restic backup monitoring
269 # DISABLED: Causes excessive load (restic check every 60s over SFTP)
270 # TODO: Re-enable with local repository or periodic timer-based checks
271 prometheus.exporters.restic = {
272 enable = false;
273 port = 9753;
274 user = "vincent"; # Must run as vincent to access SSH keys for aix
275 group = "users";
276 repository = "sftp:vincent@aix.sbr.pm:/data/backup/restic/aion";
277 passwordFile = config.age.secrets."restic-aix-password".path;
278 };
279
280 music-playlist-dl = {
281 enable = true; # Enable on music migration day
282 user = "vincent";
283 configFile = "/neo/music/music-playlist-dl.yaml";
284 baseDir = "/neo/music/mixes"; # Downloads to /neo/music/mixes/{show}, playlists to /neo/music/playlists
285 schedule = "weekly"; # Run weekly on Sundays at 2 AM
286 notification = {
287 enable = true;
288 ntfyUrl = "https://ntfy.sbr.pm";
289 topic = "homelab";
290 tokenFile = config.age.secrets."ntfy-token".path;
291 };
292 };
293
294 # XMPP Research Bot (disabled until secrets are created)
295 xmpp-research-bot = {
296 enable = false; # TODO: Enable after creating secrets with agenix
297 # jid = "researchbot@xmpp.sbr.pm";
298 # ownerJid = "vincent@xmpp.sbr.pm";
299 # passwordFile = config.age.secrets."xmpp-research-bot-password".path;
300 # apiKeyFile = config.age.secrets."anthropic-api-key".path;
301 # inboxPath = "/home/vincent/desktop/org/inbox.org";
302 # user = "vincent";
303 # group = "users";
304 };
305
306 navidrome = {
307 enable = true;
308 settings = {
309 MusicFolder = "/neo/music";
310 Address = "0.0.0.0";
311 Port = 4533;
312 BaseURL = "https://music.sbr.pm";
313
314 # Paths
315 DataFolder = "/var/lib/navidrome";
316 CacheFolder = "/var/cache/navidrome";
317
318 # Features
319 EnableTranscodingConfig = false; # Disabled for security - transcoding still works, UI editing disabled
320 EnableSubsonic = true;
321
322 # Optional: Scrobbling (can enable later)
323 # LastFM.Enabled = true;
324 };
325 };
326
327 transmission = serviceDefaults // {
328 enable = true; # Enable on music migration day
329 package = pkgs.transmission_4;
330 openRPCPort = true; # Open firewall for RPC (port 9091)
331 home = "/neo/torrents";
332 settings = {
333 # Override default settings
334 incomplete-dir-enabled = true;
335 rpc-bind-address = "0.0.0.0"; # Bind to all interfaces
336 rpc-host-whitelist = "localhost,tm.sbr.pm,transmission-music.sbr.pm,aion.home,aion.vpn,aion.sbr.pm,192.168.1.51,10.100.0.51";
337 rpc-host-whitelist-enabled = true;
338 rpc-whitelist-enabled = true;
339 rpc-whitelist = "127.0.0.1,192.168.1.*,10.100.0.*"; # Allow local network access
340 rpc-username = "transmission";
341 rpc-password = "transmission";
342 download-queue-enabled = true;
343 download-queue-size = 15;
344 queue-stalled-enabled = true;
345 queue-stalled-minutes = 30;
346 ratio-limit = 0.1;
347 ratio-limit-enabled = true;
348 };
349 };
350
351 # Samba shares for music and audiobooks
352 samba.settings = {
353 global."server string" = "Aion";
354 music = libx.mkSambaShare {
355 name = "music";
356 path = "/neo/music";
357 };
358 audiobooks = libx.mkSambaShare {
359 name = "audiobooks";
360 path = "/neo/audiobooks";
361 };
362 };
363
364 # NFS server for music and audiobooks
365 nfs.server = {
366 enable = true;
367 # Fixed ports for firewall configuration
368 lockdPort = 4001;
369 mountdPort = 4002;
370 statdPort = 4000;
371 exports = ''
372 /neo/music 192.168.1.0/24(rw,fsid=0,no_subtree_check) 10.100.0.0/24(rw,fsid=0,no_subtree_check)
373 /neo/audiobooks 192.168.1.0/24(rw,fsid=1,no_subtree_check) 10.100.0.0/24(rw,fsid=1,no_subtree_check)
374 '';
375 };
376 };
377
378 # Override prometheus-restic-exporter service to disable DynamicUser
379 # This is needed so the service runs as vincent and can access SSH keys
380 # DISABLED: Service is currently disabled due to excessive load
381 # systemd.services.prometheus-restic-exporter.serviceConfig = {
382 # DynamicUser = lib.mkForce false;
383 # User = lib.mkForce "vincent";
384 # Group = lib.mkForce "users";
385 # ProtectHome = lib.mkForce false; # Disable home protection to allow SSH control sockets
386 # RestrictAddressFamilies = lib.mkForce [
387 # "AF_UNIX"
388 # "AF_INET"
389 # "AF_INET6"
390 # ]; # Allow all network families for SSH
391 # };
392
393 networking = {
394 useDHCP = lib.mkDefault true;
395 firewall = {
396 allowedTCPPorts = [
397 3001 # Homepage dashboard
398 4533 # Navidrome
399 13378 # Audiobookshelf
400 8686 # Lidarr
401 9000 # Node exporter
402 9709 # Lidarr exportarr (prometheus)
403 # 9753 # Restic exporter (prometheus) - DISABLED
404 9091 # Transmission (music torrents)
405 # NFS ports
406 111 # rpcbind
407 2049 # NFS daemon
408 4000 # statd
409 4001 # lockd
410 4002 # mountd
411 20048 # mountd (NFSv4)
412 ];
413 allowedUDPPorts = [
414 # NFS ports
415 111 # rpcbind
416 2049 # NFS daemon
417 4000 # statd
418 4001 # lockd
419 4002 # mountd
420 20048 # mountd (NFSv4)
421 ];
422 };
423 };
424
425 environment.systemPackages = with pkgs; [
426 lm_sensors
427 gnumake
428 audible-converter
429 audible-cli
430 ffmpeg-full
431 ];
432
433}