cleanup
Some checks failed
Test / tests (push) Failing after 2m13s
/ OpenTofu (push) Successful in 13s

This commit is contained in:
plasmagoat 2025-07-28 12:08:32 +02:00
parent bcbcc8b17b
commit 3362c47211
29 changed files with 44 additions and 2138 deletions

View file

@ -1,187 +0,0 @@
# modules/backup-manager.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.homelab.backups;
globalCfg = config.homelab.global;
# Create systemd services for backup jobs
createBackupService = job: let
serviceName = "backup-${job.name}";
allExcludes = globalCfg.backups.globalExcludes ++ job.excludePatterns;
excludeArgs = map (pattern: "--exclude '${pattern}'") allExcludes;
backupScript =
if job.backend == "restic"
then ''
#!/bin/bash
set -euo pipefail
${optionalString (job.preHook != null) job.preHook}
# Restic backup
${pkgs.restic}/bin/restic backup \
${concatStringsSep " " (map (path: "'${path}'") job.paths)} \
${concatStringsSep " " excludeArgs} \
--tag "host:${globalCfg.hostname}" \
--tag "job:${job.name}" \
--tag "env:${globalCfg.environment}"
# Apply retention policy
${pkgs.restic}/bin/restic forget \
--keep-daily ${job.retention.daily} \
--keep-weekly ${job.retention.weekly} \
--keep-monthly ${job.retention.monthly} \
--keep-yearly ${job.retention.yearly} \
--prune
${optionalString (job.postHook != null) job.postHook}
''
else if job.backend == "borg"
then ''
#!/bin/bash
set -euo pipefail
${optionalString (job.preHook != null) job.preHook}
# Borg backup
${pkgs.borgbackup}/bin/borg create \
--stats --progress \
${concatStringsSep " " excludeArgs} \
"::${globalCfg.hostname}-${job.name}-{now}" \
${concatStringsSep " " (map (path: "'${path}'") job.paths)}
# Apply retention policy
${pkgs.borgbackup}/bin/borg prune \
--keep-daily ${job.retention.daily} \
--keep-weekly ${job.retention.weekly} \
--keep-monthly ${job.retention.monthly} \
--keep-yearly ${job.retention.yearly}
${optionalString (job.postHook != null) job.postHook}
''
else throw "Unsupported backup backend: ${job.backend}";
in {
${serviceName} = {
description = "Backup job: ${job.name}";
after = ["network-online.target"];
wants = ["network-online.target"];
serviceConfig = {
Type = "oneshot";
User = "backup";
Group = "backup";
ExecStart = pkgs.writeScript "backup-${job.name}" backupScript;
EnvironmentFile = "/etc/backup/environment";
};
};
};
# Create systemd timers for backup jobs
createBackupTimer = job: let
serviceName = "backup-${job.name}";
timerName = "${serviceName}.timer";
in {
${timerName} = {
description = "Timer for backup job: ${job.name}";
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar =
if job.schedule == "daily"
then "daily"
else if job.schedule == "weekly"
then "weekly"
else if job.schedule == "hourly"
then "hourly"
else job.schedule; # Assume it's a cron expression
Persistent = true;
RandomizedDelaySec = "15min";
};
};
};
in {
options.homelab.backups = {
enable = mkEnableOption "Backup management";
restic = {
repository = mkOption {
type = types.str;
description = "Restic repository URL";
};
passwordFile = mkOption {
type = types.str;
default = "/etc/backup/restic-password";
description = "Path to file containing restic password";
};
};
borg = {
repository = mkOption {
type = types.str;
description = "Borg repository path";
};
sshKey = mkOption {
type = types.str;
default = "/etc/backup/borg-ssh-key";
description = "Path to SSH key for borg repository";
};
};
};
config = mkIf (cfg.enable && globalCfg.enable && (length globalCfg.backups.jobs) > 0) {
# Create backup user
users.users.backup = {
isSystemUser = true;
group = "backup";
home = "/var/lib/backup";
createHome = true;
};
users.groups.backup = {};
# Install backup tools
environment.systemPackages = with pkgs; [
restic
borgbackup
rclone
(pkgs.writeScriptBin "backup-status" ''
#!/bin/bash
echo "=== Backup Status ==="
echo
${concatStringsSep "\n" (map (job: ''
echo "Job: ${job.name}"
systemctl is-active backup-${job.name}.timer || echo "Timer inactive"
systemctl status backup-${job.name}.timer --no-pager -l | grep -E "(Active|Trigger)" || true
echo
'')
globalCfg.backups.jobs)}
'')
];
# Create systemd services and timers
systemd.services = lib.foldl' (acc: job: acc // (createBackupService job)) {} globalCfg.backups.jobs;
systemd.timers = lib.foldl' (acc: job: acc // (createBackupTimer job)) {} globalCfg.backups.jobs;
# Environment file template
environment.etc."backup/environment.example".text = ''
# Restic configuration
RESTIC_REPOSITORY=${cfg.restic.repository}
RESTIC_PASSWORD_FILE=${cfg.restic.passwordFile}
# AWS S3 credentials (if using S3 backend)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# Borg configuration
BORG_REPO=${cfg.borg.repository}
BORG_RSH="ssh -i ${cfg.borg.sshKey}"
# Notification settings
NOTIFICATION_URL=your-webhook-url
'';
};
}

View file

@ -1,8 +1,3 @@
{
ente = import ./ente.nix;
global-config = import ./global-config.nix;
backup-manager = import ./backup-manager.nix;
# Service modules
services = import ./services;
}

