604 lines
25 KiB
Python
604 lines
25 KiB
Python
#!/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)
|