Initial commit: Debian Manager — управление Debian-серверами

- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
dinlo
2026-09-05 19:46:45 +08:00
commit f624fee5e0
65 changed files with 6981 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
"""Webhook-сервер для автодеплоя из Gitea (push → redeploy)."""
from __future__ import annotations
import asyncio
import json
from typing import Optional
from sqlmodel import select
from app.core import servers as servers_core
from app.db.database import get_session
from app.db.models import WebhookBinding
_server = None
def list_bindings() -> list[WebhookBinding]:
with get_session() as s:
return list(s.exec(select(WebhookBinding)).all())
def add_binding(**fields) -> WebhookBinding:
with get_session() as s:
b = WebhookBinding(**fields)
s.add(b)
s.commit()
s.refresh(b)
return b
def delete_binding(bid: int) -> None:
with get_session() as s:
b = s.get(WebhookBinding, bid)
if b:
s.delete(b)
s.commit()
async def _handler(reader, writer):
"""Минимальный HTTP-сервер для /webhooks/gitea."""
try:
req = await reader.readuntil(b"\r\n\r\n")
head = req.decode(errors="replace")
first = head.splitlines()[0] if head else ""
# тело
length = 0
for line in head.splitlines():
if line.lower().startswith("content-length:"):
length = int(line.split(":", 1)[1].strip())
body = await reader.readexactly(length) if length else b""
status, text = 200, "ok"
if first.startswith("POST /webhooks/gitea"):
try:
payload = json.loads(body or b"{}")
repo = payload.get("repository", {}).get("full_name", "")
ref = payload.get("ref", "") # refs/heads/main
branch = ref.rsplit("/", 1)[-1]
asyncio.ensure_future(_deploy(repo, branch))
text = f"deploy scheduled: {repo}@{branch}"
except Exception as e:
status, text = 400, f"bad payload: {e}"
else:
status, text = 404, "not found"
writer.write(f"HTTP/1.1 {status} OK\r\nContent-Length: {len(text)}\r\n"
f"Connection: close\r\n\r\n{text}".encode())
await writer.drain()
except Exception:
pass
finally:
try:
writer.close()
except Exception:
pass
async def _deploy(repo: str, branch: str) -> None:
with get_session() as s:
bindings = list(s.exec(select(WebhookBinding)
.where(WebhookBinding.repo == repo,
WebhookBinding.branch == branch,
WebhookBinding.enabled == True)).all()) # noqa: E712
for b in bindings:
srv = servers_core.get_server(b.server_id)
if not srv:
continue
try:
from app.core import gitea as g
from app.core.ssh import SSHClient
from app.core.tasks import tasks
async def job(log, srv=srv, b=b):
async with SSHClient(srv) as c:
cfg = g.get_config()
clone_url = f"{cfg['url']}/{repo}.git"
await g.deploy(c, clone_url, repo.split("/")[-1],
b.branch, b.compose_path, b.env or None, log)
tasks.start("webhook", f"auto-deploy {repo}@{branch}", srv.id, job)
except Exception:
pass
async def start(port: int = 9100) -> None:
global _server
if _server:
return
_server = await asyncio.start_server(_handler, "0.0.0.0", port)
def is_running() -> bool:
return _server is not None