3 neue Datenquellen: Forgejo (Issues/Commits), Seafile (Cloud-Speicher), PBS (Backups)

This commit is contained in:
root 2026-03-09 14:50:08 +07:00
parent c5fb45e532
commit 1f4e5ed388
5 changed files with 350 additions and 0 deletions

View file

@ -7,6 +7,7 @@ import re
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client
from core import forgejo_client, seafile_client, pbs_client
def _load_config(): def _load_config():
@ -147,6 +148,24 @@ def _tool_get_wordpress_stats() -> str:
return wordpress_client.format_overview(cfg) return wordpress_client.format_overview(cfg)
def _tool_get_forgejo_status() -> str:
cfg = _load_config()
forgejo_client.init(cfg)
return forgejo_client.format_overview()
def _tool_get_seafile_status() -> str:
cfg = _load_config()
seafile_client.init(cfg)
return seafile_client.format_overview()
def _tool_get_backup_status() -> str:
cfg = _load_config()
pbs_client.init(cfg)
return pbs_client.format_overview()
def _tool_get_feed_stats() -> str: def _tool_get_feed_stats() -> str:
cfg = _load_config() cfg = _load_config()
ct_109 = config.get_container(cfg, vmid=109) ct_109 = config.get_container(cfg, vmid=109)
@ -179,4 +198,7 @@ def get_tool_handlers() -> dict:
"get_server_warnings": lambda: _tool_get_server_warnings(), "get_server_warnings": lambda: _tool_get_server_warnings(),
"get_wordpress_stats": lambda: _tool_get_wordpress_stats(), "get_wordpress_stats": lambda: _tool_get_wordpress_stats(),
"get_feed_stats": lambda: _tool_get_feed_stats(), "get_feed_stats": lambda: _tool_get_feed_stats(),
"get_forgejo_status": lambda: _tool_get_forgejo_status(),
"get_seafile_status": lambda: _tool_get_seafile_status(),
"get_backup_status": lambda: _tool_get_backup_status(),
} }

View file

@ -0,0 +1,91 @@
"""Forgejo REST API Client — Issues, Repos, Commits."""
import requests
from typing import Optional
FORGEJO_URL = ""
FORGEJO_TOKEN = ""
REPO = "orbitalo/homelab-brain"
def init(cfg):
global FORGEJO_URL, FORGEJO_TOKEN
ct_111 = None
for c in cfg.containers:
if c.vmid == 111 and c.host == "pve-hetzner":
ct_111 = c
break
ip = ct_111.tailscale_ip if ct_111 else "100.89.246.60"
FORGEJO_URL = f"http://{ip}:3000/api/v1"
FORGEJO_TOKEN = cfg.api_keys.get("forgejo_token", cfg.raw.get("FORGEJO_TOKEN", ""))
def _get(endpoint: str, params: dict = None) -> Optional[dict | list]:
if not FORGEJO_URL or not FORGEJO_TOKEN:
return None
try:
r = requests.get(
f"{FORGEJO_URL}{endpoint}",
headers={"Authorization": f"token {FORGEJO_TOKEN}"},
params=params or {},
timeout=10,
)
r.raise_for_status()
return r.json()
except Exception:
return None
def get_open_issues(repo: str = REPO) -> list[dict]:
data = _get(f"/repos/{repo}/issues", {"state": "open", "limit": 20})
if not data:
return []
results = []
for i in data:
labels = [l["name"] for l in i.get("labels", [])]
results.append({
"number": i["number"],
"title": i["title"],
"labels": labels,
"created": i["created_at"][:10],
})
return results
def get_recent_commits(repo: str = REPO, limit: int = 10) -> list[dict]:
data = _get(f"/repos/{repo}/commits", {"limit": limit})
if not data:
return []
results = []
for c in data:
msg = c["commit"]["message"].split("\n")[0][:80]
date = c["commit"]["author"]["date"][:16]
results.append({"date": date, "message": msg})
return results
def get_repos() -> list[dict]:
data = _get("/repos/search", {"limit": 20})
if not data or "data" not in data:
return []
return [{"name": r["full_name"], "issues": r["open_issues_count"]} for r in data["data"]]
def format_overview() -> str:
issues = get_open_issues()
commits = get_recent_commits(limit=5)
lines = [f"Forgejo — {len(issues)} offene Issues\n"]
if issues:
for i in issues:
lbl = f" [{', '.join(i['labels'])}]" if i["labels"] else ""
lines.append(f" #{i['number']}: {i['title']}{lbl}")
else:
lines.append(" Keine offenen Issues.")
if commits:
lines.append("\nLetzte Commits:")
for c in commits:
lines.append(f" {c['date']} {c['message']}")
return "\n".join(lines)

View file