View file

@ -1,462 +0,0 @@
# modules/global-config.nix
{
config,
lib,
outputs,
...
}:
with lib; let
cfg = config.homelab.global;
# Service type definition
serviceType = types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable this service";
};
description = mkOption {
type = types.str;
description = "Human-readable description of the service";
};
category = mkOption {
type = types.enum ["monitoring" "networking" "storage" "security" "media" "development" "backup" "other"];
default = "other";
description = "Service category for organization";
};
dependencies = mkOption {
type = types.listOf types.str;
default = [];
description = "List of other homelab services this depends on";
};
ports = mkOption {
type = types.listOf types.port;
default = [];
description = "Ports this service uses";
};
tags = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional tags for this service";
};
priority = mkOption {
type = types.int;
default = 100;
description = "Service priority (lower numbers start first)";
};
};
};
# Type definitions
monitoringEndpointType = types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Name of the monitoring endpoint";
};
port = mkOption {
type = types.port;
description = "Port number for the endpoint";
};
path = mkOption {
type = types.str;
default = "/metrics";
description = "Path for the metrics endpoint";
};
jobName = mkOption {
type = types.str;
description = "Prometheus job name";
};
scrapeInterval = mkOption {
type = types.str;
default = "30s";
description = "Prometheus scrape interval";
};
labels = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Additional labels for this endpoint";
};
};
};
backupJobType = types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Name of the backup job";
};
backend = mkOption {
type = types.enum ["restic" "borg" "rclone"];
description = "Backup backend to use";
};
paths = mkOption {
type = types.listOf types.str;
description = "List of paths to backup";
};
schedule = mkOption {
type = types.str;
default = "daily";
description = "Backup schedule (cron format or preset)";
};
retention = mkOption {
type = types.attrsOf types.str;
default = {
daily = "7";
weekly = "4";
monthly = "6";
yearly = "2";
};
description = "Retention policy";
};
excludePatterns = mkOption {
type = types.listOf types.str;
default = [];
description = "Patterns to exclude from backup";
};
preHook = mkOption {
type = types.nullOr types.str;
default = null;
description = "Script to run before backup";
};
postHook = mkOption {
type = types.nullOr types.str;
default = null;
description = "Script to run after backup";
};
};
};
reverseProxyEntryType = types.submodule {
options = {
subdomain = mkOption {
type = types.str;
description = "Subdomain for the service";
};
port = mkOption {
type = types.port;
description = "Internal port to proxy to";
};
path = mkOption {
type = types.str;
default = "/";
description = "Path prefix for the service";
};
enableAuth = mkOption {
type = types.bool;
default = false;
description = "Enable authentication for this service";
};
enableSSL = mkOption {
type = types.bool;
default = true;
description = "Enable SSL for this service";
};
customHeaders = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Custom headers to add";
};
websockets = mkOption {
type = types.bool;
default = false;
description = "Enable websocket support";
};
};
};
# Helper functions for services
enabledServices = filterAttrs (name: service: service.enable) cfg.services;
servicesByCategory = category: filterAttrs (name: service: service.enable && service.category == category) cfg.services;
in {
imports = [
./motd
];
options.homelab.global = {
enable = mkEnableOption "Global homelab configuration";
hostname = mkOption {
type = types.str;
description = "Hostname for this system";
};
domain = mkOption {
type = types.str;
default = "procopius.dk";
description = "Base domain for the homelab";
};
environment = mkOption {
type = types.enum ["production" "staging" "development"];
default = "production";
description = "Environment type";
};
location = mkOption {
type = types.str;
default = "homelab";
description = "Physical location identifier";
};
tags = mkOption {
type = types.listOf types.str;
default = [];
description = "Tags for this system";
};
services = mkOption {
type = types.attrsOf serviceType;
default = {};
description = "Homelab services configuration";
example = literalExpression ''
{
prometheus = {
enable = true;
description = "Metrics collection and monitoring";
category = "monitoring";
ports = [ 9090 ];
tags = [ "metrics" "alerting" ];
};
traefik = {
enable = true;
description = "Reverse proxy and load balancer";
category = "networking";
ports = [ 80 443 8080 ];
tags = [ "proxy" "loadbalancer" ];
priority = 10;
};
}
'';
};
monitoring = {
endpoints = mkOption {
type = types.listOf monitoringEndpointType;
default = [];
description = "Monitoring endpoints exposed by this system";
};
nodeExporter = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable node exporter";
};
port = mkOption {
type = types.port;
default = 9100;
description = "Node exporter port";
};
};
};
backups = {
jobs = mkOption {
type = types.listOf backupJobType;
default = [];
description = "Backup jobs for this system";
};
globalExcludes = mkOption {
type = types.listOf types.str;
default = [
"*.tmp"
"*.cache"
"*/.git"
"*/node_modules"
"*/target"
];
description = "Global exclude patterns for all backup jobs";
};
};
reverseProxy = {
entries = mkOption {
type = types.listOf reverseProxyEntryType;
default = [];
description = "Reverse proxy entries for this system";
};
};
# Helper function to add monitoring endpoint
addMonitoringEndpoint = mkOption {
type = types.functionTo (types.functionTo types.anything);
default = name: endpoint: {
homelab.global.monitoring.endpoints = [
(endpoint // {inherit name;})
];
};
description = "Helper function to add monitoring endpoints";
};
# Helper function to add backup job
addBackupJob = mkOption {
type = types.functionTo (types.functionTo types.anything);
default = name: job: {
homelab.global.backups.jobs = [
(job // {inherit name;})
];
};
description = "Helper function to add backup jobs";
};
# Helper function to add reverse proxy entry
addReverseProxyEntry = mkOption {
type = types.functionTo (types.functionTo types.anything);
default = subdomain: entry: {
homelab.global.reverseProxy.entries = [
(entry // {inherit subdomain;})
];
};
description = "Helper function to add reverse proxy entries";
};
# Helper functions
enabledServicesList = mkOption {
type = types.listOf types.str;
default = attrNames enabledServices;
description = "List of enabled service names";
readOnly = true;
};
servicesByPriority = mkOption {
type = types.listOf types.str;
default =
map (x: x.name) (sort (a: b: a.priority < b.priority)
(mapAttrsToList (name: service: service // {inherit name;}) enabledServices));
description = "Services sorted by priority";
readOnly = true;
};
};
config = mkIf cfg.enable {
# Set hostname
networking.hostName = cfg.hostname;
# Configure node exporter if enabled
services.prometheus.exporters.node = mkIf cfg.monitoring.nodeExporter.enable {
enable = true;
port = cfg.monitoring.nodeExporter.port;
enabledCollectors = [
"systemd"
"textfile"
"filesystem"
"loadavg"
"meminfo"
"netdev"
"stat"
];
};
# Automatically add node exporter to monitoring endpoints
homelab.global.monitoring.endpoints = mkIf cfg.monitoring.nodeExporter.enable [
{
name = "node-exporter";
port = cfg.monitoring.nodeExporter.port;
path = "/metrics";
jobName = "node";
labels = {
instance = cfg.hostname;
environment = cfg.environment;
location = cfg.location;
};
}
];
# Export configuration for external consumption
environment.etc."homelab/config.json".text = builtins.toJSON {
inherit (cfg) hostname domain environment location tags;
services =
mapAttrs (name: service: {
inherit (service) enable description category dependencies ports tags priority;
})
cfg.services;
enabledServices = enabledServices;
servicesByCategory = {
monitoring = servicesByCategory "monitoring";
networking = servicesByCategory "networking";
storage = servicesByCategory "storage";
security = servicesByCategory "security";
media = servicesByCategory "media";
development = servicesByCategory "development";
backup = servicesByCategory "backup";
other = servicesByCategory "other";
};
monitoring = {
endpoints =
map (endpoint: {
name = endpoint.name;
url = "http://${cfg.hostname}:${toString endpoint.port}${endpoint.path}";
port = endpoint.port;
path = endpoint.path;
jobName = endpoint.jobName;
scrapeInterval = endpoint.scrapeInterval;
labels =
endpoint.labels
// {
hostname = cfg.hostname;
environment = cfg.environment;
};
})
cfg.monitoring.endpoints;
};
backups = {
jobs = cfg.backups.jobs;
};
reverseProxy = {
entries =
map (entry: {
subdomain = entry.subdomain;
url = "http://${cfg.hostname}:${toString entry.port}";
port = entry.port;
path = entry.path;
domain = "${entry.subdomain}.${cfg.domain}";
enableAuth = entry.enableAuth;
enableSSL = entry.enableSSL;
customHeaders = entry.customHeaders;
websockets = entry.websockets;
})
cfg.reverseProxy.entries;
};
};
# Create a status command that shows service information
environment.systemPackages = [
# (pkgs.writeScriptBin "homelab-services" ''
# #!/bin/bash
# echo "🏠 Homelab Services Status"
# echo "=========================="
# echo
# ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
# echo "${name}: ${service.description}"
# echo " Category: ${service.category}"
# echo " Status: $(systemctl is-active ${name} 2>/dev/null || echo "not found")"
# ${optionalString (service.ports != []) ''
# echo " Ports: ${concatStringsSep ", " (map toString service.ports)}"
# ''}
# ${optionalString (service.tags != []) ''
# echo " Tags: ${concatStringsSep ", " service.tags}"
# ''}
# echo
# '')
# enabledServices)}
# '')
];
};
}

View file

@ -1,304 +0,0 @@
# modules/motd/default.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.homelab.motd;
globalCfg = config.homelab.global;
enabledServices = filterAttrs (name: service: service.enable) globalCfg.services;
homelab-motd = pkgs.writeShellScriptBin "homelab-motd" ''
#! /usr/bin/env bash
source /etc/os-release
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Helper functions
print_header() {
echo -e "''${BOLD}''${BLUE}''${NC}"
echo -e "''${BOLD}''${BLUE}''${NC}''${WHITE} 🏠 HOMELAB STATUS ''${NC}''${BOLD}''${BLUE}''${NC}"
echo -e "''${BOLD}''${BLUE}''${NC}"
}
print_section() {
echo -e "\n''${BOLD}''${CYAN} $1''${NC}"
echo -e "''${CYAN}''${NC}"
}
get_service_status() {
local service="$1"
if ${pkgs.systemd}/bin/systemctl is-active --quiet "$service" 2>/dev/null; then
echo -e "''${GREEN}''${NC} Active"
elif ${pkgs.systemd}/bin/systemctl is-enabled --quiet "$service" 2>/dev/null; then
echo -e "''${YELLOW}''${NC} Inactive"
else
echo -e "''${RED}''${NC} Disabled"
fi
}
get_timer_status() {
local timer="$1"
if ${pkgs.systemd}/bin/systemctl is-active --quiet "$timer" 2>/dev/null; then
local next_run=$(${pkgs.systemd}/bin/systemctl show "$timer" --property=NextElapseUSecRealtime --value 2>/dev/null || echo "0")
if [[ "$next_run" != "0" && "$next_run" != "n/a" ]]; then
local next_readable=$(${pkgs.systemd}/bin/systemctl list-timers --no-pager "$timer" 2>/dev/null | tail -n +2 | head -n 1 | awk '{print $1, $2}' || echo "Unknown")
echo -e "''${GREEN}''${NC} Next: ''${next_readable}"
else
echo -e "''${GREEN}''${NC} Active"
fi
else
echo -e "''${RED}''${NC} Inactive"
fi
}
# Main script
${optionalString cfg.clearScreen "clear"}
print_header
# Check if global config exists
CONFIG_FILE="/etc/homelab/config.json"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "''${RED} Global homelab configuration not found at $CONFIG_FILE''${NC}"
exit 1
fi
# Parse global configuration
HOSTNAME=$(${pkgs.jq}/bin/jq -r '.hostname' "$CONFIG_FILE" 2>/dev/null || hostname)
DOMAIN=$(${pkgs.jq}/bin/jq -r '.domain' "$CONFIG_FILE" 2>/dev/null || echo "unknown")
ENVIRONMENT=$(${pkgs.jq}/bin/jq -r '.environment' "$CONFIG_FILE" 2>/dev/null || echo "unknown")
LOCATION=$(${pkgs.jq}/bin/jq -r '.location' "$CONFIG_FILE" 2>/dev/null || echo "unknown")
TAGS=$(${pkgs.jq}/bin/jq -r '.tags[]?' "$CONFIG_FILE" 2>/dev/null | tr '\n' ' ' || echo "none")
print_section "SYSTEM INFO"
echo -e " ''${BOLD}Hostname:''${NC} $HOSTNAME"
echo -e " ''${BOLD}Domain:''${NC} $DOMAIN"
echo -e " ''${BOLD}Environment:''${NC} $ENVIRONMENT"
echo -e " ''${BOLD}Location:''${NC} $LOCATION"
echo -e " ''${BOLD}Tags:''${NC} ''${TAGS:-none}"
echo -e " ''${BOLD}Uptime:''${NC} $(${pkgs.procps}/bin/uptime -p)"
echo -e " ''${BOLD}Load:''${NC} $(${pkgs.procps}/bin/uptime | awk -F'load average:' '{print $2}' | xargs)"
${optionalString cfg.showServices ''
# Enabled services from homelab config
print_section "HOMELAB SERVICES"
${concatStringsSep "\n" (mapAttrsToList (name: service: ''
status=$(get_service_status "${service.systemdService}")
printf " %-25s %s\n" "${name}" "$status"
'')
cfg.services)}
''}
${optionalString cfg.showMonitoring ''
# Monitoring endpoints
print_section "MONITORING ENDPOINTS"
ENDPOINTS=$(${pkgs.jq}/bin/jq -c '.monitoring.endpoints[]?' "$CONFIG_FILE" 2>/dev/null || echo "")
if [[ -n "$ENDPOINTS" ]]; then
while IFS= read -r endpoint; do
name=$(echo "$endpoint" | ${pkgs.jq}/bin/jq -r '.name')
port=$(echo "$endpoint" | ${pkgs.jq}/bin/jq -r '.port')
path=$(echo "$endpoint" | ${pkgs.jq}/bin/jq -r '.path')
job=$(echo "$endpoint" | ${pkgs.jq}/bin/jq -r '.jobName')
# Check if port is accessible
if ${pkgs.netcat}/bin/nc -z localhost "$port" 2>/dev/null; then
status="''${GREEN}''${NC}"
else
status="''${RED}''${NC}"
fi
printf " %-20s %s %s:%s%s (job: %s)\n" "$name" "$status" "$HOSTNAME" "$port" "$path" "$job"
done <<< "$ENDPOINTS"
else
echo -e " ''${YELLOW}No monitoring endpoints configured''${NC}"
fi
''}
${optionalString cfg.showBackups ''
# Backup jobs status
print_section "BACKUP JOBS"
BACKUP_JOBS=$(${pkgs.jq}/bin/jq -c '.backups.jobs[]?' "$CONFIG_FILE" 2>/dev/null || echo "")
if [[ -n "$BACKUP_JOBS" ]]; then
while IFS= read -r job; do
name=$(echo "$job" | ${pkgs.jq}/bin/jq -r '.name')
backend=$(echo "$job" | ${pkgs.jq}/bin/jq -r '.backend')
schedule=$(echo "$job" | ${pkgs.jq}/bin/jq -r '.schedule')
service_name="backup-''${name}"
timer_name="''${service_name}.timer"
timer_status=$(get_timer_status "$timer_name")
# Get last backup info
last_run="Unknown"
if ${pkgs.systemd}/bin/systemctl show "$service_name" --property=ExecMainStartTimestamp --value 2>/dev/null | grep -q "^[^n]"; then
last_run=$(${pkgs.systemd}/bin/systemctl show "$service_name" --property=ExecMainStartTimestamp --value 2>/dev/null | head -1)
if [[ "$last_run" != "n/a" && -n "$last_run" ]]; then
last_run=$(${pkgs.coreutils}/bin/date -d "$last_run" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "Unknown")
fi
fi
printf " %-20s %s (%s, %s) Last: %s\n" "$name" "$timer_status" "$backend" "$schedule" "$last_run"
done <<< "$BACKUP_JOBS"
# Show backup-status command output if available
if command -v backup-status >/dev/null 2>&1; then
echo -e "\n ''${BOLD}Quick Status:''${NC}"
backup-status 2>/dev/null | tail -n +3 | head -10 | sed 's/^/ /'
fi
else
echo -e " ''${YELLOW}No backup jobs configured''${NC}"
fi
''}
${optionalString cfg.showReverseProxy ''
# Reverse proxy entries
print_section "REVERSE PROXY ENTRIES"
PROXY_ENTRIES=$(${pkgs.jq}/bin/jq -c '.reverseProxy.entries[]?' "$CONFIG_FILE" 2>/dev/null || echo "")
if [[ -n "$PROXY_ENTRIES" ]]; then
while IFS= read -r entry; do
subdomain=$(echo "$entry" | ${pkgs.jq}/bin/jq -r '.subdomain')
port=$(echo "$entry" | ${pkgs.jq}/bin/jq -r '.port')
domain=$(echo "$entry" | ${pkgs.jq}/bin/jq -r '.domain')
auth=$(echo "$entry" | ${pkgs.jq}/bin/jq -r '.enableAuth')
ssl=$(echo "$entry" | ${pkgs.jq}/bin/jq -r '.enableSSL')
# Check if service is running on the port
if ${pkgs.netcat}/bin/nc -z localhost "$port" 2>/dev/null; then
status="''${GREEN}''${NC}"
else
status="''${RED}''${NC}"
fi
auth_indicator=""
[[ "$auth" == "true" ]] && auth_indicator=" 🔐"
ssl_indicator=""
[[ "$ssl" == "true" ]] && ssl_indicator=" 🔒"
printf " %-25s %s :%s %s%s%s\n" "''${domain}" "$status" "$port" "$domain" "$auth_indicator" "$ssl_indicator"
done <<< "$PROXY_ENTRIES"
else
echo -e " ''${YELLOW}No reverse proxy entries configured''${NC}"
fi
''}
${optionalString cfg.showResources ''
# Resource usage
print_section "RESOURCE USAGE"
echo -e " ''${BOLD}Memory:''${NC} $(${pkgs.procps}/bin/free -h | awk '/^Mem:/ {printf "%s/%s (%.1f%%)", $3, $2, ($3/$2)*100}')"
echo -e " ''${BOLD}Disk (root):''${NC} $(${pkgs.coreutils}/bin/df -h / | awk 'NR==2 {printf "%s/%s (%s)", $3, $2, $5}')"
echo -e " ''${BOLD}CPU Usage:''${NC} $(${pkgs.procps}/bin/top -bn1 | grep "Cpu(s)" | awk '{printf "%.1f%%", $2+$4}' | sed 's/%us,//')%"
''}
${optionalString cfg.showRecentIssues ''
# Recent logs (errors only)
print_section "RECENT ISSUES"
error_count=$(${pkgs.systemd}/bin/journalctl --since "24 hours ago" --priority=err --no-pager -q | wc -l)
if [[ "$error_count" -gt 0 ]]; then
echo -e " ''${RED} $error_count errors in last 24h''${NC}"
${pkgs.systemd}/bin/journalctl --since "24 hours ago" --priority=err --no-pager -q | tail -3 | sed 's/^/ /'
else
echo -e " ''${GREEN} No critical errors in last 24h''${NC}"
fi
''}
echo -e "\n''${BOLD}''${BLUE}''${NC}"
echo -e "''${BOLD}''${BLUE}''${NC} ''${WHITE}Run 'backup-status' for detailed backup info ''${NC}''${BOLD}''${BLUE}''${NC}"
echo -e "''${BOLD}''${BLUE}''${NC} ''${WHITE}Config: /etc/homelab/config.json ''${NC}''${BOLD}''${BLUE}''${NC}"
echo -e "''${BOLD}''${BLUE}''${NC}"
echo
'';
in {
options.homelab.motd = {
enable = mkEnableOption "Dynamic homelab MOTD";
clearScreen = mkOption {
type = types.bool;
default = true;
description = "Clear screen before showing MOTD";
};
showServices = mkOption {
type = types.bool;
default = true;
description = "Show enabled homelab services";
};
showMonitoring = mkOption {
type = types.bool;
default = true;
description = "Show monitoring endpoints";
};
showBackups = mkOption {
type = types.bool;
default = true;
description = "Show backup jobs status";
};
showReverseProxy = mkOption {
type = types.bool;
default = true;
description = "Show reverse proxy entries";
};
showResources = mkOption {
type = types.bool;
default = true;
description = "Show system resource usage";
};
showRecentIssues = mkOption {
type = types.bool;
default = true;
description = "Show recent system issues";
};
services = mkOption {
type = types.attrsOf (types.submodule {
options = {
systemdService = mkOption {
type = types.str;
description = "Name of the systemd service to monitor";
};
description = mkOption {
type = types.str;
default = "";
description = "Human-readable description of the service";
};
};
});
default = {};
description = "Homelab services to monitor in MOTD";
};
};
config = mkIf (cfg.enable && globalCfg.enable) {
# Register services with MOTD
homelab.motd.services =
mapAttrs (name: service: {
systemdService = name;
description = service.description;
})
enabledServices;
# Create a command to manually run the MOTD
environment.systemPackages = with pkgs; [
jq
netcat
homelab-motd
];
};
}

View file

@ -1,4 +0,0 @@
{
jellyfin = import ./jellyfin.nix;
grafana = import ./grafana.nix;
}

View file

@ -1 +0,0 @@

View file

@ -1,72 +0,0 @@
# modules/services/grafana.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.services.grafana;
helpers = import ../lib/helpers.nix {inherit lib;};
in {
options.services.grafana = {
enable = mkEnableOption "Grafana monitoring dashboard";
port = mkOption {
type = types.port;
default = 3000;
description = "Grafana web interface port";
};
adminPassword = mkOption {
type = types.str;
description = "Admin password for Grafana";
};
};
config = mkIf cfg.enable {
services.grafana = {
enable = true;
settings = {
server = {
http_port = cfg.port;
domain = "${config.homelab.global.hostname}.${config.homelab.global.domain}";
};
security = {
admin_password = cfg.adminPassword;
};
};
};
homelab.global = {
backups.jobs = [
{
name = "grafana-data";
backend = "restic";
paths = ["/var/lib/grafana"];
schedule = "daily";
excludePatterns = ["*/plugins/*" "*/png/*"];
}
];
reverseProxy.entries = [
{
subdomain = "grafana";
port = cfg.port;
enableAuth = false; # Grafana handles its own auth
}
];
monitoring.endpoints = [
{
name = "grafana";
port = cfg.port;
path = "/metrics";
jobName = "grafana";
labels = {
service = "grafana";
type = "monitoring";
};
}
];
};
};
}

View file

@ -1,125 +0,0 @@
# modules/services/jellyfin.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.services.jellyfin;
in {
options.services.jellyfin = {
enable = mkEnableOption "Jellyfin media server";
port = mkOption {
type = types.port;
default = 8096;
description = "Port for Jellyfin web interface";
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/jellyfin";
description = "Directory to store Jellyfin data";
};
mediaDir = mkOption {
type = types.str;
default = "/media";
description = "Directory containing media files";
};
enableMetrics = mkOption {
type = types.bool;
default = true;
description = "Enable Prometheus metrics";
};
exposeWeb = mkOption {
type = types.bool;
default = true;
description = "Expose web interface through reverse proxy";
};
};
config = mkIf cfg.enable {
# Enable the service
services.jellyfin = {
enable = true;
dataDir = cfg.dataDir;
};
# Configure global settings
homelab.global = {
# Add backup job for Jellyfin data
backups.jobs = [
{
name = "jellyfin-config";
backend = "restic";
paths = ["${cfg.dataDir}/config" "${cfg.dataDir}/data"];
schedule = "0 2 * * *"; # Daily at 2 AM
excludePatterns = [
"*/cache/*"
"*/transcodes/*"
"*/logs/*"
];
preHook = ''
# Stop jellyfin for consistent backup
systemctl stop jellyfin
'';
postHook = ''
# Restart jellyfin after backup
systemctl start jellyfin
'';
}
{
name = "jellyfin-media";
backend = "restic";
paths = [cfg.mediaDir];
schedule = "0 3 * * 0"; # Weekly on Sunday at 3 AM
excludePatterns = [
"*.tmp"
"*/.@__thumb/*" # Synology thumbnails
];
}
];
# Add reverse proxy entry if enabled
reverseProxy.entries = mkIf cfg.exposeWeb [
{
subdomain = "jellyfin";
port = cfg.port;
enableAuth = false; # Jellyfin has its own auth
websockets = true;
customHeaders = {
"X-Forwarded-Proto" = "$scheme";
"X-Forwarded-Host" = "$host";
};
}
];
# Add monitoring endpoint if metrics enabled
monitoring.endpoints = mkIf cfg.enableMetrics [
{
name = "jellyfin";
port = cfg.port;
path = "/metrics"; # Assuming you have a metrics plugin
jobName = "jellyfin";
scrapeInterval = "60s";
labels = {
service = "jellyfin";
type = "media-server";
};
}
];
};
# Open firewall
networking.firewall.allowedTCPPorts = [cfg.port];
# Create media directory
systemd.tmpfiles.rules = [
"d ${cfg.mediaDir} 0755 jellyfin jellyfin -"
];
};
}

View file

@ -1,208 +0,0 @@
# modules/services/prometheus.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.homelab.services.prometheus;
globalCfg = config.homelab.global;
in {
options.homelab.services.prometheus = {
enable = mkEnableOption "Prometheus monitoring server";
port = mkOption {
type = types.port;
default = 9090;
description = "Prometheus server port";
};
webExternalUrl = mkOption {
type = types.str;
default = "http://${globalCfg.hostname}:${toString cfg.port}";
description = "External URL for Prometheus";
};
retention = mkOption {
type = types.str;
default = "30d";
description = "Data retention period";
};
scrapeConfigs = mkOption {
type = types.listOf types.attrs;
default = [];
description = "Additional scrape configurations";
};
alertmanager = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable Alertmanager integration";
};
url = mkOption {
type = types.str;
default = "http://localhost:9093";
description = "Alertmanager URL";
};
};
};
config = mkIf cfg.enable {
# Register service with global homelab config
homelab.global.services.prometheus = {
enable = true;
description = "Metrics collection and monitoring server";
category = "monitoring";
ports = [cfg.port];
tags = ["metrics" "monitoring" "alerting"];
priority = 20;
dependencies = ["node-exporter"];
};
# Configure the actual Prometheus service
services.prometheus = {
enable = true;
port = cfg.port;
webExternalUrl = cfg.webExternalUrl;
retentionTime = cfg.retention;
scrapeConfigs =
[
# Auto-discover monitoring endpoints from global config
{
job_name = "homelab-auto";
static_configs = [
{
targets =
map (
endpoint: "${globalCfg.hostname}:${toString endpoint.port}"
)
globalCfg.monitoring.endpoints;
}
];
scrape_interval = "30s";
metrics_path = "/metrics";
}
]
++ cfg.scrapeConfigs;
# Alertmanager configuration
alertmanagers = mkIf cfg.alertmanager.enable [
{
static_configs = [
{
targets = [cfg.alertmanager.url];
}
];
}
];
rules = [
# Basic homelab alerting rules
(pkgs.writeText "homelab-alerts.yml" ''
groups:
- name: homelab
rules:
- alert: ServiceDown
expr: up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.instance }} is down"
description: "{{ $labels.job }} on {{ $labels.instance }} has been down for more than 5 minutes."
- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is above 90% on {{ $labels.instance }}"
- alert: HighDiskUsage
expr: (node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "High disk usage on {{ $labels.instance }}"
description: "Disk usage is above 85% on {{ $labels.instance }} for filesystem {{ $labels.mountpoint }}"
'')
];
};
# Add monitoring endpoint to global config
homelab.global.monitoring.endpoints = [
{
name = "prometheus";
port = cfg.port;
path = "/metrics";
jobName = "prometheus";
scrapeInterval = "30s";
labels = {
service = "prometheus";
role = "monitoring";
};
}
];
# Add reverse proxy entry if configured
homelab.global.reverseProxy.entries = mkIf (globalCfg.domain != null) [
{
subdomain = "prometheus";
port = cfg.port;
path = "/";
enableAuth = true;
enableSSL = true;
customHeaders = {
"X-Frame-Options" = "DENY";
"X-Content-Type-Options" = "nosniff";
};
}
];
# Add backup job for Prometheus data
homelab.global.backups.jobs = [
{
name = "prometheus-data";
backend = "restic";
paths = ["/var/lib/prometheus2"];
schedule = "daily";
retention = {
daily = "7";
weekly = "4";
monthly = "3";
yearly = "1";
};
excludePatterns = [
"*.tmp"
"*/wal/*"
];
preHook = ''
# Stop prometheus temporarily for consistent backup
systemctl stop prometheus
'';
postHook = ''
# Restart prometheus after backup
systemctl start prometheus
'';
}
];
# Open firewall port
networking.firewall.allowedTCPPorts = [cfg.port];
# Create prometheus configuration directory
systemd.tmpfiles.rules = [
"d /var/lib/prometheus2 0755 prometheus prometheus -"
"d /etc/prometheus 0755 root root -"
];
};
}

