3 neue Datenquellen: Forgejo (Issues/Commits), Seafile (Cloud-Speicher), PBS (Backups)
This commit is contained in:
parent
c5fb45e532
commit
1f4e5ed388
5 changed files with 350 additions and 0 deletions
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
homelab-ai-bot/core/forgejo_client.py
Normal file
91
homelab-ai-bot/core/forgejo_client.py
Normal 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)
|
||||||
118
homelab-ai-bot/core/pbs_client.py
Normal file
118
homelab-ai-bot/core/pbs_client.py
Normal 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)
|
||||||
95
homelab-ai-bot/core/seafile_client.py
Normal file
95
homelab-ai-bot/core/seafile_client.py
Normal 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)
|
||||||
|
|
@ -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": []},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue