Files
debian-manager/app/ui/pages/wizard.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

191 lines
9.2 KiB
Python

"""Мастер первичной настройки сервера: чекбоксы → плейбук."""
import flet as ft
from app.core.ssh import SSHClient
from app.core.tasks import tasks
from app.core import playbooks as pb
from app.db.models import Server
from app.ui.components import append_log, log_view, snack
# (id, название, описание, шаги)
WIZARD_STEPS = [
("hostname", "Изменить hostname", "Задать имя сервера",
lambda v: [{"name": "Hostname", "shell": f"hostnamectl set-hostname {v['hostname']}"}]),
("timezone", "Часовой пояс", "timedatectl set-timezone (по умолчанию Asia/Ulaanbaatar)",
lambda v: [{"name": "Timezone", "shell": f"timedatectl set-timezone {v['timezone']}"}]),
("ntp", "Синхронизация времени", "Включить NTP",
lambda v: [{"name": "NTP", "shell": "timedatectl set-ntp true"}]),
("update", "Обновить систему", "apt update + dist-upgrade",
lambda v: [{"name": "Update", "apt": {"update": True, "upgrade": "dist"}}]),
("user", "Создать пользователя", "Новый пользователь с sudo",
lambda v: [
{"name": "Создать пользователя",
"shell": f"useradd -m -s /bin/bash {v['new_user']} 2>/dev/null || true"},
{"name": "Пароль",
"shell": f"echo '{v['new_user']}:{v['new_pass']}' | chpasswd"},
{"name": "Sudo",
"shell": f"usermod -aG sudo {v['new_user']}"},
]),
("utils", "Базовые утилиты", "curl, wget, htop, git, mc, net-tools",
lambda v: [{"name": "Утилиты",
"apt": {"install": ["curl", "wget", "htop", "git", "mc", "net-tools"]}}]),
("docker", "Docker", "docker.io + docker-compose + автозапуск",
lambda v: [
{"name": "Установить Docker",
"apt": {"install": ["docker.io", "docker-compose"]}},
{"name": "Запустить Docker", "systemd": {"unit": "docker", "action": "enable --now"}},
]),
("portainer", "Portainer (порт 9000)", "Веб-интерфейс для Docker",
lambda v: [
{"name": "Portainer volume", "shell": "docker volume create portainer_data || true"},
{"name": "Portainer run",
"shell": "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"},
]),
("ufw", "Firewall (ufw)", "Установить, разрешить SSH, включить",
lambda v: [
{"name": "Установить ufw", "apt": {"install": ["ufw"]}},
{"name": "Пресет", "ufw": {"preset": "Только SSH"}},
{"name": "Включить ufw", "shell": "yes | ufw enable"},
]),
("fail2ban", "Fail2ban", "Защита SSH от перебора",
lambda v: [
{"name": "Установить fail2ban", "apt": {"install": ["fail2ban"]}},
{"name": "Запустить", "systemd": {"unit": "fail2ban", "action": "enable --now"}},
]),
("unattended", "Автообновления безопасности", "unattended-upgrades",
lambda v: [
{"name": "Установить", "apt": {"install": ["unattended-upgrades"]}},
{"name": "Включить",
"shell": "echo 'unattended-upgrades unattended-upgrades/enable_auto_updates boolean true' "
"| debconf-set-selections && dpkg-reconfigure -f noninteractive unattended-upgrades"},
]),
("agent", "Агент мониторинга", "dm-agent для real-time метрик (HTTP)",
None), # обрабатывается отдельно
]
RECOMMENDED = {"update", "utils", "ufw", "fail2ban", "ntp"}
class WizardPage(ft.Column):
def __init__(self, page: ft.Page, go):
super().__init__(expand=True, scroll=ft.ScrollMode.AUTO, spacing=12)
self._page = page
from app.core import servers as servers_core
self.srv_dd = ft.Dropdown(label="Сервер", width=300, options=[
ft.dropdown.Option(str(s.id), s.name) for s in servers_core.list_servers()
])
self.fields = {
"hostname": ft.TextField(label="hostname", width=250, visible=False, dense=True),
"timezone": ft.TextField(label="часовой пояс", value="Asia/Ulaanbaatar",
width=250, visible=False, dense=True),
"new_user": ft.TextField(label="имя пользователя", width=200, visible=False, dense=True),
"new_pass": ft.TextField(label="пароль", password=True, width=200,
visible=False, dense=True),
}
self.checks = {}
step_rows = []
for sid, name, desc, _ in WIZARD_STEPS:
cb = ft.Checkbox(label=name, value=sid in RECOMMENDED,
on_change=lambda e, sid=sid: self._toggle_fields())
self.checks[sid] = cb
extra = []
if sid == "hostname":
extra = [self.fields["hostname"]]
elif sid == "timezone":
extra = [self.fields["timezone"]]
elif sid == "user":
extra = [ft.Row([self.fields["new_user"], self.fields["new_pass"]])]
step_rows.append(ft.Column([
ft.Row([cb, ft.Text(desc, size=12, color=ft.Colors.GREY_500)]),
ft.Container(content=ft.Row(extra), padding=ft.Padding.only(left=40))
if extra else ft.Container(),
], spacing=2))
self.log = log_view()
self.log_container = ft.Container(
height=250, border=ft.Border.all(1, ft.Colors.GREY_800),
border_radius=8, padding=8, content=self.log, visible=False)
self.controls = [
ft.Text("Мастер первичной настройки", size=28, weight=ft.FontWeight.BOLD),
ft.Text("Отметьте что настроить, заполните поля, нажмите «Применить».",
size=13, color=ft.Colors.GREY_500),
self.srv_dd,
ft.Card(content=ft.Container(padding=14, content=ft.Column(step_rows, spacing=6))),
ft.Row([
ft.FilledButton("Применить выбранное", icon=ft.Icons.PLAY_ARROW,
on_click=self._apply),
ft.TextButton("Выбрать рекомендуемое", on_click=lambda e: self._preset()),
]),
self.log_container,
]
def _toggle_fields(self):
self.fields["hostname"].visible = self.checks["hostname"].value
self.fields["timezone"].visible = self.checks["timezone"].value
user_on = self.checks["user"].value
self.fields["new_user"].visible = user_on
self.fields["new_pass"].visible = user_on
self._page.update()
def _preset(self):
for sid, cb in self.checks.items():
cb.value = sid in RECOMMENDED
self._toggle_fields()
def _apply(self, e):
if not self.srv_dd.value:
snack(self._page, "Выберите сервер", error=True)
return
from app.core import servers as servers_core
srv = servers_core.get_server(int(self.srv_dd.value))
values = {
"hostname": self.fields["hostname"].value or "server",
"timezone": self.fields["timezone"].value or "Asia/Ulaanbaatar",
"new_user": self.fields["new_user"].value or "deploy",
"new_pass": self.fields["new_pass"].value or "changeme",
"ssh_port": str(srv.port),
}
steps = []
agent_selected = False
for sid, _, _, builder in WIZARD_STEPS:
if not self.checks[sid].value:
continue
if sid == "agent":
agent_selected = True
continue
steps.extend(builder(values))
if not steps and not agent_selected:
snack(self._page, "Ничего не выбрано", error=True)
return
self.log_container.visible = True
self._page.update()
playbook = {"name": "Мастер настройки", "vars": values, "steps": steps}
async def job(log):
async with SSHClient(srv) as c:
if steps:
await pb.run_playbook(c, srv, playbook, log)
if agent_selected:
from app.core import agent as agent_core
import socket
# определить IP приложения, доступный серверу
app_host = socket.gethostbyname(socket.gethostname())
log(f"установка агента (app_host={app_host})...")
await agent_core.install_agent(srv, app_host, log)
tasks.start("wizard", "мастер настройки", srv.id, job,
on_log=lambda l: (append_log(self.log, l), self._page.update()))
snack(self._page, "Мастер запущен — следите за логом")