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
+101
View File
@@ -0,0 +1,101 @@
"""APT: update/upgrade, списки пакетов, установка/удаление."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, List
from app.core.ssh import SSHClient
SYSTEM_PRIORITIES = {"required", "important"}
@dataclass
class Package:
name: str
version: str
priority: str
manual: bool
upgradable: bool = False
candidate: str = ""
@property
def is_system(self) -> bool:
return self.priority in SYSTEM_PRIORITIES
async def apt_update(c: SSHClient) -> str:
r = await c.sudo("DEBIAN_FRONTEND=noninteractive apt-get update", timeout=600)
return r.stdout + r.stderr
async def list_upgradable(c: SSHClient) -> List[dict]:
r = await c.run("apt list --upgradable 2>/dev/null | tail -n +2")
out = []
for line in r.stdout.splitlines():
if not line.strip():
continue
# nginx/stable 1.22.1-9+deb12u1 amd64 [upgradable from: 1.22.1-9]
parts = line.split()
if len(parts) >= 2:
name = parts[0].split("/")[0]
out.append({"name": name, "candidate": parts[1], "raw": line})
return out
async def list_packages(c: SSHClient) -> List[Package]:
"""Все установленные пакеты с приоритетом и признаком manual."""
r = await c.run(
"dpkg-query -W -f='${Package}\\t${Version}\\t${Priority}\\n'", timeout=180
)
manual_r = await c.run("apt-mark showmanual", timeout=120)
manual = set(manual_r.stdout.split())
pkgs: List[Package] = []
for line in r.stdout.splitlines():
parts = line.split("\t")
if len(parts) == 3:
name, version, prio = parts
pkgs.append(Package(name, version, prio, name in manual))
return pkgs
def split_packages(pkgs: List[Package]) -> Dict[str, List[Package]]:
"""Разбивка на системные / пользовательские / зависимости."""
system = [p for p in pkgs if p.is_system]
user = [p for p in pkgs if not p.is_system and p.manual]
deps = [p for p in pkgs if not p.is_system and not p.manual]
return {"system": system, "user": user, "deps": deps}
async def apt_upgrade(c: SSHClient, dist: bool = False):
"""Стриминг upgrade. Возвращает async-итератор строк."""
cmd = "DEBIAN_FRONTEND=noninteractive apt-get {} -y".format(
"dist-upgrade" if dist else "upgrade"
)
return c.run_stream(cmd, use_sudo=True)
async def install(c: SSHClient, names: List[str]):
cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -y " + " ".join(names)
return c.run_stream(cmd, use_sudo=True)
async def remove(c: SSHClient, names: List[str], purge: bool = False):
action = "purge" if purge else "remove"
cmd = f"DEBIAN_FRONTEND=noninteractive apt-get {action} -y " + " ".join(names)
return c.run_stream(cmd, use_sudo=True)
async def autoremove(c: SSHClient):
return c.run_stream(
"DEBIAN_FRONTEND=noninteractive apt-get autoremove -y", use_sudo=True
)
async def apt_show(c: SSHClient, name: str) -> str:
r = await c.run(f"apt-cache show {name} 2>/dev/null | head -40")
return r.stdout
async def apt_search(c: SSHClient, query: str) -> List[str]:
r = await c.run(f"apt-cache search {query} 2>/dev/null | head -50")
return [l for l in r.stdout.splitlines() if l.strip()]