@ -0,0 +1,118 @@
"""Proxmox Backup Server REST API Client — Backup-Status und Datastore-Info."""
import requests
import urllib3
from datetime import datetime
from typing import Optional
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PBS_URL = ""
PBS_USER = "root@pam"
PBS_PASS = ""
_ticket_cache = ""
def init(cfg):
global PBS_URL, PBS_PASS, _ticket_cache
_ticket_cache = ""
pbs_ip = cfg.raw.get("SRV_PBS_MU", "100.99.139.22")
PBS_URL = f"https://{pbs_ip}:8007/api2/json"
PBS_PASS = cfg.passwords.get("default", "")
def _get_ticket() -> str:
global _ticket_cache
if _ticket_cache:
return _ticket_cache
try:
r = requests.post(
f"{PBS_URL}/access/ticket",
data={"username": PBS_USER, "password": PBS_PASS},
verify=False, timeout=5,
)
r.raise_for_status()
_ticket_cache = r.json()["data"]["ticket"]
return _ticket_cache
except Exception:
return ""
def _get(endpoint: str) -> Optional[dict | list]:
ticket = _get_ticket()
if not ticket:
return None
try:
r = requests.get(
f"{PBS_URL}{endpoint}",
cookies={"PBSAuthCookie": ticket},
verify=False, timeout=10,
)
r.raise_for_status()
return r.json().get("data")
except Exception:
return None
def get_datastore_usage() -> list[dict]:
data = _get("/status/datastore-usage")
if not data:
return []
results = []
for d in data:
total = d.get("total", 0) / 1024**3
used = d.get("used", 0) / 1024**3
avail = d.get("avail", 0) / 1024**3
pct = (used / total * 100) if total > 0 else 0
results.append({
"store": d.get("store", "?"),
"total_gb": total,
"used_gb": used,
"avail_gb": avail,
"used_pct": pct,
})
return results
def get_recent_snapshots(datastore: str = "nvme-pool", limit: int = 10) -> list[dict]:
data = _get(f"/admin/datastore/{datastore}/snapshots")
if not data:
return []
data.sort(key=lambda s: s.get("backup-time", 0), reverse=True)
results = []
for s in data[:limit]:
ts = datetime.fromtimestamp(s.get("backup-time", 0))
results.append({
"id": s.get("backup-id", "?"),
"type": s.get("backup-type", "?"),
"time": ts.strftime("%Y-%m-%d %H:%M"),
"size_gb": s.get("size", 0) / 1024**3,
})
return results
def get_snapshot_count(datastore: str = "nvme-pool") -> int:
data = _get(f"/admin/datastore/{datastore}/snapshots")
return len(data) if data else 0
def format_overview() -> str:
stores = get_datastore_usage()
if not stores:
return "PBS nicht erreichbar."
lines = ["PBS Muldenstein — Backup-Status\n"]
lines.append("Datastores:")
for s in stores:
if s["total_gb"] < 1:
continue
lines.append(f" {s['store']}: {s['used_gb']:.0f}/{s['total_gb']:.0f} GB ({s['used_pct']:.0f}%)")
snaps = get_recent_snapshots(limit=5)
total_snaps = get_snapshot_count()
if snaps:
lines.append(f"\nLetzte Backups ({total_snaps} total):")
for s in snaps:
lines.append(f" {s['time']} CT {s['id']} ({s['size_gb']:.1f} GB)")
return "\n".join(lines)

View file

@ -0,0 +1,95 @@
"""Seafile REST API Client — Speicherplatz und Bibliotheken."""
import requests
from typing import Optional
SEAFILE_URL = ""
SEAFILE_USER = ""
SEAFILE_PASS = ""
_token_cache = ""
def init(cfg):
global SEAFILE_URL, SEAFILE_USER, SEAFILE_PASS, _token_cache
_token_cache = ""
ct_103 = None
for c in cfg.containers:
if c.vmid == 103 and c.host == "pve-hetzner":
ct_103 = c
break
ip = "10.10.10.103"
SEAFILE_URL = f"http://{ip}:8080"
SEAFILE_USER = cfg.raw.get("SEAFILE_ADMIN_EMAIL", "admin@orbitalo.net")
SEAFILE_PASS = cfg.passwords.get("default", "")
def _get_token() -> str:
global _token_cache
if _token_cache:
return _token_cache
try:
r = requests.post(
f"{SEAFILE_URL}/api2/auth-token/",
data={"username": SEAFILE_USER, "password": SEAFILE_PASS},
timeout=5,
)
r.raise_for_status()
_token_cache = r.json()["token"]
return _token_cache
except Exception:
return ""
def _get(endpoint: str) -> Optional[dict | list]:
token = _get_token()
if not token:
return None
try:
r = requests.get(
f"{SEAFILE_URL}{endpoint}",
headers={"Authorization": f"Token {token}"},
timeout=10,
)
r.raise_for_status()
return r.json()
except Exception:
return None
def get_account_info() -> dict:
data = _get("/api2/account/info/")
if not data:
return {"error": "nicht erreichbar"}
return {
"usage_gb": data.get("usage", 0) / 1024**3,
"total_gb": data.get("total", 0) / 1024**3,
"email": data.get("email", ""),
}
def get_libraries() -> list[dict]:
data = _get("/api2/repos/")
if not data:
return []
results = []
for r in data:
results.append({
"name": r.get("name", "?"),
"size_mb": r.get("size", 0) / 1024**2,
"encrypted": r.get("encrypted", False),
})
return results
def format_overview() -> str:
acct = get_account_info()
if "error" in acct:
return "Seafile nicht erreichbar."
libs = get_libraries()
lines = [f"Seafile — {acct['usage_gb']:.1f} GB belegt\n"]
if libs:
lines.append("Bibliotheken:")
for lib in libs:
lines.append(f" {lib['name']}: {lib['size_mb']:.0f} MB")
return "\n".join(lines)

View file

@ -122,6 +122,30 @@ TOOLS = [
"parameters": {"type": "object", "properties": {}, "required": []}, "parameters": {"type": "object", "properties": {}, "required": []},
}, },
}, },
{
"type": "function",
"function": {
"name": "get_forgejo_status",
"description": "Forgejo Git-Server: Offene Issues/TODOs, letzte Commits, Repos",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_seafile_status",
"description": "Seafile Cloud-Speicher: Belegter Speicherplatz, Bibliotheken",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
{
"type": "function",
"function": {
"name": "get_backup_status",
"description": "Proxmox Backup Server (PBS): Datastore-Belegung, letzte Backups, Snapshot-Anzahl",
"parameters": {"type": "object", "properties": {}, "required": []},
},
},
] ]