View file

@ -1,4 +0,0 @@
{
# TODO
# https://github.com/L-Trump/nixos-configs/blob/ab3fb16e330b8a2904b9967e46af8c061b56266e/modules/nixos/server/backrest.nix#L7
}

View file

@ -1,95 +0,0 @@
# backups-option.nix
cfg: let
inherit (cfg.lib) mkOption types mkEnableOption attrNames;
in
mkOption {
type = types.attrsOf (
types.submodule (
{
name,
config,
...
} @ args: {
options = {
backend = mkOption {
type = types.enum (attrNames cfg.backends);
description = "The backup backend to use";
};
paths = mkOption {
type = types.listOf types.str;
default = [];
description = "Paths to backup";
};
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to enable this backup job";
};
timerConfig = mkOption {
type = with types; nullOr attrs;
default = null;
example = {
OnCalendar = "00:05";
Persistent = true;
RandomizedDelaySec = "5h";
};
description = ''
When to run the backup. If null, inherits from backend's default timerConfig.
Set to null to disable automatic scheduling.
'';
};
backendOptions = mkOption {
type = let
backupConfig = config;
backupName = name;
in
types.submodule (
{config, ...} @ args'':
cfg.backends.${args.config.backend} (args'' // {inherit backupConfig backupName;})
);
default = {};
description = "Backend-specific options";
};
preBackupScript = mkOption {
type = types.lines;
default = "";
description = "Script to run before backing up";
};
postBackupScript = mkOption {
type = types.lines;
default = "";
description = ''
Script to run after backing up. Runs even if the backup fails.
'';
};
notifications = {
failure = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable failure notifications";
};
};
success = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable success notifications";
};
};
};
};
}
)
);
default = {};
description = "Backup job definitions";
}

