f624fee5e0
- SSH/SFTP управление без агентов - Пакеты (apt), Docker, файлы, systemd, ufw, cron - Плейбуки, групповые операции, планировщик - Бэкапы, отчёты, Portainer, Proxmox VE, Gitea Deploy - Терминал, Telegram-уведомления, тёмная/светлая тема
191 lines
9.2 KiB
Python
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, "Мастер запущен — следите за логом")
|