From be63181a85684828f5a30bf69a51f99de287784d Mon Sep 17 00:00:00 2001 From: vboxuser Date: Thu, 22 Jan 2026 20:37:45 +0300 Subject: [PATCH] Initial commit: saikyo-server-installer --- .../dh_installchangelogs.dch.trimmed | 8 + .../installed-by-dh_install | 2 + .../installed-by-dh_installdocs | 0 debian/changelog | 8 + debian/control | 18 + debian/debhelper-build-stamp | 1 + debian/files | 2 + debian/install | 2 + debian/rules | 3 + debian/saikyo-server-installer.substvars | 2 + debian/saikyo-server-installer/DEBIAN/control | 16 + debian/saikyo-server-installer/DEBIAN/md5sums | 3 + .../usr/bin/saikyo-installer | 3 + .../doc/saikyo-server-installer/changelog.gz | Bin 0 -> 291 bytes .../usr/share/saikyo-installer/installer.py | 603 ++++++++++++++++++ usr/bin/saikyo-installer | 3 + usr/share/saikyo-installer/installer.py | 603 ++++++++++++++++++ 17 files changed, 1277 insertions(+) create mode 100644 debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed create mode 100644 debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_install create mode 100644 debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_installdocs create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/debhelper-build-stamp create mode 100644 debian/files create mode 100644 debian/install create mode 100755 debian/rules create mode 100644 debian/saikyo-server-installer.substvars create mode 100644 debian/saikyo-server-installer/DEBIAN/control create mode 100644 debian/saikyo-server-installer/DEBIAN/md5sums create mode 100755 debian/saikyo-server-installer/usr/bin/saikyo-installer create mode 100644 debian/saikyo-server-installer/usr/share/doc/saikyo-server-installer/changelog.gz create mode 100644 debian/saikyo-server-installer/usr/share/saikyo-installer/installer.py create mode 100755 usr/bin/saikyo-installer create mode 100644 usr/share/saikyo-installer/installer.py diff --git a/debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..aafbe2d --- /dev/null +++ b/debian/.debhelper/generated/saikyo-server-installer/dh_installchangelogs.dch.trimmed @@ -0,0 +1,8 @@ +saikyo-server-installer (1.1.0) stable; urgency=medium + + * Обновлён ASCII-логотип + * Полная русификация + * Добавлена информация о приватности + * Убраны эмодзи для совместимости + + -- Saikyo OS Team Wed, 22 Jan 2026 00:50:00 +0300 diff --git a/debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_install b/debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_install new file mode 100644 index 0000000..095d524 --- /dev/null +++ b/debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_install @@ -0,0 +1,2 @@ +./usr/share/saikyo-installer/installer.py +./usr/bin/saikyo-installer diff --git a/debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_installdocs b/debian/.debhelper/generated/saikyo-server-installer/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..aafbe2d --- /dev/null +++ b/debian/changelog @@ -0,0 +1,8 @@ +saikyo-server-installer (1.1.0) stable; urgency=medium + + * Обновлён ASCII-логотип + * Полная русификация + * Добавлена информация о приватности + * Убраны эмодзи для совместимости + + -- Saikyo OS Team Wed, 22 Jan 2026 00:50:00 +0300 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..c3f41c2 --- /dev/null +++ b/debian/control @@ -0,0 +1,18 @@ +Source: saikyo-server-installer +Section: admin +Priority: optional +Maintainer: Saikyo OS Team +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) + - Создание пользователей + - Выбор компонентов + - Автоматическую установку diff --git a/debian/debhelper-build-stamp b/debian/debhelper-build-stamp new file mode 100644 index 0000000..d44f1a6 --- /dev/null +++ b/debian/debhelper-build-stamp @@ -0,0 +1 @@ +saikyo-server-installer diff --git a/debian/files b/debian/files new file mode 100644 index 0000000..ad4064d --- /dev/null +++ b/debian/files @@ -0,0 +1,2 @@ +saikyo-server-installer_1.1.0_all.deb admin optional +saikyo-server-installer_1.1.0_amd64.buildinfo admin optional diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..3ebc353 --- /dev/null +++ b/debian/install @@ -0,0 +1,2 @@ +usr/share/saikyo-installer/installer.py +usr/bin/saikyo-installer diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..cbe925d --- /dev/null +++ b/debian/rules @@ -0,0 +1,3 @@ +#!/usr/bin/make -f +%: + dh $@ diff --git a/debian/saikyo-server-installer.substvars b/debian/saikyo-server-installer.substvars new file mode 100644 index 0000000..978fc8b --- /dev/null +++ b/debian/saikyo-server-installer.substvars @@ -0,0 +1,2 @@ +misc:Depends= +misc:Pre-Depends= diff --git a/debian/saikyo-server-installer/DEBIAN/control b/debian/saikyo-server-installer/DEBIAN/control new file mode 100644 index 0000000..84f132f --- /dev/null +++ b/debian/saikyo-server-installer/DEBIAN/control @@ -0,0 +1,16 @@ +Package: saikyo-server-installer +Version: 1.1.0 +Architecture: all +Maintainer: Saikyo OS Team +Installed-Size: 36 +Depends: python3 +Section: admin +Priority: optional +Description: Saikyo OS Server - TUI установщик + Графический (TUI) установщик для Saikyo OS Server. + Поддерживает: + - Выбор диска и разметку + - Настройку сети (DHCP/Static) + - Создание пользователей + - Выбор компонентов + - Автоматическую установку diff --git a/debian/saikyo-server-installer/DEBIAN/md5sums b/debian/saikyo-server-installer/DEBIAN/md5sums new file mode 100644 index 0000000..94d48e6 --- /dev/null +++ b/debian/saikyo-server-installer/DEBIAN/md5sums @@ -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 diff --git a/debian/saikyo-server-installer/usr/bin/saikyo-installer b/debian/saikyo-server-installer/usr/bin/saikyo-installer new file mode 100755 index 0000000..850788c --- /dev/null +++ b/debian/saikyo-server-installer/usr/bin/saikyo-installer @@ -0,0 +1,3 @@ +#!/bin/bash +# Saikyo OS Server Installer Launcher +exec python3 /usr/share/saikyo-installer/installer.py "$@" diff --git a/debian/saikyo-server-installer/usr/share/doc/saikyo-server-installer/changelog.gz b/debian/saikyo-server-installer/usr/share/doc/saikyo-server-installer/changelog.gz new file mode 100644 index 0000000000000000000000000000000000000000..408ca62fca6c50573b7592b31a28388ff8849eab GIT binary patch literal 291 zcmV+;0o?u{iwFP!0000212vGlPQySDMSFk6EqU467(o#p0#SvA!bsGHtdNBt(Qb^C zE(Sz{P=rJc1wY^fA}ky`zcBkpjAN*F_RhI8SEK#lW|Y`ks#~e-Al5@a45h+a$LTm? z9sIux4PpZuw6|`*0ca}(8#W-2CYGp5Bq3#vg&Rtg4l-oqhG*6eGi ze9)AeIUAo~p4nH^7kyI!e#$06whFzVdFyJ3KfeXrhPOHj&ODq;Kf=CF2ZKZnk92#i pL^~=y1TJKM3$BY(KZYyZT?o= 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) diff --git a/usr/bin/saikyo-installer b/usr/bin/saikyo-installer new file mode 100755 index 0000000..850788c --- /dev/null +++ b/usr/bin/saikyo-installer @@ -0,0 +1,3 @@ +#!/bin/bash +# Saikyo OS Server Installer Launcher +exec python3 /usr/share/saikyo-installer/installer.py "$@" diff --git a/usr/share/saikyo-installer/installer.py b/usr/share/saikyo-installer/installer.py new file mode 100644 index 0000000..6a64704 --- /dev/null +++ b/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)