Files
debian-manager/app/ui/tabs/docker_tab.py
T
dinlo f624fee5e0 Initial commit: Debian Manager — управление Debian-серверами
- SSH/SFTP управление без агентов
- Пакеты (apt), Docker, файлы, systemd, ufw, cron
- Плейбуки, групповые операции, планировщик
- Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy
- Терминал, Telegram-уведомления, тёмная/светлая тема
2026-09-05 19:46:45 +08:00

297 lines
13 KiB
Python

"""Вкладка 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)