import os import subprocess import sys import signal from dialog import Dialog from postinstall.root import check_root from postinstall.option import ( network, update, add_user, install_packages, ssh, hostname, shell, database, docker, ) import json check_root() from postinstall.logger import logger # import debugpy # debugpy.listen(("0.0.0.0", 5678)) # debugpy.wait_for_client() SSH_CONFIG_PATH = "/etc/ssh/sshd_config" HOSTNAME_FILE = "/etc/hostname" AUTOSTART_FILE = "/usr/share/postinstall/.norestart" BACKUPS_FILE = "/usr/share/postinstall/backups.json" UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" logging = logger def get_hostname() -> str: try: with open(HOSTNAME_FILE, "r") as f: return f.read().strip() except IOError as e: logging.error(f"Ошибка при чтении hostname: {e}") return "localhost" def get_ssh_port() -> str: try: result = subprocess.run( ["grep", "^#*Port", SSH_CONFIG_PATH], capture_output=True, text=True, check=True, ) port = result.stdout.strip().split()[-1] return port if port.isdigit() else "22" except subprocess.CalledProcessError: logging.warning("Не удалось получить порт SSH, используется порт по умолчанию") return "22" class MenuScreen: def __init__(self): self.d = Dialog(dialog="dialog") self.ssh_port = get_ssh_port() self.hostname = get_hostname() self.ssh_changes = None self.new_hostname = None self.network = None self.autostart_enabled = self.check_auto_start() self.new_autostart_enabled = self.autostart_enabled self.options = [ { "name": "Сетевые настройки", "value": "", "tabs": "\t \t", "option": "network", }, { "name": "Обновление системы", "value": "", "tabs": "\t \t", "option": "update", }, { "name": "Добавить пользователя", "value": "", "tabs": "\t \t", "option": "add_user", }, { "name": "Установка программ", "value": "", "tabs": "\t \t", "option": "install_packages", }, { "name": "Настройки ssh", "value": "", "tabs": "\t \t", "option": "ssh", }, { "name": "Изменить доменное имя ПК", "value": self.hostname, "tabs": "\t \t", "option": "hostname", }, {"name": "Выбор оболочки", "value": "", "tabs": "\t \t", "option": "shell"}, { "name": "Автозапуск при следующем запуске", "value": "Включен" if self.new_autostart_enabled else "Отключен", "tabs": "\t", "option": "auto_start", }, { "name": "Настройка базы данных", "value": "", "tabs": "\t \t", "option": "database", }, { "name": "Настройка docker", "value": "", "tabs": "\t \t", "option": "docker", }, { "name": "Откат последних изменений", "value": "", "tabs": "\t \t", "option": "rollback", }, { "name": "Сохранить и выйти", "value": "", "tabs": "\t \t", "option": "save_and_exit", }, ] os.makedirs(os.path.dirname(AUTOSTART_FILE), exist_ok=True) def show_menu(self): choices = [] for i, option in enumerate(self.options, 1): value = option["value"] if option["option"] == "hostname": value = ( self.new_hostname if self.new_hostname is not None else self.hostname ) if self.new_hostname is not None: value = f"{value} (не применено)" elif option["option"] == "network": value = "Изменено (не применено)" if self.network else "" choice_text = f"{option['name']}{option['tabs']}{value}" choices.append((str(i), choice_text)) code, tag = self.d.menu( "Выберите опцию:", choices=choices, title="Главное меню", cancel_label="Выйти без сохранения", width=70, height=15, ) if code == self.d.OK and tag: return self.options[int(tag) - 1]["option"] return None def set_temp_network(self, settings): self.temp_network = settings def check_auto_start(self) -> bool: return not os.path.exists(AUTOSTART_FILE) def set_auto_start(self, enable: bool) -> None: try: if enable: if os.path.exists(AUTOSTART_FILE): os.remove(AUTOSTART_FILE) logging.info("Автозапуск включен") else: with open(AUTOSTART_FILE, "w") as f: f.write("# Это файл создан для отключения автозапуска\n") logging.info("Автозапуск отключен") except Exception as e: logging.error(f"Ошибка при изменении автозапуска: {e}") def update_ssh_port(self, new_port): self.new_ssh_port = new_port def update_hostname(self, new_hostname): self.new_hostname = new_hostname def update_autostart(self, new_state): self.new_autostart_enabled = new_state def choose(option, menu): if menu.new_autostart_enabled != menu.autostart_enabled: menu.set_auto_start(menu.new_autostart_enabled) if option == "network": net_config = network.NetworkConfig() if net_config.run(): menu.network = net_config elif option == "update": logging.info("Начало обновления системы.") update.run_update() logging.info("Завершено обновление системы.") elif option == "add_user": logging.info("Началось добавления пользователя.") add_user.run_add_user() logging.info("Завершено добавление пользователя.") elif option == "install_packages": logging.info("Начало установки пакетов.") install_packages.run_install_packages() logging.info("Завершена установка пакетов.") elif option == "ssh": ssh_config, changes_made = ssh.run_ssh_config() if changes_made: menu.ssh_changes = ssh_config elif option == "hostname": new_hostname = hostname.run_change_hostname() if new_hostname: menu.new_hostname = new_hostname menu.options[5]["value"] = f"{new_hostname} (не применено)" elif option == "shell": logging.info("Началось изменение shell.") shell.run_change_shell() logging.info("Завершено изменение shell.") elif option == "auto_start": menu.new_autostart_enabled = not menu.new_autostart_enabled menu.options[7]["value"] = ( "Включен" if menu.new_autostart_enabled else "Отключен" ) logging.info( f"Автозапуск {'включен' if menu.new_autostart_enabled else 'отключен'}" ) elif option == "database": logging.info("Запуск настройки базы данных.") # Открытие диалогового окна для выбора базы данных code, choice = menu.d.menu("Выберите базу данных для установки:", choices=[("PostgreSQL", "Установить PostgreSQL"), ("MariaDB", "Установить MariaDB")]) if code == menu.d.OK: if choice == "PostgreSQL": if not database.is_installed('postgresql-16'): database.install_postgresql(menu.d) else: menu.d.msgbox("PostgreSQL и Harlequin уже установлены.") elif choice == "MariaDB": if not database.is_installed('mariadb-server'): database.install_mariadb(menu.d) else: menu.d.msgbox("MariaDB уже установлена.") # Независимо от выбранной базы данных, предложить установку Webmin if not database.is_installed('webmin'): code = menu.d.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") if code == menu.d.OK: database.install_webmin(menu.d) else: menu.d.msgbox("Webmin уже установлен.") logging.info("Настройка базы данных завершена.") else: menu.d.msgbox("Выбор базы данных отменён") elif option == "docker": docker.run() elif option == "rollback": try: with open(BACKUPS_FILE, "r") as f: backups = json.load(f) f.close() for i in backups: with open(i["file"], "w") as f: f.write(i["content"]) f.close() subprocess.run(["systemctl", "restart", "networking"], check=True) subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) ssh_port = get_ssh_port() subprocess.run( [ "sudo", "sed", "-i", f"s/^ports=.*/ports={ssh_port}\/tcp/", UFW_OPENSSH_CONFIG_PATH, ], check=True, ) subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True) subprocess.run(["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True) subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True) menu.d.msgbox("Резервные копии успешно восстановлены") logging.info("Резервные копии успешно восстановлены") except Exception as e: menu.d.msgbox(f"Вы еще не применяли изменения.{e}") return False elif option == "save_and_exit": with open("/etc/network/interfaces", "r") as f: interfaces = f.read() f.close() with open("/etc/resolv.conf", "r") as f: dns_settings = f.read() f.close() with open("/etc/ssh/sshd_config", "r") as f: ssh_settings = f.read() f.close() try: with open("/etc/ssh/sshd_config.d/auth.conf", "r") as f: ssh_auth = f.read() except Exception: ssh_auth = "" backup = [ { "file": "/etc/network/interfaces", "content": interfaces, }, { "file": "/etc/resolv.conf", "content": dns_settings, }, { "file": "/etc/ssh/sshd_config", "content": ssh_settings, }, { "file": "/etc/ssh/sshd_config.d/auth.conf", "content": ssh_auth, }, ] with open(BACKUPS_FILE, "w") as f: f.write(json.dumps(backup)) f.close() if menu.ssh_changes: logging.info( f"Применены новые настройки SSH. {'Новый порт SSH: ' + str(menu.ssh_changes.new_port) if menu.ssh_changes.new_port is not None else ''} {'Новый метод авторизации: ' + menu.ssh_changes.new_auth_method if menu.ssh_changes.new_auth_method is not None else ''}" ) menu.ssh_changes.apply_changes() if menu.new_hostname: logging.info(f"Применено новое имя хоста: {menu.new_hostname}") if hostname.apply_hostname_change(menu.new_hostname): menu.hostname = menu.new_hostname if menu.network: logging.info( f"Применены новые настройки сети: {menu.network.temp_network_settings}" ) if not menu.network.save_and_apply_network_settings(): return True return False return True def signal_handler(sig, frame): logging.info("Приложение закрыто") subprocess.run("clear", shell=True) sys.exit(0) def main(): logging.info("Приложение запущено") signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) menu = MenuScreen() while True: option = menu.show_menu() if option is None: break if not choose(option, menu): break logging.info("Приложение закрыто") subprocess.run("clear", shell=True) if __name__ == "__main__": main()