Sanitized mirror from private repository - 2026-04-19 08:28:02 UTC
This commit is contained in:
335
scripts/generate_stack_comparison.py
Normal file
335
scripts/generate_stack_comparison.py
Normal file
@@ -0,0 +1,335 @@
|
||||
#!/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")
|
||||
Reference in New Issue
Block a user