diff --git a/hosts/nixserve/default.nix b/hosts/nixserve/default.nix index 9de8653..84f8215 100644 --- a/hosts/nixserve/default.nix +++ b/hosts/nixserve/default.nix @@ -1,7 +1,7 @@ { config, pkgs, ... }: { - imports = [ ./hardware-configuration.nix ./services.nix ]; + imports = [ ./hardware-configuration.nix ./services.nix ./health.nix ]; boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; diff --git a/hosts/nixserve/health.nix b/hosts/nixserve/health.nix new file mode 100644 index 0000000..5b9e889 --- /dev/null +++ b/hosts/nixserve/health.nix @@ -0,0 +1,83 @@ +{ pkgs, ... }: + +let + healthScript = pkgs.writeShellScriptBin "nixserve-health" '' + R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' B='\033[0;34m' W='\033[1m' N='\033[0m' + ok() { echo -e " ''${G}✓''${N} $*"; } + warn() { echo -e " ''${Y}⚠''${N} $*"; } + bad() { echo -e " ''${R}✗''${N} $*"; } + section(){ echo -e "\n''${W}''${B}$*''${N}"; } + + echo -e "''${W}nixserve''${N} — $(date '+%Y-%m-%d %H:%M') uptime: $(uptime -p | sed 's/up //') load: $(cut -d' ' -f1-3 /proc/loadavg)" + + # ── ZFS ──────────────────────────────────────────────────────────────────── + section "ZFS" + while IFS= read -r pool; do + health=$(zpool list -H -o health "$pool" 2>/dev/null) + errors=$(zpool status "$pool" 2>/dev/null | awk '/errors:/{$1=""; print substr($0,2)}') + scrub=$(zpool status "$pool" 2>/dev/null | awk '/scan:/{$1=""; print substr($0,2)}' | head -1) + if [[ "$health" == "ONLINE" ]]; then + ok "$pool $health — $errors" + else + bad "$pool $health — $errors" + fi + [[ -n "$scrub" ]] && echo -e " scrub: $scrub" + done < <(zpool list -H -o name 2>/dev/null) + + # ── Systemd ───────────────────────────────────────────────────────────────── + section "Systemd" + failed=$(systemctl --failed --no-legend --no-pager 2>/dev/null | awk '{print $1}') + if [[ -z "$failed" ]]; then + ok "No failed units" + else + while IFS= read -r u; do bad "$u"; done <<< "$failed" + fi + + # ── Docker ────────────────────────────────────────────────────────────────── + section "Docker" + if docker info &>/dev/null 2>&1; then + while IFS=$'\t' read -r name status; do + [[ -z "$name" ]] && continue + if [[ "$status" == Up* ]]; then ok "$name ($status)" + else bad "$name ($status)"; fi + done < <(docker ps -a --format $'{{.Names}}\t{{.Status}}' 2>/dev/null) + else + warn "docker not reachable" + fi + + # ── Disk ──────────────────────────────────────────────────────────────────── + section "Disk" + df -h --output=target,pcent,used,size -x tmpfs -x devtmpfs -x efivarfs 2>/dev/null \ + | tail -n +2 \ + | while read -r mnt pct used size; do + n=''${pct/\%/} + if (( n >= 90 )); then bad "$mnt $pct ($used / $size)" + elif (( n >= 75 )); then warn "$mnt $pct ($used / $size)" + else ok "$mnt $pct ($used / $size)" + fi + done + + # ── Memory ────────────────────────────────────────────────────────────────── + section "Memory" + free -h | awk '/^Mem:/{printf " used: %s / %s free: %s\n", $3, $2, $4}' + + # ── Recent errors ─────────────────────────────────────────────────────────── + section "Journal errors (last hour)" + errs=$(journalctl -p err --since "1 hour ago" --no-pager -q 2>/dev/null \ + | grep -v "^$" | tail -8) + if [[ -z "$errs" ]]; then + ok "None" + else + while IFS= read -r line; do warn "$line"; done <<< "$errs" + fi + + echo + ''; +in +{ + environment.systemPackages = [ healthScript ]; + + programs.zsh.loginShellInit = '' + [[ -n "$SSH_CONNECTION" ]] && nixserve-health + ''; +}