View file

@ -1,6 +0,0 @@
{
imports = [
./root.nix
./restic.nix
];
}

View file

@ -1,234 +0,0 @@
# restic.nix - Restic backend implementation
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.system.backups;
resticCfg = cfg.restic;
# Get only restic backups that are enabled
resticBackups = filterAttrs (_: backup: backup.backend == "restic" && backup.enable) cfg.backups;
# Create restic service configurations
createResticServices =
mapAttrs (
name: backup: let
# Merge global defaults with backup-specific options
serviceConfig =
recursiveUpdate resticCfg.defaultBackendOptions backup.backendOptions
// {
inherit (backup) paths;
# Use backup-specific timer or fall back to global default
timerConfig =
if backup.timerConfig != null
then backup.timerConfig
else resticCfg.timerConfig;
};
in
serviceConfig
)
resticBackups;
in {
options.system.backups.restic = {
enable = mkEnableOption "restic backup backend";
timerConfig = mkOption {
type = types.attrs;
default = {
OnCalendar = "*-*-* 05:00:00";
Persistent = true;
};
description = "Default systemd timer configuration for restic backups";
};
defaultBackendOptions = mkOption {
type = types.attrs;
default = {};
example = {
repository = "/backup/restic";
passwordFile = "/etc/nixos/secrets/restic-password";
initialize = true;
pruneOpts = [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
description = "Default backend options applied to all restic backup jobs";
};
# Advanced options
runMaintenance = mkOption {
type = types.bool;
default = true;
description = "Whether to run repository maintenance after backups";
};
maintenanceTimer = mkOption {
type = types.attrs;
default = {
OnCalendar = "*-*-* 06:00:00";
Persistent = true;
};
description = "Timer configuration for maintenance tasks";
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 6"
"--keep-yearly 3"
];
description = "Default pruning options for maintenance";
};
};
config = mkIf resticCfg.enable {
# Register restic backend
system.backups.backends.restic = {
backupConfig,
backupName,
...
}: {
# Define the proper options schema for restic backendOptions
options = {
repository = mkOption {
type = types.str;
description = "Restic repository path or URL";
};
passwordFile = mkOption {
type = types.str;
description = "Path to file containing the repository password";
};
initialize = mkOption {
type = types.bool;
default = true;
description = "Whether to initialize the repository if it doesn't exist";
};
exclude = mkOption {
type = types.listOf types.str;
default = [];
description = "Patterns to exclude from backup";
};
extraBackupArgs = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional arguments passed to restic backup command";
};
user = mkOption {
type = types.str;
default = "root";
description = "User to run the backup as";
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = resticCfg.pruneOpts;
description = "Pruning options for this backup";
};
};
# Default config merged with global defaults
config = {
extraBackupArgs =
[
"--tag ${backupName}"
"--verbose"
]
++ (resticCfg.defaultBackendOptions.extraBackupArgs or []);
};
};
# Create actual restic backup services
services.restic.backups = createResticServices;
# Add restic package
environment.systemPackages = [pkgs.restic];
# Systemd service customizations for restic backups
systemd.services =
(mapAttrs' (
name: backup:
nameValuePair "restic-backups-${name}" {
# Custom pre/post scripts
preStart = mkBefore backup.preBackupScript;
postStop = mkAfter backup.postBackupScript;
# Enhanced service configuration
serviceConfig = {
# Restart configuration
Restart = "on-failure";
RestartSec = "5m";
RestartMaxDelaySec = "30m";
RestartSteps = 3;
# Rate limiting
StartLimitBurst = 4;
StartLimitIntervalSec = "2h";
};
# Failure handling could be extended here for notifications
# onFailure = optional backup.notifications.failure.enable "restic-backup-${name}-failure-notify.service";
}
)
resticBackups)
// optionalAttrs resticCfg.runMaintenance {
# Repository maintenance service
restic-maintenance = {
description = "Restic repository maintenance";
after = map (name: "restic-backups-${name}.service") (attrNames resticBackups);
environment =
resticCfg.defaultBackendOptions
// {
RESTIC_CACHE_DIR = "/var/cache/restic-maintenance";
};
serviceConfig = {
Type = "oneshot";
ExecStart = [
"${pkgs.restic}/bin/restic forget --prune ${concatStringsSep " " resticCfg.pruneOpts}"
"${pkgs.restic}/bin/restic check --read-data-subset=500M"
];
User = "root";
CacheDirectory = "restic-maintenance";
CacheDirectoryMode = "0700";
};
};
};
# Maintenance timer
systemd.timers = mkIf resticCfg.runMaintenance {
restic-maintenance = {
description = "Timer for restic repository maintenance";
wantedBy = ["timers.target"];
timerConfig = resticCfg.maintenanceTimer;
};
};
# Helpful shell aliases
programs.zsh.shellAliases =
{
restic-snapshots = "restic snapshots --compact --group-by tags";
restic-repo-size = "restic stats --mode raw-data";
}
// (mapAttrs' (
name: _:
nameValuePair "backup-${name}" "systemctl start restic-backups-${name}"
)
resticBackups);
};
}

