117 lines
5.1 KiB
Python
117 lines
5.1 KiB
Python
|
|
"""Вкладка Сервисы: systemd-юниты, журнал."""
|
||
|
|
import flet as ft
|
||
|
|
|
||
|
|
from app.core import systemd_svc as sd
|
||
|
|
from app.core.ssh import SSHClient
|
||
|
|
from app.db.models import Server
|
||
|
|
from app.ui.components import confirm_dialog, snack
|
||
|
|
|
||
|
|
|
||
|
|
class SystemdTab(ft.Column):
|
||
|
|
def __init__(self, page: ft.Page, srv: Server):
|
||
|
|
super().__init__(expand=True, spacing=8)
|
||
|
|
self._page = page
|
||
|
|
self.srv = srv
|
||
|
|
self._units = []
|
||
|
|
|
||
|
|
self.search = ft.TextField(hint_text="Поиск юнита…", prefix_icon=ft.Icons.SEARCH,
|
||
|
|
on_change=lambda e: self._render(), expand=True, dense=True)
|
||
|
|
self.only_failed = ft.Checkbox(label="только failed", value=False,
|
||
|
|
on_change=lambda e: self._render())
|
||
|
|
self.table = ft.DataTable(columns=[
|
||
|
|
ft.DataColumn(ft.Text("Юнит")),
|
||
|
|
ft.DataColumn(ft.Text("Состояние")),
|
||
|
|
ft.DataColumn(ft.Text("Enabled")),
|
||
|
|
ft.DataColumn(ft.Text("Описание")),
|
||
|
|
ft.DataColumn(ft.Text("Действия")),
|
||
|
|
], rows=[])
|
||
|
|
self.log_text = ft.Column(scroll=ft.ScrollMode.AUTO, spacing=1)
|
||
|
|
|
||
|
|
self.controls = [
|
||
|
|
ft.Container(height=8),
|
||
|
|
ft.Row([
|
||
|
|
ft.OutlinedButton("Обновить", icon=ft.Icons.REFRESH,
|
||
|
|
on_click=lambda e: self._page.run_task(self._load)),
|
||
|
|
self.search, self.only_failed,
|
||
|
|
]),
|
||
|
|
ft.Row([
|
||
|
|
ft.Container(expand=3, content=ft.Column([self.table],
|
||
|
|
scroll=ft.ScrollMode.AUTO)),
|
||
|
|
ft.VerticalDivider(width=1),
|
||
|
|
ft.Container(expand=2, content=self.log_text),
|
||
|
|
], expand=True),
|
||
|
|
]
|
||
|
|
self._page.run_task(self._load)
|
||
|
|
|
||
|
|
async def _load(self):
|
||
|
|
try:
|
||
|
|
async with SSHClient(self.srv) as c:
|
||
|
|
self._units = await sd.list_units(c)
|
||
|
|
except Exception as e:
|
||
|
|
snack(self._page, f"systemd: {e}", error=True)
|
||
|
|
self._render()
|
||
|
|
|
||
|
|
def _render(self):
|
||
|
|
q = (self.search.value or "").lower()
|
||
|
|
units = self._units
|
||
|
|
if self.only_failed.value:
|
||
|
|
units = [u for u in units if u.active == "failed"]
|
||
|
|
if q:
|
||
|
|
units = [u for u in units if q in u.name.lower() or q in u.description.lower()]
|
||
|
|
|
||
|
|
def state_color(u):
|
||
|
|
return {"active": ft.Colors.GREEN_300, "failed": ft.Colors.RED_400,
|
||
|
|
"inactive": ft.Colors.GREY_500}.get(u.active, ft.Colors.GREY_500)
|
||
|
|
|
||
|
|
self.table.rows = [
|
||
|
|
ft.DataRow(cells=[
|
||
|
|
ft.DataCell(ft.Text(u.name, size=12, selectable=True)),
|
||
|
|
ft.DataCell(ft.Text(f"{u.active}/{u.sub}", size=12, color=state_color(u))),
|
||
|
|
ft.DataCell(ft.Text(u.enabled or "—", size=11)),
|
||
|
|
ft.DataCell(ft.Text(u.description[:50], size=11)),
|
||
|
|
ft.DataCell(ft.Row([
|
||
|
|
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=16, tooltip="start",
|
||
|
|
on_click=lambda e, n=u.name: self._act(n, "start")),
|
||
|
|
ft.IconButton(ft.Icons.STOP, icon_size=16, tooltip="stop",
|
||
|
|
on_click=lambda e, n=u.name: self._act(n, "stop")),
|
||
|
|
ft.IconButton(ft.Icons.RESTART_ALT, icon_size=16, tooltip="restart",
|
||
|
|
on_click=lambda e, n=u.name: self._act(n, "restart")),
|
||
|
|
ft.IconButton(ft.Icons.ARTICLE, icon_size=16, tooltip="логи",
|
||
|
|
on_click=lambda e, n=u.name: self._logs(n)),
|
||
|
|
], spacing=0)),
|
||
|
|
]) for u in units[:200]
|
||
|
|
]
|
||
|
|
self._page.update()
|
||
|
|
|
||
|
|
def _act(self, name, action):
|
||
|
|
def do():
|
||
|
|
async def _d():
|
||
|
|
try:
|
||
|
|
async with SSHClient(self.srv) as c:
|
||
|
|
await sd.action(c, name, action)
|
||
|
|
snack(self._page, f"{name}: {action} ✓")
|
||
|
|
await self._load()
|
||
|
|
except Exception as ex:
|
||
|
|
snack(self._page, f"{ex}", error=True)
|
||
|
|
self._page.run_task(_d)
|
||
|
|
if name in sd.CRITICAL_UNITS and action in ("stop", "restart"):
|
||
|
|
confirm_dialog(self._page, f"⚠ {action} {name}?",
|
||
|
|
"Это критичный системный сервис! Можно потерять связь с сервером.", do)
|
||
|
|
else:
|
||
|
|
do()
|
||
|
|
|
||
|
|
def _logs(self, name):
|
||
|
|
async def _l():
|
||
|
|
self.log_text.controls = [ft.Text(f"journalctl -u {name}", weight=ft.FontWeight.BOLD, size=12)]
|
||
|
|
self._page.update()
|
||
|
|
try:
|
||
|
|
async with SSHClient(self.srv) as c:
|
||
|
|
out = await sd.logs(c, name)
|
||
|
|
for line in out.splitlines()[-80:]:
|
||
|
|
self.log_text.controls.append(
|
||
|
|
ft.Text(line, size=10, font_family="Consolas", selectable=True))
|
||
|
|
except Exception as ex:
|
||
|
|
self.log_text.controls.append(ft.Text(f"ошибка: {ex}", size=11))
|
||
|
|
self._page.update()
|
||
|
|
self._page.run_task(_l)
|