132 lines
5.9 KiB
Python
132 lines
5.9 KiB
Python
|
|
"""Групповые операции по серверам."""
|
||
|
|
import flet as ft
|
||
|
|
|
||
|
|
from app.core import servers as servers_core
|
||
|
|
from app.core.group_ops import (GroupResult, op_apt_update, op_install_package,
|
||
|
|
op_reboot, op_shell, op_upgrade, run_group)
|
||
|
|
from app.ui.components import snack
|
||
|
|
|
||
|
|
|
||
|
|
class GroupOpsPage(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
|
||
|
|
|
||
|
|
servers = servers_core.list_servers()
|
||
|
|
self.checks = {s.id: ft.Checkbox(label=f"{s.name} ({s.host})", value=False,
|
||
|
|
data=s.id) for s in servers}
|
||
|
|
|
||
|
|
self.op_dd = ft.Dropdown(
|
||
|
|
label="Операция", value="update", width=300,
|
||
|
|
options=[
|
||
|
|
ft.dropdown.Option("update", "apt update"),
|
||
|
|
ft.dropdown.Option("upgrade", "upgrade"),
|
||
|
|
ft.dropdown.Option("dist", "dist-upgrade"),
|
||
|
|
ft.dropdown.Option("reboot", "reboot (с ожиданием)"),
|
||
|
|
ft.dropdown.Option("shell", "произвольная команда"),
|
||
|
|
ft.dropdown.Option("install", "установить пакет"),
|
||
|
|
],
|
||
|
|
on_select=lambda e: self._toggle_param(),
|
||
|
|
)
|
||
|
|
self.param_tf = ft.TextField(label="Команда / пакет", visible=False, width=400)
|
||
|
|
self.rolling_cb = ft.Checkbox(label="Rolling (по одному, ждать возврата)", value=False)
|
||
|
|
self.failfast_cb = ft.Checkbox(label="Остановиться при первой ошибке", value=False)
|
||
|
|
self.results_col = ft.Column(spacing=6)
|
||
|
|
|
||
|
|
self.controls = [
|
||
|
|
ft.Text("Групповые операции", size=28, weight=ft.FontWeight.BOLD),
|
||
|
|
ft.Row([
|
||
|
|
ft.Card(content=ft.Container(padding=14, width=380, content=ft.Column([
|
||
|
|
ft.Text("Серверы", weight=ft.FontWeight.BOLD),
|
||
|
|
*self.checks.values(),
|
||
|
|
ft.TextButton("выбрать все", on_click=lambda e: self._select_all(True)),
|
||
|
|
ft.TextButton("снять все", on_click=lambda e: self._select_all(False)),
|
||
|
|
]))),
|
||
|
|
ft.Card(content=ft.Container(padding=14, expand=True, content=ft.Column([
|
||
|
|
ft.Text("Операция", weight=ft.FontWeight.BOLD),
|
||
|
|
self.op_dd,
|
||
|
|
self.param_tf,
|
||
|
|
self.rolling_cb,
|
||
|
|
self.failfast_cb,
|
||
|
|
ft.FilledButton("Запустить", icon=ft.Icons.PLAY_ARROW,
|
||
|
|
on_click=self._run),
|
||
|
|
], spacing=10))),
|
||
|
|
], vertical_alignment=ft.CrossAxisAlignment.START),
|
||
|
|
ft.Text("Результаты", size=18, weight=ft.FontWeight.BOLD),
|
||
|
|
self.results_col,
|
||
|
|
]
|
||
|
|
|
||
|
|
def _select_all(self, v):
|
||
|
|
for cb in self.checks.values():
|
||
|
|
cb.value = v
|
||
|
|
self._page.update()
|
||
|
|
|
||
|
|
def _toggle_param(self):
|
||
|
|
self.param_tf.visible = self.op_dd.value in ("shell", "install")
|
||
|
|
self._page.update()
|
||
|
|
|
||
|
|
def _run(self, e):
|
||
|
|
selected = [s for s in servers_core.list_servers()
|
||
|
|
if self.checks.get(s.id) and self.checks[s.id].value]
|
||
|
|
if not selected:
|
||
|
|
snack(self._page, "Выберите хотя бы один сервер", error=True)
|
||
|
|
return
|
||
|
|
|
||
|
|
op_name = self.op_dd.value
|
||
|
|
if op_name == "update":
|
||
|
|
op = op_apt_update()
|
||
|
|
elif op_name == "upgrade":
|
||
|
|
op = op_upgrade(False)
|
||
|
|
elif op_name == "dist":
|
||
|
|
op = op_upgrade(True)
|
||
|
|
elif op_name == "reboot":
|
||
|
|
op = op_reboot(wait=True)
|
||
|
|
elif op_name == "shell":
|
||
|
|
if not self.param_tf.value:
|
||
|
|
snack(self._page, "Введите команду", error=True)
|
||
|
|
return
|
||
|
|
op = op_shell(self.param_tf.value)
|
||
|
|
else:
|
||
|
|
if not self.param_tf.value:
|
||
|
|
snack(self._page, "Введите имя пакета", error=True)
|
||
|
|
return
|
||
|
|
op = op_install_package(self.param_tf.value.split())
|
||
|
|
|
||
|
|
self.results_col.controls = [
|
||
|
|
ft.Text(f"запущено на {len(selected)} серверах…", color=ft.Colors.GREY_500)]
|
||
|
|
self._page.update()
|
||
|
|
|
||
|
|
async def job():
|
||
|
|
def on_status(res: GroupResult):
|
||
|
|
self._render_results(res)
|
||
|
|
res = await run_group(
|
||
|
|
selected, op,
|
||
|
|
rolling=self.rolling_cb.value,
|
||
|
|
fail_fast=self.failfast_cb.value,
|
||
|
|
on_status=on_status,
|
||
|
|
)
|
||
|
|
self._render_results(res)
|
||
|
|
snack(self._page, f"готово: ✅{res.ok_count} ❌{res.err_count}")
|
||
|
|
self._page.run_task(job)
|
||
|
|
|
||
|
|
def _render_results(self, res: GroupResult):
|
||
|
|
self.results_col.controls = []
|
||
|
|
for r in res.results.values():
|
||
|
|
icon = {"pending": ft.Icons.HOURGLASS_EMPTY, "running": ft.Icons.SYNC,
|
||
|
|
"success": ft.Icons.CHECK_CIRCLE, "error": ft.Icons.ERROR}.get(r.status)
|
||
|
|
color = {"success": ft.Colors.GREEN_400, "error": ft.Colors.RED_400,
|
||
|
|
"running": ft.Colors.BLUE_300}.get(r.status, ft.Colors.GREY_500)
|
||
|
|
log_preview = r.log.strip().splitlines()[-3:] if r.log.strip() else []
|
||
|
|
self.results_col.controls.append(ft.Card(content=ft.Container(
|
||
|
|
padding=10,
|
||
|
|
content=ft.Column([
|
||
|
|
ft.Row([ft.Icon(icon, color=color, size=18),
|
||
|
|
ft.Text(r.name, weight=ft.FontWeight.BOLD),
|
||
|
|
ft.Text(r.status, size=12, color=color)]),
|
||
|
|
*[ft.Text(l, size=10, font_family="Consolas",
|
||
|
|
color=ft.Colors.GREY_500) for l in log_preview],
|
||
|
|
], spacing=2),
|
||
|
|
)))
|
||
|
|
self._page.update()
|