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
+110
View File
@@ -0,0 +1,110 @@
"""Gitea: репозитории, raw-файлы, деплой compose на сервер."""
from __future__ import annotations
from typing import List, Optional
import httpx
from app.core.ssh import SSHClient
from app.db.database import get_session
from app.db.models import Setting
DEFAULT_URL = "http://192.168.1.171:3000"
def _get(key: str) -> str:
with get_session() as s:
row = s.get(Setting, key)
return row.value if row else ""
def _set(key: str, value: str) -> None:
with get_session() as s:
row = s.get(Setting, key) or Setting(key=key)
row.value = value
s.add(row)
s.commit()
def get_config() -> dict:
return {"url": _get("gitea_url") or DEFAULT_URL, "token": _get("gitea_token")}
def save_config(url: str, token: str) -> None:
_set("gitea_url", url.rstrip("/"))
_set("gitea_token", token)
async def _client() -> httpx.AsyncClient:
cfg = get_config()
headers = {"Authorization": f"token {cfg['token']}"} if cfg["token"] else {}
return httpx.AsyncClient(base_url=cfg["url"], headers=headers, timeout=20)
async def list_repos() -> List[dict]:
async with await _client() as cl:
r = await cl.get("/api/v1/user/repos", params={"limit": 50})
r.raise_for_status()
return [{"name": x["name"], "full_name": x["full_name"],
"clone_url": x["clone_url"], "default_branch": x["default_branch"]}
for x in r.json()]
async def get_file(repo: str, path: str, ref: str = "main") -> str:
async with await _client() as cl:
r = await cl.get(f"/api/v1/repos/{repo}/raw/{path}", params={"ref": ref})
r.raise_for_status()
return r.text
async def test_connection() -> tuple[bool, str]:
try:
async with await _client() as cl:
r = await cl.get("/api/v1/version")
if r.status_code == 200:
return True, f"Gitea {r.json().get('version')}"
return False, f"HTTP {r.status_code}"
except Exception as e:
return False, str(e)
# ---------- деплой compose ----------
async def deploy(c: SSHClient, clone_url: str, repo_name: str, branch: str,
compose_path: str, env_content: Optional[str], log) -> None:
"""git clone/pull в /opt/stacks/<repo> + docker compose up -d."""
base = "/opt/stacks"
workdir = f"{base}/{repo_name}"
await c.sudo(f"mkdir -p {base}")
r = await c.sudo(f"test -d {workdir}/.git && echo exists || echo missing")
if "exists" in r.stdout:
log(f"git pull в {workdir}")
r = await c.sudo(f"git -C {workdir} fetch --all && git -C {workdir} checkout {branch} "
f"&& git -C {workdir} pull origin {branch}", timeout=300)
else:
log(f"git clone {clone_url} → {workdir}")
url = clone_url
r = await c.sudo(f"git clone -b {branch} {url} {workdir}", timeout=300)
log(r.stdout[-1000:] + r.stderr[-500:])
if not r.ok:
raise RuntimeError("git операция не удалась")
if env_content is not None:
import base64
b64 = base64.b64encode(env_content.encode()).decode()
await c.sudo(f"echo {b64} | base64 -d > {workdir}/.env")
log(".env записан")
log("docker compose up -d")
r = await c.sudo(f"docker compose -f {workdir}/{compose_path} up -d", timeout=900)
log(r.stdout + r.stderr)
if not r.ok:
raise RuntimeError("compose up не удался")
async def stack_action(c: SSHClient, repo_name: str, compose_path: str, action: str) -> str:
assert action in ("up -d", "down", "restart", "pull", "logs --tail 100", "ps")
r = await c.sudo(f"docker compose -f /opt/stacks/{repo_name}/{compose_path} {action}",
timeout=600)
return r.stdout + r.stderr