#!/usr/bin/env python3 """ Portainer Stack vs Git Repository Comparison Tool Generates documentation comparing running stacks with repo configurations """ import json import os from datetime import datetime from pathlib import Path # Endpoint ID to Server Name mapping ENDPOINT_MAP = { 2: "Atlantis", 443395: "Concord NUC", 443397: "Calypso (vish-nuc)", 443398: "vish-nuc-edge", 443399: "Homelab VM" } # Server folder mapping in repo REPO_FOLDER_MAP = { "Atlantis": ["Atlantis"], "Concord NUC": ["concord_nuc"], "Calypso (vish-nuc)": ["Calypso"], "vish-nuc-edge": [], "Homelab VM": ["homelab_vm"] } # Running stacks data (collected from Portainer API) RUNNING_STACKS = { "Atlantis": { "stacks": [ {"name": "arr-stack", "containers": ["deluge", "sonarr", "radarr", "lidarr", "gluetun", "jackett", "tautulli", "sabnzbd", "plex", "whisparr", "flaresolverr", "wizarr", "bazarr", "prowlarr", "jellyseerr"], "git_linked": True, "git_path": "Atlantis/arr-suite/"}, {"name": "nginx_repo-stack", "containers": ["nginx"], "git_linked": True, "git_path": "Atlantis/repo_nginx.yaml"}, {"name": "dyndns-updater-stack", "containers": ["ddns-vish-unproxied", "ddns-vish-proxied", "ddns-thevish-unproxied", "ddns-thevish-proxied"], "git_linked": True, "git_path": "Atlantis/dynamicdnsupdater.yaml"}, {"name": "baikal-stack", "containers": ["baikal"], "git_linked": True, "git_path": "Atlantis/baikal/"}, {"name": "jitsi", "containers": ["jitsi-web", "jitsi-jvb", "jitsi-jicofo", "coturn", "jitsi-prosody"], "git_linked": True, "git_path": "Atlantis/jitsi/"}, {"name": "youtubedl", "containers": ["youtube_downloader"], "git_linked": True, "git_path": "Atlantis/youtubedl.yaml"}, {"name": "matrix_synapse-stack", "containers": ["Synapse", "Synapse-DB"], "git_linked": True, "git_path": "Atlantis/synapse.yml", "issues": ["Synapse container exited"]}, {"name": "joplin-stack", "containers": ["joplin-app", "joplin-db"], "git_linked": True, "git_path": "Atlantis/joplin.yml"}, {"name": "immich-stack", "containers": ["Immich-SERVER", "Immich-LEARNING", "Immich-DB", "Immich-REDIS"], "git_linked": True, "git_path": "Atlantis/immich/"}, {"name": "vaultwarden-stack", "containers": ["Vaultwarden", "Vaultwarden-DB"], "git_linked": True, "git_path": "Atlantis/vaultwarden.yaml"}, {"name": "node-exporter-stack", "containers": ["snmp_exporter", "node_exporter"], "git_linked": False}, {"name": "fenrus-stack", "containers": ["Fenrus"], "git_linked": True, "git_path": "Atlantis/fenrus.yaml"}, {"name": "syncthing-stack", "containers": [], "git_linked": True, "git_path": "Atlantis/syncthing.yml", "status": "stopped"}, ], "standalone": ["portainer"] }, "Concord NUC": { "stacks": [ {"name": "invidious", "containers": ["invidious-companion", "invidious-db", "invidious"], "git_linked": True, "git_path": "concord_nuc/invidious/", "issues": ["invidious unhealthy"]}, {"name": "syncthing-stack", "containers": ["syncthing"], "git_linked": True, "git_path": "concord_nuc/syncthing.yaml"}, {"name": "homeassistant-stack", "containers": ["homeassistant", "matter-server"], "git_linked": True, "git_path": "concord_nuc/homeassistant.yaml"}, {"name": "adguard-stack", "containers": ["AdGuard"], "git_linked": True, "git_path": "concord_nuc/adguard.yaml"}, {"name": "yourspotify-stack", "containers": ["yourspotify-server", "mongo", "yourspotify-web"], "git_linked": True, "git_path": "concord_nuc/yourspotify.yaml"}, {"name": "dyndns-updater", "containers": ["ddns-vish-13340"], "git_linked": True, "git_path": "concord_nuc/dyndns_updater.yaml"}, {"name": "wireguard-stack", "containers": ["wg-easy"], "git_linked": True, "git_path": "concord_nuc/wireguard.yaml"}, {"name": "node-exporter", "containers": ["node_exporter"], "git_linked": False, "issues": ["restarting"]}, ], "standalone": ["portainer_edge_agent", "watchtower"], "issues": ["watchtower restarting", "node_exporter restarting"] }, "Calypso (vish-nuc)": { "stacks": [ {"name": "arr-stack", "containers": ["jellyseerr", "bazarr", "sonarr", "lidarr", "prowlarr", "plex", "readarr", "radarr", "flaresolverr", "sabnzbd", "tautulli", "whisparr"], "git_linked": True, "git_path": "Calypso/arr_suite_with_dracula.yml"}, {"name": "rxv4-stack", "containers": ["Resume-ACCESS", "Resume-DB", "Resume-CHROME", "Resume-MINIO"], "git_linked": True, "git_path": "Calypso/reactive_resume_v4/"}, {"name": "seafile", "containers": ["Seafile-DB", "Seafile-CACHE", "Seafile-REDIS", "Seafile"], "git_linked": True, "git_path": "Calypso/seafile-server.yaml"}, {"name": "gitea", "containers": ["Gitea-DB", "Gitea"], "git_linked": True, "git_path": "Calypso/gitea-server.yaml"}, {"name": "paperless-testing", "containers": ["PaperlessNGX", "PaperlessNGX-REDIS", "PaperlessNGX-DB", "PaperlessNGX-GOTENBERG", "PaperlessNGX-TIKA"], "git_linked": False}, {"name": "paperless-ai", "containers": ["PaperlessNGX-AI"], "git_linked": False}, {"name": "rustdesk", "containers": ["Rustdesk-HBBS", "Rustdesk-HBBR"], "git_linked": False}, {"name": "immich-stack", "containers": ["Immich-SERVER", "Immich-LEARNING", "Immich-DB", "Immich-REDIS"], "git_linked": True, "git_path": "Calypso/immich/"}, {"name": "rackula-stack", "containers": ["Rackula"], "git_linked": True, "git_path": "Calypso/rackula.yml"}, {"name": "adguard-stack", "containers": ["AdGuard"], "git_linked": True, "git_path": "Calypso/adguard.yaml"}, {"name": "syncthing-stack", "containers": ["syncthing"], "git_linked": True, "git_path": "Calypso/syncthing.yaml"}, {"name": "node-exporter", "containers": ["snmp_exporter", "node_exporter"], "git_linked": False}, {"name": "actual-budget-stack", "containers": ["Actual"], "git_linked": True, "git_path": "Calypso/actualbudget.yml"}, {"name": "apt-cacher-ng", "containers": ["apt-cacher-ng"], "git_linked": True, "git_path": "Calypso/apt-cacher-ng/"}, {"name": "iperf3-stack", "containers": ["iperf3"], "git_linked": True, "git_path": "Calypso/iperf3.yml"}, {"name": "wireguard", "containers": ["wgeasy"], "git_linked": True, "git_path": "Calypso/wireguard-server.yaml"}, ], "standalone": ["portainer_edge_agent", "openspeedtest"] }, "Homelab VM": { "stacks": [ {"name": "openhands", "containers": ["openhands-app"], "git_linked": False}, {"name": "monitoring", "containers": ["prometheus", "grafana", "node_exporter"], "git_linked": True, "git_path": "homelab_vm/prometheus_grafana_hub/"}, {"name": "perplexica", "containers": ["perplexica"], "git_linked": False}, {"name": "syncthing-stack", "containers": ["syncthing"], "git_linked": True, "git_path": "homelab_vm/syncthing.yml"}, {"name": "hoarder-karakeep-stack", "containers": ["meilisearch", "web", "chrome"], "git_linked": True, "git_path": "homelab_vm/hoarder.yaml"}, {"name": "drawio-stack", "containers": ["Draw.io"], "git_linked": True, "git_path": "homelab_vm/drawio.yml"}, {"name": "redlib-stack", "containers": ["Libreddit"], "git_linked": True, "git_path": "homelab_vm/libreddit.yaml"}, {"name": "signal-api-stack", "containers": ["signal-api"], "git_linked": True, "git_path": "homelab_vm/signal_api.yaml"}, {"name": "binternet-stack", "containers": ["binternet"], "git_linked": True, "git_path": "homelab_vm/binternet.yaml"}, {"name": "archivebox-stack", "containers": ["archivebox_scheduler", "archivebox", "archivebox_sonic"], "git_linked": True, "git_path": "homelab_vm/archivebox.yaml"}, {"name": "watchyourlan-stack", "containers": ["WatchYourLAN"], "git_linked": True, "git_path": "homelab_vm/watchyourlan.yaml"}, {"name": "webcheck-stack", "containers": ["Web-Check"], "git_linked": True, "git_path": "homelab_vm/webcheck.yaml"}, ], "standalone": ["portainer_edge_agent", "openhands-runtime"] }, "vish-nuc-edge": { "stacks": [ {"name": "kuma", "containers": ["uptime-kuma"], "git_linked": False}, {"name": "glances", "containers": ["glances"], "git_linked": False}, ], "standalone": ["portainer_edge_agent"] } } # Repo configs not running def get_repo_configs(): """List all compose files in the repo organized by server""" repo_configs = {} base_path = Path("/workspace/homelab") server_folders = { "Atlantis": base_path / "Atlantis", "Calypso": base_path / "Calypso", "concord_nuc": base_path / "concord_nuc", "homelab_vm": base_path / "homelab_vm", "Bulgaria_vm": base_path / "Bulgaria_vm", "Chicago_vm": base_path / "Chicago_vm", "anubis": base_path / "anubis", "guava": base_path / "guava", "setillo": base_path / "setillo", } for server, folder in server_folders.items(): if folder.exists(): configs = [] for ext in ["*.yml", "*.yaml"]: configs.extend(folder.rglob(ext)) repo_configs[server] = [str(c.relative_to(base_path)) for c in configs] return repo_configs def generate_markdown_report(): """Generate the comparison report in markdown""" report = [] report.append("# Portainer Stack vs Repository Configuration Comparison") report.append(f"\n*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}*") report.append("\n---\n") # Summary Section report.append("## Executive Summary\n") total_stacks = sum(len(data["stacks"]) for data in RUNNING_STACKS.values()) git_linked = sum( sum(1 for s in data["stacks"] if s.get("git_linked", False)) for data in RUNNING_STACKS.values() ) not_git_linked = total_stacks - git_linked report.append(f"- **Total Running Stacks:** {total_stacks}") report.append(f"- **Git-Linked Stacks:** {git_linked} ({git_linked/total_stacks*100:.1f}%)") report.append(f"- **Not Git-Linked:** {not_git_linked}") report.append(f"- **Servers Monitored:** {len(RUNNING_STACKS)}") report.append("") # Issues Summary all_issues = [] for server, data in RUNNING_STACKS.items(): for stack in data["stacks"]: if "issues" in stack: for issue in stack["issues"]: all_issues.append(f"{server}/{stack['name']}: {issue}") if "issues" in data: for issue in data["issues"]: all_issues.append(f"{server}: {issue}") if all_issues: report.append("### ⚠️ Current Issues\n") for issue in all_issues: report.append(f"- {issue}") report.append("") # Per-Server Details report.append("---\n") report.append("## Server Details\n") for server, data in RUNNING_STACKS.items(): report.append(f"### 🖥️ {server}\n") # Running Stacks Table report.append("#### Running Stacks\n") report.append("| Stack Name | Containers | Git-Linked | Config Path | Status |") report.append("|------------|------------|------------|-------------|--------|") for stack in data["stacks"]: name = stack["name"] containers = len(stack["containers"]) git_linked = "✅" if stack.get("git_linked") else "❌" config_path = stack.get("git_path", "-") status = "🟢 Running" if stack.get("status") == "stopped": status = "🔴 Stopped" elif "issues" in stack: status = f"⚠️ {stack['issues'][0]}" report.append(f"| {name} | {containers} | {git_linked} | `{config_path}` | {status} |") report.append("") # Standalone containers if data.get("standalone"): report.append("#### Standalone Containers (not in stacks)\n") report.append(", ".join([f"`{c}`" for c in data["standalone"]])) report.append("") report.append("") # Configs in Repo but Not Running report.append("---\n") report.append("## Repository Configs Not Currently Running\n") report.append("These configurations exist in the repo but are not deployed:\n") repo_configs = get_repo_configs() # Known running config paths running_paths = set() for server, data in RUNNING_STACKS.items(): for stack in data["stacks"]: if "git_path" in stack: running_paths.add(stack["git_path"].rstrip("/")) for server, configs in repo_configs.items(): not_running = [] for config in configs: config_base = config.rsplit("/", 1)[0] if "/" in config else config is_running = any( config.startswith(p.rstrip("/")) or p.startswith(config.rsplit("/", 1)[0]) for p in running_paths ) if not is_running: not_running.append(config) if not_running: report.append(f"\n### {server}\n") for config in not_running[:15]: # Limit to first 15 report.append(f"- `{config}`") if len(not_running) > 15: report.append(f"- ... and {len(not_running) - 15} more") # Recommendations report.append("\n---\n") report.append("## Recommendations\n") report.append(""" 1. **Link Remaining Stacks to Git**: The following stacks should be linked to Git for version control: - `paperless-testing` and `paperless-ai` on Calypso - `rustdesk` on Calypso - `node-exporter` stacks on multiple servers - `openhands` and `perplexica` on Homelab VM - `kuma` and `glances` on vish-nuc-edge 2. **Address Current Issues**: - Fix `Synapse` container on Atlantis (currently exited) - Investigate `invidious` unhealthy status on Concord NUC - Fix `watchtower` and `node_exporter` restart loops on Concord NUC 3. **Cleanup Unused Configs**: Review configs in repo not currently deployed and either: - Deploy if needed - Archive if deprecated - Document why they exist but aren't deployed 4. **Standardize Naming**: Some stacks use `-stack` suffix, others don't. Consider standardizing. """) return "\n".join(report) def generate_infrastructure_overview(): """Generate infrastructure overview document""" report = [] report.append("# Homelab Infrastructure Overview") report.append(f"\n*Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}*") report.append("\n---\n") report.append("## Server Inventory\n") report.append("| Server | Type | Endpoint ID | Status | Total Containers |") report.append("|--------|------|-------------|--------|------------------|") server_info = [ ("Atlantis", "Local Docker", 2, "🟢 Online", "41"), ("Concord NUC", "Edge Agent", 443395, "🟢 Online", "15"), ("Calypso (vish-nuc)", "Edge Agent", 443397, "🟢 Online", "45"), ("vish-nuc-edge", "Edge Agent", 443398, "🟢 Online", "3"), ("Homelab VM", "Edge Agent", 443399, "🟢 Online", "20"), ] for server, type_, eid, status, containers in server_info: report.append(f"| {server} | {type_} | {eid} | {status} | {containers} |") report.append("\n## Service Categories\n") categories = { "Media Management": ["arr-stack (Atlantis)", "arr-stack (Calypso)", "plex", "jellyseerr", "tautulli"], "Photo Management": ["Immich (Atlantis)", "Immich (Calypso)"], "Document Management": ["PaperlessNGX", "Joplin"], "Network & DNS": ["AdGuard (Concord NUC)", "AdGuard (Calypso)", "WireGuard", "DynDNS"], "Home Automation": ["Home Assistant", "Matter Server"], "Development & DevOps": ["Gitea", "Portainer", "OpenHands"], "Communication": ["Matrix/Synapse", "Jitsi", "Signal API"], "Monitoring": ["Prometheus", "Grafana", "Uptime Kuma", "Glances", "WatchYourLAN"], "Security": ["Vaultwarden/Bitwarden"], "File Sync": ["Syncthing", "Seafile"], "Privacy Tools": ["Invidious", "Libreddit/Redlib", "Binternet"], "Productivity": ["Draw.io", "Reactive Resume", "ArchiveBox", "Hoarder/Karakeep"], } for category, services in categories.items(): report.append(f"### {category}\n") for service in services: report.append(f"- {service}") report.append("") return "\n".join(report) if __name__ == "__main__": # Generate comparison report comparison_report = generate_markdown_report() with open("/workspace/homelab/docs/STACK_COMPARISON_REPORT.md", "w") as f: f.write(comparison_report) print("Generated: docs/STACK_COMPARISON_REPORT.md") # Generate infrastructure overview infra_report = generate_infrastructure_overview() with open("/workspace/homelab/docs/INFRASTRUCTURE_OVERVIEW.md", "w") as f: f.write(infra_report) print("Generated: docs/INFRASTRUCTURE_OVERVIEW.md")