View file

@ -1,66 +0,0 @@
# root.nix - Main backup system module
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.system.backups;
# Filter backups by backend
getBackupsByBackend = backend:
filterAttrs (_: backup: backup.backend == backend && backup.enable) cfg.backups;
in {
options.system.backups = {
# Backend registration system - backends register themselves here
backends = mkOption {
type = with types; attrsOf (functionTo attrs);
internal = true;
default = {};
description = ''
Attribute set of backends where the value is a function that accepts
backend-specific arguments and returns an attribute set for the backend's options.
'';
};
# Import the backups option from separate file, passing cfg for backend inference
backups = import ./backups-option.nix cfg;
# Pass lib to the backups-option for access to mkOption, types, etc.
lib = mkOption {
type = types.attrs;
internal = true;
default = lib;
};
};
config = {
# Re-export backups at root level for convenience
# backups = cfg.backups;
# Common backup packages
environment.systemPackages = with pkgs; [
# Add common backup utilities here
];
# Common systemd service modifications for all backup services
systemd.services = let
allBackupServices = flatten (
mapAttrsToList (
backendName: backups:
mapAttrsToList (name: backup: "${backendName}-backups-${name}") backups
) (genAttrs (attrNames cfg.backends) (backend: getBackupsByBackend backend))
);
in
genAttrs allBackupServices (serviceName: {
serviceConfig = {
# Common hardening for all backup services
ProtectSystem = "strict";
ProtectHome = "read-only";
PrivateTmp = true;
NoNewPrivileges = true;
};
});
};
}