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,296 @@
|
||||
"""Вкладка Docker: контейнеры, образы, тома, сети, compose."""
|
||||
import flet as ft
|
||||
|
||||
from app.core import docker_svc as d
|
||||
from app.core.ssh import SSHClient
|
||||
from app.core.tasks import tasks
|
||||
from app.db.models import Server
|
||||
from app.ui.components import append_log, confirm_dialog, log_view, snack
|
||||
|
||||
|
||||
class DockerTab(ft.Column):
|
||||
def __init__(self, page: ft.Page, srv: Server):
|
||||
super().__init__(expand=True, spacing=8)
|
||||
self._page = page
|
||||
self.srv = srv
|
||||
self.inner_tabs = ft.Tabs(
|
||||
selected_index=0, length=5, expand=True,
|
||||
content=ft.Column(
|
||||
expand=True,
|
||||
controls=[
|
||||
ft.TabBar(tabs=[
|
||||
ft.Tab(label="Контейнеры"),
|
||||
ft.Tab(label="Образы"),
|
||||
ft.Tab(label="Тома"),
|
||||
ft.Tab(label="Сети"),
|
||||
ft.Tab(label="Система"),
|
||||
]),
|
||||
ft.TabBarView(
|
||||
expand=True,
|
||||
controls=[
|
||||
self._containers_view(),
|
||||
self._images_view(),
|
||||
self._volumes_view(),
|
||||
self._networks_view(),
|
||||
self._system_view(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
self.log = log_view()
|
||||
self.log_container = ft.Container(
|
||||
height=200, border=ft.Border.all(1, ft.Colors.GREY_800),
|
||||
border_radius=8, padding=8, content=self.log, visible=False)
|
||||
self.controls = [ft.Container(height=8), self.inner_tabs, self.log_container]
|
||||
self._page.run_task(self._check)
|
||||
|
||||
async def _check(self):
|
||||
try:
|
||||
async with SSHClient(self.srv) as c:
|
||||
if not await d.available(c):
|
||||
snack(self._page, "Docker не установлен на сервере", error=True)
|
||||
return
|
||||
await self._load_containers()
|
||||
await self._load_images()
|
||||
await self._load_volumes()
|
||||
await self._load_networks()
|
||||
await self._load_system()
|
||||
except Exception as e:
|
||||
snack(self._page, f"Docker: {e}", error=True)
|
||||
|
||||
# ---------- контейнеры ----------
|
||||
|
||||
def _containers_view(self):
|
||||
self.ct_table = ft.DataTable(columns=[
|
||||
ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Образ")),
|
||||
ft.DataColumn(ft.Text("Статус")), ft.DataColumn(ft.Text("Порты")),
|
||||
ft.DataColumn(ft.Text("Действия")),
|
||||
], rows=[])
|
||||
self.stats_row = ft.Row(spacing=16, wrap=True)
|
||||
return ft.Column([
|
||||
ft.Row([
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load_containers)),
|
||||
]),
|
||||
ft.Container(expand=True, content=ft.Column(
|
||||
[self.ct_table, ft.Divider(), ft.Text("Нагрузка:", weight=ft.FontWeight.BOLD), self.stats_row],
|
||||
scroll=ft.ScrollMode.AUTO)),
|
||||
], expand=True)
|
||||
|
||||
async def _load_containers(self):
|
||||
async with SSHClient(self.srv) as c:
|
||||
containers = await d.list_containers(c)
|
||||
try:
|
||||
stats = {s["Name"]: s for s in await d.container_stats(c)}
|
||||
except Exception:
|
||||
stats = {}
|
||||
self.ct_table.rows = []
|
||||
for ct in containers:
|
||||
running = "Up" in ct.get("Status", "")
|
||||
cid = ct["ID"]
|
||||
name = ct["Names"]
|
||||
self.ct_table.rows.append(ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(name, weight=ft.FontWeight.BOLD)),
|
||||
ft.DataCell(ft.Text(ct["Image"], size=12)),
|
||||
ft.DataCell(ft.Text(ct["Status"], size=12,
|
||||
color=ft.Colors.GREEN_300 if running else ft.Colors.GREY_500)),
|
||||
ft.DataCell(ft.Text(ct.get("Ports", "") or "—", size=11)),
|
||||
ft.DataCell(ft.Row([
|
||||
ft.IconButton(ft.Icons.PLAY_ARROW if not running else ft.Icons.STOP,
|
||||
icon_size=18, tooltip="start" if not running else "stop",
|
||||
on_click=lambda e, c=cid, a="start" if not running else "stop": self._ct_action(c, a)),
|
||||
ft.IconButton(ft.Icons.RESTART_ALT, icon_size=18, tooltip="restart",
|
||||
on_click=lambda e, c=cid: self._ct_action(c, "restart")),
|
||||
ft.IconButton(ft.Icons.ARTICLE, icon_size=18, tooltip="логи",
|
||||
on_click=lambda e, c=cid, n=name: self._show_logs(c, n)),
|
||||
ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=18, tooltip="удалить",
|
||||
on_click=lambda e, c=cid, n=name: self._remove_ct(c, n)),
|
||||
], spacing=0)),
|
||||
]))
|
||||
self.stats_row.controls = [
|
||||
ft.Chip(label=ft.Text(f"{n}: CPU {s.get('CPUPerc','?')} MEM {s.get('MemPerc','?')}"))
|
||||
for n, s in stats.items()
|
||||
] or [ft.Text("нет запущенных", size=12, color=ft.Colors.GREY_500)]
|
||||
self._page.update()
|
||||
|
||||
def _ct_action(self, cid, action):
|
||||
async def _do():
|
||||
async with SSHClient(self.srv) as c:
|
||||
await d.container_action(c, cid, action)
|
||||
await self._load_containers()
|
||||
self._page.run_task(_do)
|
||||
|
||||
def _show_logs(self, cid, name):
|
||||
self.log_container.visible = True
|
||||
append_log(self.log, f"=== логи {name} ===")
|
||||
self._page.update()
|
||||
async def _do():
|
||||
async with SSHClient(self.srv) as c:
|
||||
out = await d.container_logs(c, cid)
|
||||
for line in out.splitlines()[-100:]:
|
||||
append_log(self.log, line)
|
||||
self._page.update()
|
||||
self._page.run_task(_do)
|
||||
|
||||
def _remove_ct(self, cid, name):
|
||||
def do():
|
||||
async def _d():
|
||||
async with SSHClient(self.srv) as c:
|
||||
await d.container_action(c, cid, "rm")
|
||||
await self._load_containers()
|
||||
self._page.run_task(_d)
|
||||
confirm_dialog(self._page, f"Удалить контейнер {name}?", "Действие необратимо.", do)
|
||||
|
||||
# ---------- образы ----------
|
||||
|
||||
def _images_view(self):
|
||||
self.img_table = ft.DataTable(columns=[
|
||||
ft.DataColumn(ft.Text("Репозиторий")), ft.DataColumn(ft.Text("Тег")),
|
||||
ft.DataColumn(ft.Text("Размер")), ft.DataColumn(ft.Text("Создан")),
|
||||
ft.DataColumn(ft.Text("")),
|
||||
], rows=[])
|
||||
pull_tf = ft.TextField(hint_text="образ для pull (напр. nginx:alpine)", width=300, dense=True)
|
||||
return ft.Column([
|
||||
ft.Row([
|
||||
pull_tf,
|
||||
ft.FilledButton("Pull", icon=ft.Icons.DOWNLOAD,
|
||||
on_click=lambda e: self._pull(pull_tf.value)),
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load_images)),
|
||||
]),
|
||||
ft.Container(expand=True, content=ft.Column([self.img_table], scroll=ft.ScrollMode.AUTO)),
|
||||
], expand=True)
|
||||
|
||||
async def _load_images(self):
|
||||
async with SSHClient(self.srv) as c:
|
||||
images = await d.list_images(c)
|
||||
self.img_table.rows = [
|
||||
ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(i["Repository"])),
|
||||
ft.DataCell(ft.Text(i["Tag"])),
|
||||
ft.DataCell(ft.Text(i["Size"])),
|
||||
ft.DataCell(ft.Text(i.get("CreatedSince", ""), size=11)),
|
||||
ft.DataCell(ft.IconButton(
|
||||
ft.Icons.DELETE_OUTLINE, icon_size=18,
|
||||
on_click=lambda e, ref=f"{i['Repository']}:{i['Tag']}": self._rmi(ref))),
|
||||
]) for i in images
|
||||
]
|
||||
self._page.update()
|
||||
|
||||
def _pull(self, image):
|
||||
if not image:
|
||||
return
|
||||
self.log_container.visible = True
|
||||
async def job(log):
|
||||
async with SSHClient(self.srv) as c:
|
||||
stream = await d.pull(c, image)
|
||||
async for line in stream:
|
||||
log(line)
|
||||
tasks.start("docker", f"docker pull {image}", self.srv.id, job,
|
||||
on_log=lambda l: (append_log(self.log, l), self._page.update()),
|
||||
on_done=lambda r: self._page.run_task(self._load_images))
|
||||
|
||||
def _rmi(self, ref):
|
||||
def do():
|
||||
async def _d():
|
||||
async with SSHClient(self.srv) as c:
|
||||
await d.remove_image(c, ref)
|
||||
await self._load_images()
|
||||
self._page.run_task(_d)
|
||||
confirm_dialog(self._page, f"Удалить образ {ref}?", "", do)
|
||||
|
||||
# ---------- тома ----------
|
||||
|
||||
def _volumes_view(self):
|
||||
self.vol_table = ft.DataTable(columns=[
|
||||
ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Драйвер")),
|
||||
ft.DataColumn(ft.Text("")),
|
||||
], rows=[])
|
||||
return ft.Column([
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load_volumes)),
|
||||
ft.Container(expand=True, content=ft.Column([self.vol_table], scroll=ft.ScrollMode.AUTO)),
|
||||
], expand=True)
|
||||
|
||||
async def _load_volumes(self):
|
||||
async with SSHClient(self.srv) as c:
|
||||
vols = await d.list_volumes(c)
|
||||
self.vol_table.rows = [
|
||||
ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(v["Name"])),
|
||||
ft.DataCell(ft.Text(v.get("Driver", ""))),
|
||||
ft.DataCell(ft.IconButton(ft.Icons.DELETE_OUTLINE, icon_size=18,
|
||||
on_click=lambda e, n=v["Name"]: self._rm_vol(n))),
|
||||
]) for v in vols
|
||||
]
|
||||
self._page.update()
|
||||
|
||||
def _rm_vol(self, name):
|
||||
def do():
|
||||
async def _d():
|
||||
async with SSHClient(self.srv) as c:
|
||||
await d.remove_volume(c, name)
|
||||
await self._load_volumes()
|
||||
self._page.run_task(_d)
|
||||
confirm_dialog(self._page, f"Удалить том {name}?", "Данные будут потеряны.", do)
|
||||
|
||||
# ---------- сети ----------
|
||||
|
||||
def _networks_view(self):
|
||||
self.net_table = ft.DataTable(columns=[
|
||||
ft.DataColumn(ft.Text("Имя")), ft.DataColumn(ft.Text("Драйвер")),
|
||||
ft.DataColumn(ft.Text("Scope")),
|
||||
], rows=[])
|
||||
return ft.Column([
|
||||
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load_networks)),
|
||||
ft.Container(expand=True, content=ft.Column([self.net_table], scroll=ft.ScrollMode.AUTO)),
|
||||
], expand=True)
|
||||
|
||||
async def _load_networks(self):
|
||||
async with SSHClient(self.srv) as c:
|
||||
nets = await d.list_networks(c)
|
||||
self.net_table.rows = [
|
||||
ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(n["Name"])),
|
||||
ft.DataCell(ft.Text(n.get("Driver", ""))),
|
||||
ft.DataCell(ft.Text(n.get("Scope", ""))),
|
||||
]) for n in nets
|
||||
]
|
||||
self._page.update()
|
||||
|
||||
# ---------- система ----------
|
||||
|
||||
def _system_view(self):
|
||||
self.sys_text = ft.Text("…", selectable=True, size=12, font_family="Consolas")
|
||||
return ft.Column([
|
||||
ft.Row([
|
||||
ft.OutlinedButton("Обновить df", icon=ft.Icons.REFRESH,
|
||||
on_click=lambda e: self._page.run_task(self._load_system)),
|
||||
ft.FilledButton("System prune", icon=ft.Icons.CLEANING_SERVICES,
|
||||
style=ft.ButtonStyle(bgcolor=ft.Colors.ORANGE_800),
|
||||
on_click=self._prune),
|
||||
]),
|
||||
ft.Container(padding=8, content=self.sys_text),
|
||||
])
|
||||
|
||||
async def _load_system(self):
|
||||
async with SSHClient(self.srv) as c:
|
||||
ver = await d.server_version(c)
|
||||
df = await d.system_df(c)
|
||||
self.sys_text.value = f"Docker {ver}\n\n{df}"
|
||||
self._page.update()
|
||||
|
||||
def _prune(self, e):
|
||||
def do():
|
||||
self.log_container.visible = True
|
||||
async def job(log):
|
||||
async with SSHClient(self.srv) as c:
|
||||
log(await d.system_prune(c))
|
||||
tasks.start("docker", "docker system prune", self.srv.id, job,
|
||||
on_log=lambda l: (append_log(self.log, l), self._page.update()),
|
||||
on_done=lambda r: self._page.run_task(self._load_system))
|
||||
confirm_dialog(self._page, "Docker system prune?",
|
||||
"Будут удалены остановленные контейнеры, неиспользуемые сети, "
|
||||
"висячие образы и кэш сборки.", do)
|
||||
Reference in New Issue
Block a user