saikyo-server-installer/usr/share/saikyo-installer/installer.py

604 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Saikyo OS Server Installer
TUI установщик для серверной ОС
"""
import curses
import subprocess
import os
import sys
import json
import socket
import re
import time
from pathlib import Path
VERSION = "1.0.0"
# Цветовые пары
COLOR_HEADER = 1
COLOR_MENU = 2
COLOR_SELECTED = 3
COLOR_INPUT = 4
COLOR_SUCCESS = 5
COLOR_ERROR = 6
COLOR_PROGRESS = 7
class SaikyoInstaller:
def __init__(self, stdscr):
self.stdscr = stdscr
self.config = {
"hostname": "saikyo-server",
"domain": "",
"disk": "",
"timezone": "Europe/Moscow",
"locale": "ru_RU.UTF-8",
"keyboard": "ru",
"root_password": "",
"user_name": "admin",
"user_password": "",
"network_mode": "dhcp",
"ip_address": "",
"netmask": "255.255.255.0",
"gateway": "",
"dns": "8.8.8.8",
"install_cockpit": True,
"install_docker": True,
"install_monitoring": True,
"enable_ssh": True,
"enable_firewall": True,
}
self.disks = []
self.current_step = 0
self.steps = [
("welcome", "Добро пожаловать"),
("license", "Лицензионное соглашение"),
("disk", "Выбор диска"),
("network", "Настройка сети"),
("hostname", "Имя хоста"),
("users", "Пользователи"),
("packages", "Компоненты"),
("summary", "Подтверждение"),
("install", "Установка"),
("complete", "Завершение"),
]
self.init_colors()
self.detect_disks()
def init_colors(self):
curses.start_color()
curses.use_default_colors()
curses.init_pair(COLOR_HEADER, curses.COLOR_BLACK, curses.COLOR_CYAN)
curses.init_pair(COLOR_MENU, curses.COLOR_WHITE, -1)
curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_GREEN)
curses.init_pair(COLOR_INPUT, curses.COLOR_CYAN, -1)
curses.init_pair(COLOR_SUCCESS, curses.COLOR_GREEN, -1)
curses.init_pair(COLOR_ERROR, curses.COLOR_RED, -1)
curses.init_pair(COLOR_PROGRESS, curses.COLOR_YELLOW, -1)
def detect_disks(self):
"""Обнаружение дисков в системе"""
try:
result = subprocess.run(
["lsblk", "-d", "-n", "-o", "NAME,SIZE,TYPE,MODEL"],
capture_output=True, text=True
)
for line in result.stdout.strip().split("\n"):
parts = line.split()
if len(parts) >= 3 and parts[2] == "disk":
name = parts[0]
size = parts[1]
model = " ".join(parts[3:]) if len(parts) > 3 else "Unknown"
self.disks.append({
"name": f"/dev/{name}",
"size": size,
"model": model
})
except Exception:
pass
def draw_header(self):
"""Отрисовка заголовка"""
h, w = self.stdscr.getmaxyx()
title = f" SAIKYO OS SERVER INSTALLER v{VERSION} "
step_info = f"Шаг {self.current_step + 1}/{len(self.steps)}: {self.steps[self.current_step][1]}"
self.stdscr.attron(curses.color_pair(COLOR_HEADER))
self.stdscr.addstr(0, 0, " " * w)
self.stdscr.addstr(0, (w - len(title)) // 2, title)
self.stdscr.attroff(curses.color_pair(COLOR_HEADER))
self.stdscr.addstr(1, 2, step_info, curses.color_pair(COLOR_INPUT))
self.stdscr.addstr(2, 0, "" * w)
def draw_footer(self):
"""Отрисовка подвала"""
h, w = self.stdscr.getmaxyx()
footer = " [Enter] Далее [Esc] Назад [F10] Выход "
self.stdscr.addstr(h - 2, 0, "" * w)
self.stdscr.attron(curses.color_pair(COLOR_HEADER))
self.stdscr.addstr(h - 1, 0, " " * w)
self.stdscr.addstr(h - 1, (w - len(footer)) // 2, footer)
self.stdscr.attroff(curses.color_pair(COLOR_HEADER))
def draw_box(self, y, x, h, w, title=""):
"""Отрисовка рамки"""
self.stdscr.addstr(y, x, "" + "" * (w - 2) + "")
for i in range(1, h - 1):
self.stdscr.addstr(y + i, x, "" + " " * (w - 2) + "")
self.stdscr.addstr(y + h - 1, x, "" + "" * (w - 2) + "")
if title:
self.stdscr.addstr(y, x + 2, f" {title} ", curses.color_pair(COLOR_INPUT))
def center_text(self, y, text, attr=0):
"""Центрирование текста"""
h, w = self.stdscr.getmaxyx()
x = (w - len(text)) // 2
self.stdscr.addstr(y, x, text, attr)
def input_field(self, y, x, prompt, default="", password=False, width=40):
"""Поле ввода"""
curses.echo()
curses.curs_set(1)
self.stdscr.addstr(y, x, prompt + ": ", curses.color_pair(COLOR_MENU))
if password:
curses.noecho()
input_win = curses.newwin(1, width, y, x + len(prompt) + 2)
input_win.attron(curses.color_pair(COLOR_INPUT))
input_win.addstr(0, 0, "_" * width)
input_win.move(0, 0)
input_win.refresh()
value = ""
while True:
ch = input_win.getch()
if ch == 10: # Enter
break
elif ch == 27: # Escape
value = default
break
elif ch in (curses.KEY_BACKSPACE, 127, 8):
if value:
value = value[:-1]
input_win.clear()
if password:
input_win.addstr(0, 0, "*" * len(value))
else:
input_win.addstr(0, 0, value)
input_win.refresh()
elif 32 <= ch <= 126:
if len(value) < width - 1:
value += chr(ch)
if password:
input_win.addstr(0, len(value) - 1, "*")
else:
input_win.addstr(0, len(value) - 1, chr(ch))
input_win.refresh()
curses.noecho()
curses.curs_set(0)
return value if value else default
def menu_select(self, y, x, options, selected=0):
"""Меню выбора"""
current = selected
while True:
for i, opt in enumerate(options):
if i == current:
self.stdscr.addstr(y + i, x, f"{opt} ", curses.color_pair(COLOR_SELECTED))
else:
self.stdscr.addstr(y + i, x, f" {opt} ", curses.color_pair(COLOR_MENU))
self.stdscr.refresh()
key = self.stdscr.getch()
if key == curses.KEY_UP and current > 0:
current -= 1
elif key == curses.KEY_DOWN and current < len(options) - 1:
current += 1
elif key == 10: # Enter
return current
elif key == 27: # Escape
return -1
def checkbox_select(self, y, x, options, selected=None):
"""Чекбоксы"""
if selected is None:
selected = [True] * len(options)
current = 0
while True:
for i, (opt, _) in enumerate(options):
check = "" if selected[i] else ""
if i == current:
self.stdscr.addstr(y + i, x, f" {check} {opt} ", curses.color_pair(COLOR_SELECTED))
else:
self.stdscr.addstr(y + i, x, f" {check} {opt} ", curses.color_pair(COLOR_MENU))
self.stdscr.refresh()
key = self.stdscr.getch()
if key == curses.KEY_UP and current > 0:
current -= 1
elif key == curses.KEY_DOWN and current < len(options) - 1:
current += 1
elif key == ord(' '):
selected[current] = not selected[current]
elif key == 10: # Enter
return selected
elif key == 27: # Escape
return None
def step_welcome(self):
"""Экран приветствия"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
logo = [
"╔═══════════════════════════════════════════════════════════════════════╗",
"║ ║",
"║ ███████╗ █████╗ ██╗██╗ ██╗██╗ ██╗ ██████╗ ██████╗ ███████╗ ║",
"║ ██╔════╝██╔══██╗██║██║ ██╔╝╚██╗ ██╔╝██╔═══██╗ ██╔═══██╗██╔════╝ ║",
"║ ███████╗███████║██║█████╔╝ ╚████╔╝ ██║ ██║ ██║ ██║███████╗ ║",
"║ ╚════██║██╔══██║██║██╔═██╗ ╚██╔╝ ██║ ██║ ██║ ██║╚════██║ ║",
"║ ███████║██║ ██║██║██║ ██╗ ██║ ╚██████╔╝ ╚██████╔╝███████║ ║",
"║ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ║",
"║ ║",
"║ ▄▄▄ ▄▄▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ ║",
"║ █▄▄ █▄▄ █▄▀ █ █ █▄▄ █▄▀ ║",
"║ ▄▄█ █▄▄ █ █ ▀▄▀ █▄▄ █ █ ║",
"║ ║",
"╚═══════════════════════════════════════════════════════════════════════╝",
]
start_y = 3
for i, line in enumerate(logo):
self.center_text(start_y + i, line, curses.color_pair(COLOR_INPUT))
self.center_text(start_y + 16, "РОССИЙСКАЯ СЕРВЕРНАЯ ОПЕРАЦИОННАЯ СИСТЕМА", curses.color_pair(COLOR_SUCCESS))
self.center_text(start_y + 18, "Реестр Минцифры РФ | ПП №1236")
self.center_text(start_y + 19, "Телеметрия отключена | Ваши данные — ваши")
self.center_text(start_y + 22, "[ Нажмите Enter для продолжения ]", curses.color_pair(COLOR_INPUT))
self.stdscr.refresh()
while self.stdscr.getch() != 10:
pass
return True
def step_license(self):
"""Лицензионное соглашение"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Лицензионное соглашение")
license_text = [
"ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ",
"",
"Saikyo OS Server распространяется на условиях",
"лицензии GNU General Public License v3 (GPLv3).",
"",
"Правообладатель: ООО «САЙКО»",
"Адрес: 420099, Республика Татарстан,",
" Высокогорский р-н, с. Семиозерка,",
" ул. Зиганшина, д. 39",
"",
"Техническая поддержка: support@saikyo-os.ru",
"Сайт: https://saikyo-server.ru",
"",
"Продукт включён в Единый реестр российского ПО",
"Минцифры России (ПП РФ №1236).",
"",
"Используя данное ПО, вы соглашаетесь с условиями",
"лицензии GPLv3.",
]
for i, line in enumerate(license_text):
if i < h - 12:
self.stdscr.addstr(6 + i, 5, line)
self.stdscr.addstr(h - 5, 5, "Вы принимаете условия лицензии?", curses.color_pair(COLOR_INPUT))
options = ["Да, принимаю", "Нет, выход"]
choice = self.menu_select(h - 4, 5, options)
if choice == 1 or choice == -1:
return False
return True
def step_disk(self):
"""Выбор диска"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Выбор диска для установки")
if not self.disks:
self.stdscr.addstr(6, 5, "Диски не обнаружены!", curses.color_pair(COLOR_ERROR))
self.stdscr.addstr(8, 5, "Нажмите любую клавишу для выхода...")
self.stdscr.getch()
return False
self.stdscr.addstr(6, 5, "Выберите диск для установки:")
self.stdscr.addstr(7, 5, "⚠ ВНИМАНИЕ: Все данные на диске будут удалены!", curses.color_pair(COLOR_ERROR))
disk_options = [f"{d['name']} - {d['size']} ({d['model']})" for d in self.disks]
choice = self.menu_select(9, 5, disk_options)
if choice == -1:
return None # Back
self.config["disk"] = self.disks[choice]["name"]
return True
def step_network(self):
"""Настройка сети"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Настройка сети")
self.stdscr.addstr(6, 5, "Выберите режим настройки сети:")
options = ["DHCP (автоматически)", "Статический IP"]
choice = self.menu_select(8, 5, options)
if choice == -1:
return None
if choice == 0:
self.config["network_mode"] = "dhcp"
else:
self.config["network_mode"] = "static"
self.stdscr.clear()
self.draw_header()
self.draw_footer()
self.draw_box(4, 2, h - 8, w - 4, "Статический IP")
self.config["ip_address"] = self.input_field(6, 5, "IP-адрес", "192.168.1.100")
self.config["netmask"] = self.input_field(8, 5, "Маска", "255.255.255.0")
self.config["gateway"] = self.input_field(10, 5, "Шлюз", "192.168.1.1")
self.config["dns"] = self.input_field(12, 5, "DNS", "8.8.8.8")
return True
def step_hostname(self):
"""Имя хоста"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Имя хоста")
self.stdscr.addstr(6, 5, "Введите имя сервера:")
self.config["hostname"] = self.input_field(8, 5, "Hostname", self.config["hostname"])
self.config["domain"] = self.input_field(10, 5, "Домен (опционально)", self.config["domain"])
return True
def step_users(self):
"""Настройка пользователей"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Настройка пользователей")
self.stdscr.addstr(6, 5, "Пароль root:")
self.config["root_password"] = self.input_field(7, 5, "Пароль", "", password=True)
self.stdscr.addstr(10, 5, "Администратор системы:")
self.config["user_name"] = self.input_field(11, 5, "Логин", self.config["user_name"])
self.config["user_password"] = self.input_field(13, 5, "Пароль", "", password=True)
return True
def step_packages(self):
"""Выбор компонентов"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Компоненты для установки")
self.stdscr.addstr(6, 5, "Выберите компоненты (пробел для переключения):")
options = [
("Cockpit (веб-панель управления)", "install_cockpit"),
("Docker + Podman (контейнеры)", "install_docker"),
("Prometheus Node Exporter (мониторинг)", "install_monitoring"),
("SSH сервер", "enable_ssh"),
("Firewall (firewalld)", "enable_firewall"),
]
selected = [self.config[opt[1]] for opt in options]
result = self.checkbox_select(8, 5, options, selected)
if result is None:
return None
for i, (_, key) in enumerate(options):
self.config[key] = result[i]
return True
def step_summary(self):
"""Подтверждение"""
self.stdscr.clear()
self.draw_header()
self.draw_footer()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 8, w - 4, "Подтверждение установки")
y = 6
self.stdscr.addstr(y, 5, f"Диск: {self.config['disk']}", curses.color_pair(COLOR_INPUT)); y += 1
self.stdscr.addstr(y, 5, f"Hostname: {self.config['hostname']}"); y += 1
self.stdscr.addstr(y, 5, f"Сеть: {self.config['network_mode'].upper()}"); y += 1
if self.config["network_mode"] == "static":
self.stdscr.addstr(y, 5, f" IP: {self.config['ip_address']}"); y += 1
self.stdscr.addstr(y, 5, f"Пользователь: {self.config['user_name']}"); y += 2
self.stdscr.addstr(y, 5, "Компоненты:"); y += 1
if self.config["install_cockpit"]:
self.stdscr.addstr(y, 7, "✓ Cockpit", curses.color_pair(COLOR_SUCCESS)); y += 1
if self.config["install_docker"]:
self.stdscr.addstr(y, 7, "✓ Docker/Podman", curses.color_pair(COLOR_SUCCESS)); y += 1
if self.config["install_monitoring"]:
self.stdscr.addstr(y, 7, "✓ Мониторинг", curses.color_pair(COLOR_SUCCESS)); y += 1
y += 2
self.stdscr.addstr(y, 5, "ВСЕ ДАННЫЕ НА ДИСКЕ БУДУТ УДАЛЕНЫ!", curses.color_pair(COLOR_ERROR))
self.stdscr.addstr(h - 5, 5, "Начать установку?", curses.color_pair(COLOR_INPUT))
options = ["Да, начать установку", "Нет, вернуться"]
choice = self.menu_select(h - 4, 5, options)
if choice == 1 or choice == -1:
return None
return True
def step_install(self):
"""Процесс установки"""
self.stdscr.clear()
self.draw_header()
h, w = self.stdscr.getmaxyx()
self.draw_box(4, 2, h - 6, w - 4, "Установка Saikyo OS Server")
steps = [
"Разметка диска...",
"Форматирование разделов...",
"Установка базовой системы...",
"Настройка загрузчика...",
"Настройка сети...",
"Создание пользователей...",
"Установка пакетов Saikyo...",
"Настройка безопасности...",
"Финализация...",
]
for i, step in enumerate(steps):
y = 6 + i
self.stdscr.addstr(y, 5, f" {step}", curses.color_pair(COLOR_PROGRESS))
self.stdscr.refresh()
# Симуляция установки (в реальности здесь будут команды)
time.sleep(0.5)
self.stdscr.addstr(y, 5, f"{step}", curses.color_pair(COLOR_SUCCESS))
self.stdscr.refresh()
# Прогресс-бар
progress_y = 6 + len(steps) + 2
self.stdscr.addstr(progress_y, 5, "Прогресс: [", curses.color_pair(COLOR_MENU))
bar_width = w - 20
for i in range(bar_width):
self.stdscr.addstr(progress_y, 16 + i, "", curses.color_pair(COLOR_SUCCESS))
self.stdscr.refresh()
time.sleep(0.02)
self.stdscr.addstr(progress_y, 16 + bar_width, "] 100%")
self.stdscr.addstr(progress_y + 2, 5, "Установка завершена! Нажмите Enter...", curses.color_pair(COLOR_SUCCESS))
self.stdscr.refresh()
while self.stdscr.getch() != 10:
pass
return True
def step_complete(self):
"""Завершение"""
self.stdscr.clear()
self.draw_header()
h, w = self.stdscr.getmaxyx()
self.center_text(6, "╔════════════════════════════════════════╗", curses.color_pair(COLOR_SUCCESS))
self.center_text(7, "║ ║", curses.color_pair(COLOR_SUCCESS))
self.center_text(8, "УСТАНОВКА УСПЕШНО ЗАВЕРШЕНА! ║", curses.color_pair(COLOR_SUCCESS))
self.center_text(9, "║ ║", curses.color_pair(COLOR_SUCCESS))
self.center_text(10, "╚════════════════════════════════════════╝", curses.color_pair(COLOR_SUCCESS))
y = 13
self.center_text(y, "Saikyo OS Server установлен на ваш сервер."); y += 2
if self.config["install_cockpit"]:
self.center_text(y, f"Веб-панель Cockpit: https://{self.config['hostname']}:9090"); y += 1
self.center_text(y + 1, f"SSH: ssh {self.config['user_name']}@{self.config['hostname']}"); y += 3
self.center_text(y + 1, "Документация: https://saikyo-server.ru/docs")
self.center_text(y + 2, "Поддержка: support@saikyo-os.ru")
self.center_text(h - 4, "Извлеките установочный носитель и нажмите Enter для перезагрузки...", curses.color_pair(COLOR_INPUT))
self.stdscr.refresh()
while self.stdscr.getch() != 10:
pass
return True
def run(self):
"""Главный цикл"""
curses.curs_set(0)
step_methods = {
"welcome": self.step_welcome,
"license": self.step_license,
"disk": self.step_disk,
"network": self.step_network,
"hostname": self.step_hostname,
"users": self.step_users,
"packages": self.step_packages,
"summary": self.step_summary,
"install": self.step_install,
"complete": self.step_complete,
}
while self.current_step < len(self.steps):
step_name = self.steps[self.current_step][0]
result = step_methods[step_name]()
if result is True:
self.current_step += 1
elif result is None and self.current_step > 0:
self.current_step -= 1
elif result is False:
break
return self.current_step >= len(self.steps)
def main(stdscr):
installer = SaikyoInstaller(stdscr)
return installer.run()
if __name__ == "__main__":
try:
result = curses.wrapper(main)
if result:
print("\nУстановка завершена. Перезагрузка...")
# subprocess.run(["reboot"])
else:
print("\nУстановка отменена.")
except KeyboardInterrupt:
print("\nУстановка прервана.")
sys.exit(1)