deploy-guard: block activation while users are online
- modules/server-deploy-guard.nix: extendable aggregator registered via
services.deployGuard.checks.<name>.{description,command}. Installs
deploy-guard-check with per-check timeout, pass/block reporting, JSON
output, DEPLOY_GUARD_BYPASS / /run/deploy-guard-bypass (single-shot).
- services/jellyfin/jellyfin-deploy-guard.nix: curl+jq on /Sessions,
blocks when any session carries NowPlayingItem; soft-fails when unreachable.
- services/minecraft-deploy-guard.nix: mcstatus SLP query on 25565, blocks
when players.online > 0; soft-fails when unreachable.
- flake.nix: wrap deploy.nodes.muffin activation with activate.custom so
deploy-guard-check runs before switch-to-configuration. Auto-rollback
catches the failure. dryActivate/boot branches preserved.
- deploy.sh: SSH preflight for ./deploy.sh muffin with --force /
DEPLOY_GUARD_FORCE=1 (touches remote bypass marker). Connectivity
failure is soft; activation still enforces.
- tests/deploy-guard.nix: aggregator contract, bypass mechanics, timeout,
JSON output.
This commit is contained in:
@@ -2,5 +2,6 @@
|
||||
imports = [
|
||||
./jellyfin.nix
|
||||
./jellyfin-qbittorrent-monitor.nix
|
||||
./jellyfin-deploy-guard.nix
|
||||
];
|
||||
}
|
||||
|
||||
78
services/jellyfin/jellyfin-deploy-guard.nix
Normal file
78
services/jellyfin/jellyfin-deploy-guard.nix
Normal file
@@ -0,0 +1,78 @@
|
||||
# Deploy guard check for Jellyfin.
|
||||
#
|
||||
# Contract (deploy-guard-check plug-in):
|
||||
# - exit 0: Jellyfin has no active playback sessions (or is unreachable, which
|
||||
# also means no users can be watching).
|
||||
# - exit 1: at least one session is actively playing back media; stdout lists
|
||||
# user / title / client so the operator sees who they'd disrupt.
|
||||
#
|
||||
# A paused session counts as "active" — the user is at the keyboard and will
|
||||
# notice a restart.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
service_configs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
apiKeyPath = config.age.secrets.jellyfin-api-key.path;
|
||||
jellyfinPort = service_configs.ports.private.jellyfin.port;
|
||||
|
||||
check = pkgs.writeShellApplication {
|
||||
name = "deploy-guard-check-jellyfin";
|
||||
runtimeInputs = with pkgs; [
|
||||
curl
|
||||
jq
|
||||
coreutils
|
||||
];
|
||||
text = ''
|
||||
api_key_path=${lib.escapeShellArg apiKeyPath}
|
||||
if [[ ! -r "$api_key_path" ]]; then
|
||||
echo "jellyfin: api key not readable at $api_key_path; skipping" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
key=$(cat "$api_key_path")
|
||||
|
||||
if ! resp=$(curl -sf --max-time 5 \
|
||||
-H "Authorization: MediaBrowser Token=$key" \
|
||||
"http://127.0.0.1:${toString jellyfinPort}/Sessions" 2>/dev/null); then
|
||||
echo "jellyfin: unreachable; assuming safe to deploy" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse defensively — if Jellyfin returns something we can't understand
|
||||
# we prefer allowing the deploy over blocking it (the worst case is we
|
||||
# restart jellyfin while nobody is watching).
|
||||
if ! active=$(printf '%s' "$resp" | jq '[.[] | select(.NowPlayingItem)] | length' 2>/dev/null); then
|
||||
echo "jellyfin: /Sessions response not parsable; assuming safe" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$active" -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Jellyfin: $active active playback session(s):"
|
||||
printf '%s' "$resp" | jq -r '
|
||||
.[]
|
||||
| select(.NowPlayingItem)
|
||||
| " - \(.UserName // "?") \(if (.PlayState.IsPaused // false) then "paused" else "playing" end) \(.NowPlayingItem.Type // "item") \"\(.NowPlayingItem.Name // "?")\" on \(.Client // "?") / \(.DeviceName // "?")"
|
||||
'
|
||||
exit 1
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../../modules/server-deploy-guard.nix
|
||||
];
|
||||
|
||||
config = lib.mkIf config.services.jellyfin.enable {
|
||||
services.deployGuard.checks.jellyfin = {
|
||||
description = "Active Jellyfin playback sessions";
|
||||
command = check;
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user