Initial commit: saikyo-server-installer
This commit is contained in:
commit
be63181a85
8
debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed
vendored
Normal file
8
debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed
vendored
Normal 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
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
./usr/share/saikyo-installer/installer.py
|
||||||
|
./usr/bin/saikyo-installer
|
||||||
0
debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_installdocs
vendored
Normal file
0
debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_installdocs
vendored
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
- Создание пользователей
|
||||||
|
- Выбор компонентов
|
||||||
|
- Автоматическую установку
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
saikyo-server-installer
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
saikyo-server-installer_1.1.0_all.deb admin optional
|
||||||
|
saikyo-server-installer_1.1.0_amd64.buildinfo admin optional
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
usr/share/saikyo-installer/installer.py
|
||||||
|
usr/bin/saikyo-installer
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
misc:Depends=
|
||||||
|
misc:Pre-Depends=
|
||||||
|
|
@ -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)
|
||||||
|
- Создание пользователей
|
||||||
|
- Выбор компонентов
|
||||||
|
- Автоматическую установку
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Saikyo OS Server Installer Launcher
|
||||||
|
exec python3 /usr/share/saikyo-installer/installer.py "$@"
|
||||||
BIN
debian/saikyo-server-installer/usr/share/doc/saikyo-server-installer/changelog.gz
vendored
Normal file
BIN
debian/saikyo-server-installer/usr/share/doc/saikyo-server-installer/changelog.gz
vendored
Normal file
Binary file not shown.
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Saikyo OS Server Installer Launcher
|
||||||
|
exec python3 /usr/share/saikyo-installer/installer.py "$@"
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue