"""Мастер первичной настройки сервера: чекбоксы → плейбук.""" 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, "Мастер запущен — следите за логом")