Initial commit: saikyo-server-installer

This commit is contained in:
vboxuser 2026-01-22 20:37:45 +03:00
commit be63181a85
17 changed files with 1277 additions and 0 deletions

View File

@ -0,0 +1,8 @@
saikyo-server-installer (1.1.0) stable; urgency=medium
* Обновлён ASCII-логотип
* Полная русификация
* Добавлена информация о приватности
* Убраны эмодзи для совместимости
-- Saikyo OS Team <support@saikyo-os.ru> Wed, 22 Jan 2026 00:50:00 +0300

View File

@ -0,0 +1,2 @@
./usr/share/saikyo-installer/installer.py
./usr/bin/saikyo-installer

8
debian/changelog vendored Normal file
View File

@ -0,0 +1,8 @@
saikyo-server-installer (1.1.0) stable; urgency=medium
* Обновлён ASCII-логотип
* Полная русификация
* Добавлена информация о приватности
* Убраны эмодзи для совместимости
-- Saikyo OS Team <support@saikyo-os.ru> Wed, 22 Jan 2026 00:50:00 +0300

18
debian/control vendored Normal file
View File

@ -0,0 +1,18 @@
Source: saikyo-server-installer
Section: admin
Priority: optional
Maintainer: Saikyo OS Team <support@saikyo-os.ru>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Package: saikyo-server-installer
Architecture: all
Depends: ${misc:Depends}, python3
Description: Saikyo OS Server - TUI установщик
Графический (TUI) установщик для Saikyo OS Server.
Поддерживает:
- Выбор диска и разметку
- Настройку сети (DHCP/Static)
- Создание пользователей
- Выбор компонентов
- Автоматическую установку

1
debian/debhelper-build-stamp vendored Normal file
View File

@ -0,0 +1 @@
saikyo-server-installer

2
debian/files vendored Normal file
View File

@ -0,0 +1,2 @@
saikyo-server-installer_1.1.0_all.deb admin optional
saikyo-server-installer_1.1.0_amd64.buildinfo admin optional

2
debian/install vendored Normal file
View File

@ -0,0 +1,2 @@
usr/share/saikyo-installer/installer.py
usr/bin/saikyo-installer

3
debian/rules vendored Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/make -f
%:
dh $@

View File

@ -0,0 +1,2 @@
misc:Depends=
misc:Pre-Depends=

View File

@ -0,0 +1,16 @@
Package: saikyo-server-installer
Version: 1.1.0
Architecture: all
Maintainer: Saikyo OS Team <support@saikyo-os.ru>
Installed-Size: 36
Depends: python3
Section: admin
Priority: optional
Description: Saikyo OS Server - TUI установщик
Графический (TUI) установщик для Saikyo OS Server.
Поддерживает:
- Выбор диска и разметку
- Настройку сети (DHCP/Static)
- Создание пользователей
- Выбор компонентов
- Автоматическую установку

View File

@ -0,0 +1,3 @@
19d55234039c0f8042dd16123237762c usr/bin/saikyo-installer
a26e9baa5cfee157f030c9a9ff970035 usr/share/doc/saikyo-server-installer/changelog.gz
e514463542f83335c2b9d0e7020c771b usr/share/saikyo-installer/installer.py

View File

@ -0,0 +1,3 @@
#!/bin/bash
# Saikyo OS Server Installer Launcher
exec python3 /usr/share/saikyo-installer/installer.py "$@"

View File

@ -0,0 +1,603 @@
#!/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)

3
usr/bin/saikyo-installer Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
# Saikyo OS Server Installer Launcher
exec python3 /usr/share/saikyo-installer/installer.py "$@"

View File

@ -0,0 +1,603 @@
#!/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)