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__))
|
||||
from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client
|
||||
from core import forgejo_client, seafile_client, pbs_client
|
||||
|
||||
|
||||
def _load_config():
|
||||
|
|
@ -147,6 +148,24 @@ def _tool_get_wordpress_stats() -> str:
|
|||
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:
|
||||
cfg = _load_config()
|
||||
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_wordpress_stats": lambda: _tool_get_wordpress_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": []},
|
||||
},
|
||||
},
|
||||
{
|
||||
"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