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