"""Portainer: статус, открытие в браузере, установка/перезапуск.""" import flet as ft from app.core.ssh import SSHClient from app.core import docker_svc as d from app.db.models import Server from app.ui.components import snack PORTAINER_PORT = 9000 class PortainerPage(ft.Column): def __init__(self, page: ft.Page, go): super().__init__(expand=True, scroll=ft.ScrollMode.AUTO, spacing=12) self._page = page self.go = go self.cards_col = ft.Column(spacing=10) self.controls = [ ft.Text("Portainer", size=28, weight=ft.FontWeight.BOLD), ft.Row([ ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH, on_click=lambda e: self._reload()), ]), self.cards_col, ] self._reload() def _reload(self): from app.core import servers as servers_core self.cards_col.controls = [] for srv in servers_core.list_servers(): self.cards_col.controls.append(self._card_placeholder(srv)) self._page.update() from app.core import servers as servers_core for srv, card in zip(servers_core.list_servers(), self.cards_col.controls): self._page.run_task(self._fill, srv, card) def _card_placeholder(self, srv): return ft.Card(content=ft.Container( padding=14, width=500, content=ft.Column([ ft.Text(srv.name, weight=ft.FontWeight.BOLD, size=16), ft.ProgressBar(height=3), ]), )) async def _fill(self, srv, card): url = f"http://{srv.host}:{PORTAINER_PORT}" try: async with SSHClient(srv) as c: containers = await d.list_containers(c) portainer = next((x for x in containers if "portainer" in x["Names"].lower()), None) if not portainer: card.content.content.controls[1] = ft.Column([ ft.Text("Portainer не установлен", color=ft.Colors.GREY_500), ft.FilledButton("Установить", icon=ft.Icons.DOWNLOAD, on_click=lambda e, s=srv: self._install(s)), ]) else: running = "Up" in portainer["Status"] cid = portainer["ID"] card.content.content.controls[1] = ft.Column([ ft.Row([ ft.Icon(ft.Icons.CIRCLE, size=12, color=ft.Colors.GREEN_400 if running else ft.Colors.RED_400), ft.Text(portainer["Status"], size=12), ]), ft.Row([ ft.FilledButton("Открыть в браузере", icon=ft.Icons.OPEN_IN_BROWSER, on_click=lambda e, u=url: self._page.launch_url(u)), ft.OutlinedButton( "Перезапустить" if running else "Запустить", on_click=lambda e, s=srv, c=cid, a="restart" if running else "start": self._action(s, c, a)), ]), ft.Text(url, size=11, color=ft.Colors.BLUE_300, selectable=True), ]) except Exception as e: card.content.content.controls[1] = ft.Text(f"ошибка: {e}", size=12, color=ft.Colors.RED_300) self._page.update() def _install(self, srv): from app.core.tasks import tasks from app.ui.components import append_log, log_view async def job(log): async with SSHClient(srv) as c: log("$ docker volume create portainer_data") await c.sudo("docker volume create portainer_data 2>/dev/null || true") log("$ docker run portainer") r = await c.sudo( "docker rm -f portainer 2>/dev/null; " "docker run -d -p 9000:9000 --name portainer --restart=always " "-v /var/run/docker.sock:/var/run/docker.sock " "-v portainer_data:/data portainer/portainer-ce:lts", timeout=600) log(r.stdout + r.stderr) tasks.start("portainer", "установка Portainer", srv.id, job, on_done=lambda r: self._reload()) snack(self._page, f"Установка Portainer на {srv.name} запущена") def _action(self, srv, cid, action): async def _do(): async with SSHClient(srv) as c: await d.container_action(c, cid, action) self._reload() self._page.run_task(_do)