#!/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)