Add Gitea health check
This commit is contained in:
@@ -89,6 +89,12 @@ Token flow:
|
|||||||
- First token: `POST /api/tokens` with `identity` and `secret`.
|
- First token: `POST /api/tokens` with `identity` and `secret`.
|
||||||
- Refresh: `GET /api/tokens` using the cached token.
|
- Refresh: `GET /api/tokens` using the cached token.
|
||||||
|
|
||||||
|
## gitea
|
||||||
|
|
||||||
|
- `base_url` (string): Gitea base url, for example `http://localhost:3000`.
|
||||||
|
- `token` (string): Optional API token.
|
||||||
|
- `verify_tls` (bool): Set to `false` for self-signed TLS.
|
||||||
|
|
||||||
## security
|
## security
|
||||||
|
|
||||||
- `reboot_password` (string): Password required before reboot.
|
- `reboot_password` (string): Password required before reboot.
|
||||||
|
|||||||
@@ -89,6 +89,12 @@
|
|||||||
- первый токен: `POST /api/tokens` с `identity` и `secret`.
|
- первый токен: `POST /api/tokens` с `identity` и `secret`.
|
||||||
- refresh: `GET /api/tokens` с текущим токеном.
|
- refresh: `GET /api/tokens` с текущим токеном.
|
||||||
|
|
||||||
|
## gitea
|
||||||
|
|
||||||
|
- `base_url` (string): base url Gitea, например `http://localhost:3000`.
|
||||||
|
- `token` (string): опциональный API токен.
|
||||||
|
- `verify_tls` (bool): `false` для self-signed TLS.
|
||||||
|
|
||||||
## security
|
## security
|
||||||
|
|
||||||
- `reboot_password` (string): пароль для подтверждения reboot.
|
- `reboot_password` (string): пароль для подтверждения reboot.
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ npmplus:
|
|||||||
cooldown_sec: 86400
|
cooldown_sec: 86400
|
||||||
interval_sec: 3600
|
interval_sec: 3600
|
||||||
|
|
||||||
|
gitea:
|
||||||
|
base_url: "http://localhost:3000"
|
||||||
|
# Optional API token for private instances
|
||||||
|
token: ""
|
||||||
|
verify_tls: true
|
||||||
|
|
||||||
security:
|
security:
|
||||||
reboot_password: "CHANGE_ME"
|
reboot_password: "CHANGE_ME"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from services.queue import enqueue
|
|||||||
from services.updates import list_updates, apply_updates
|
from services.updates import list_updates, apply_updates
|
||||||
from services.runner import run_cmd
|
from services.runner import run_cmd
|
||||||
from services.npmplus import fetch_certificates, format_certificates, list_proxy_hosts, set_proxy_host
|
from services.npmplus import fetch_certificates, format_certificates, list_proxy_hosts, set_proxy_host
|
||||||
|
from services.gitea import get_gitea_health
|
||||||
import state
|
import state
|
||||||
from state import UPDATES_CACHE, REBOOT_PENDING
|
from state import UPDATES_CACHE, REBOOT_PENDING
|
||||||
from services.metrics import summarize
|
from services.metrics import summarize
|
||||||
@@ -246,6 +247,23 @@ async def ssl_certs(msg: Message):
|
|||||||
asyncio.create_task(worker())
|
asyncio.create_task(worker())
|
||||||
|
|
||||||
|
|
||||||
|
@dp.message(F.text == "🍵 Gitea")
|
||||||
|
async def gitea_health(msg: Message):
|
||||||
|
if not is_admin_msg(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.answer("⏳ Checking Gitea health…", reply_markup=system_logs_kb)
|
||||||
|
|
||||||
|
async def worker():
|
||||||
|
try:
|
||||||
|
text = await asyncio.to_thread(get_gitea_health, cfg)
|
||||||
|
except Exception as e:
|
||||||
|
text = f"⚠️ Gitea error: {e}"
|
||||||
|
await msg.answer(text, reply_markup=system_logs_kb)
|
||||||
|
|
||||||
|
asyncio.create_task(worker())
|
||||||
|
|
||||||
|
|
||||||
@dp.message(F.text == "🧩 NPMplus")
|
@dp.message(F.text == "🧩 NPMplus")
|
||||||
async def npmplus_hosts(msg: Message):
|
async def npmplus_hosts(msg: Message):
|
||||||
if not is_admin_msg(msg):
|
if not is_admin_msg(msg):
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ system_logs_kb = ReplyKeyboardMarkup(
|
|||||||
keyboard=[
|
keyboard=[
|
||||||
[KeyboardButton(text="🧾 Audit"), KeyboardButton(text="📣 Incidents")],
|
[KeyboardButton(text="🧾 Audit"), KeyboardButton(text="📣 Incidents")],
|
||||||
[KeyboardButton(text="🧰 Processes"), KeyboardButton(text="🔒 SSL")],
|
[KeyboardButton(text="🧰 Processes"), KeyboardButton(text="🔒 SSL")],
|
||||||
[KeyboardButton(text="🔑 SSH log"), KeyboardButton(text="🧩 NPMplus")],
|
[KeyboardButton(text="🔑 SSH log"), KeyboardButton(text="🧩 NPMplus"), KeyboardButton(text="🍵 Gitea")],
|
||||||
[KeyboardButton(text="🌍 External"), KeyboardButton(text="🌐 URLs")],
|
[KeyboardButton(text="🌍 External"), KeyboardButton(text="🌐 URLs")],
|
||||||
[KeyboardButton(text="⬅️ System")],
|
[KeyboardButton(text="⬅️ System")],
|
||||||
],
|
],
|
||||||
|
|||||||
88
services/gitea.py
Normal file
88
services/gitea.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
from typing import Any
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
|
def _request(url: str, headers: dict[str, str], verify_tls: bool) -> tuple[int, str]:
|
||||||
|
context = None
|
||||||
|
if not verify_tls:
|
||||||
|
context = ssl._create_unverified_context() # nosec - config-controlled
|
||||||
|
|
||||||
|
req = Request(url, headers=headers)
|
||||||
|
try:
|
||||||
|
with urlopen(req, timeout=10, context=context) as resp:
|
||||||
|
body = resp.read().decode("utf-8")
|
||||||
|
return int(resp.status), body
|
||||||
|
except HTTPError as e:
|
||||||
|
try:
|
||||||
|
body = e.read().decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
body = ""
|
||||||
|
return int(e.code), body
|
||||||
|
except URLError as e:
|
||||||
|
raise RuntimeError(str(e.reason)) from e
|
||||||
|
|
||||||
|
|
||||||
|
def _api_base(cfg: dict[str, Any]) -> str:
|
||||||
|
g_cfg = cfg.get("gitea", {})
|
||||||
|
base = (g_cfg.get("base_url") or "").rstrip("/")
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def get_gitea_health(cfg: dict[str, Any]) -> str:
|
||||||
|
g_cfg = cfg.get("gitea", {})
|
||||||
|
base = _api_base(cfg)
|
||||||
|
verify_tls = g_cfg.get("verify_tls", True)
|
||||||
|
if not base:
|
||||||
|
return "⚠️ Gitea base_url not configured"
|
||||||
|
|
||||||
|
token = (g_cfg.get("token") or "").strip()
|
||||||
|
headers = {"User-Agent": "tg-admin-bot"}
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"token {token}"
|
||||||
|
|
||||||
|
lines = ["🍵 Gitea\n"]
|
||||||
|
|
||||||
|
health_paths = ["/api/healthz", "/api/v1/healthz"]
|
||||||
|
health_status = None
|
||||||
|
health_payload = None
|
||||||
|
for path in health_paths:
|
||||||
|
status, body = _request(f"{base}{path}", headers, verify_tls)
|
||||||
|
if status == 200:
|
||||||
|
health_status = (status, path)
|
||||||
|
try:
|
||||||
|
health_payload = json.loads(body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
health_payload = None
|
||||||
|
break
|
||||||
|
if status not in (404, 405):
|
||||||
|
health_status = (status, path)
|
||||||
|
break
|
||||||
|
|
||||||
|
if health_status:
|
||||||
|
status, path = health_status
|
||||||
|
icon = "🟢" if status == 200 else "🔴"
|
||||||
|
if status == 200 and isinstance(health_payload, dict):
|
||||||
|
state = health_payload.get("status") or "ok"
|
||||||
|
checks = health_payload.get("checks") or {}
|
||||||
|
checks_total = len(checks) if isinstance(checks, dict) else 0
|
||||||
|
lines.append(f"{icon} API health: {state} ({checks_total} checks)")
|
||||||
|
else:
|
||||||
|
lines.append(f"{icon} API health: {status} ({path})")
|
||||||
|
else:
|
||||||
|
lines.append("🟡 API health: endpoint not found")
|
||||||
|
|
||||||
|
ver_status, ver_body = _request(f"{base}/api/v1/version", headers, verify_tls)
|
||||||
|
if ver_status == 200:
|
||||||
|
try:
|
||||||
|
payload = json.loads(ver_body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
payload = {}
|
||||||
|
version = payload.get("version") or "unknown"
|
||||||
|
lines.append(f"ℹ️ Version: {version}")
|
||||||
|
else:
|
||||||
|
lines.append(f"🟡 Version: HTTP {ver_status}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
Reference in New Issue
Block a user