Files
homelab-optimized/scripts/generate_stack_comparison.py
Gitea Mirror Bot 5cdf36e545
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-04-05 12:11:15 UTC
2026-04-05 12:11:15 +00:00

336 lines
17 KiB
Python

#!/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")