Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
This commit is contained in:
@@ -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()]
|
||||
Reference in New Issue
Block a user