commit 4337a3f94eec171cd63cadea4af5f5d343fd2afe Author: s.galanskiy Date: Mon Jun 9 14:32:16 2025 +0500 Старый postinstall diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3e2e55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.vscode +.venv +code.md +prompt.txt \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7223d6f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +include postinstall/styles.tcss diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f89cbf --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. \ No newline at end of file diff --git a/build/lib/tests/__init__.py b/build/lib/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/build/lib/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/build/lib/textualconf/__init__.py b/build/lib/textualconf/__init__.py new file mode 100644 index 0000000..3918b33 --- /dev/null +++ b/build/lib/textualconf/__init__.py @@ -0,0 +1,507 @@ +import subprocess + +from textual import log +from textual import on +from textual.app import App, ComposeResult +from textual.containers import Container +from textual.screen import Screen +from textual.widgets import Header, Footer, Button, Static, Input, RadioButton, RadioSet +import setuptools +import importlib.resources + + +class CustomRadioButton(RadioButton): + BUTTON_INNER = "x" + + +class DHCPScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Введите настройки сети:", classes="chooseBlock__title") + yield Input(placeholder="Введите DNS", id="input_dns") + yield Button("Протестировать", id="button__testDHCP", classes="chooseBlock__button") + yield Button("Сохранить и выйти", id="button__saveDHCP", classes="chooseBlock__button") + yield Footer() + + +class StaticScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Введите настройки сети:", classes="chooseBlock__title") + yield Input(placeholder="Введите IP адрес", id="input_ip") + yield Input(placeholder="Введите маску", id="input_mask") + yield Input(placeholder="Введите шлюз", id="input_gateway") + yield Button("Протестировать", id="button__testStatic", classes="chooseBlock__button") + yield Button("Сохранить и выйти", id="button__saveStatic", classes="chooseBlock__button") + yield Footer() + + +class ChooseTypeScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Выберите тип подключения к сети", classes="chooseBlock__title") + yield Static(f"Интерфейс: {self.app.get_interface()}", classes="chooseBlock__title") + yield Button("DHCP", id="button__dhcp", classes="chooseBlock__button") + yield Button("Static", id="button__static", classes="chooseBlock__button") + yield Footer() + + +class ChooseInterfaceScreen(Screen): + def compose(self) -> ComposeResult: + self.interfaces = self.get_network_interfaces() + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Выберите интерфейс", classes="chooseBlock__title") + with RadioSet(id="radio__interface"): + if len(self.interfaces) != 0: + for interface in self.interfaces: + yield CustomRadioButton(interface) + yield Button("Выбрать", id="button__chooseInterface", classes="chooseBlock__button") + yield Button("На главную", id="button__toOptions", classes="chooseBlock__button") + yield Footer() + + @staticmethod + def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +class ChooseOptionScreen(Screen): + def update_options(self, new_options): + log(12) + self.options = new_options + self.refresh() + + def compose(self) -> ComposeResult: + log(123123) + self.options = self.app.get_options() + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Выберите действие", classes="chooseBlock__title") + with RadioSet(id="radio__option"): + for option in range(len(self.options)): + yield CustomRadioButton( + f"{option + 1}) {self.options[option]['name']}{self.options[option]['tabs']}{self.options[option]['value']}", + id=f"radio__option_{option}") + yield Button("Выбрать", id="button__chooseOption", classes="chooseBlock__button") + yield Footer() + + +class DomainScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Введите новый домен:", classes="chooseBlock__title") + yield Input(placeholder="Введите домен", id="input_domain") + yield Button("Сохранить и выйти", id="button__domain", classes="chooseBlock__button") + yield Footer() + + +class AddUserScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Введите данные для нового пользователя:", classes="chooseBlock__title") + yield Input(placeholder="Введите имя пользователя в системе", id="input_user_username") + yield Input(placeholder="Введите пароль пользователя", id="input_user_password") + yield Input(placeholder="Введите полное имя пользователя", id="input_user_fullname") + yield Input(placeholder="Введите мобильный телефон пользователя", id="input_user_mobilePhone") + yield Input(placeholder="Введите городской телефон пользователя", id="input_user_cityPhone") + yield Input(placeholder="Введите кабинет", id="input_user_cabinet") + yield Button("Создать", id="button__addUser", classes="chooseBlock__button") + yield Button("На главную", id="button__toOptions", classes="chooseBlock__button") + yield Footer() + + +class SudoUserScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Пользователь будет администратором?", classes="chooseBlock__title") + yield Button("Да", id="button__sudoYes", classes="chooseBlock__button") + yield Button("Нет", id="button__sudoNo", classes="chooseBlock__button") + yield Footer() + + +class UpdateScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Вы действительно хотите обновить систему?", classes="chooseBlock__title") + yield Button("Да", id="button__updateYes", classes="chooseBlock__button") + yield Button("Нет", id="button__toOptions", classes="chooseBlock__button") + yield Footer() + + +class ChangeSshPortScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + with Container(classes="chooseBlock centerBlock"): + yield Static("Изменение порта ssh.", classes="chooseBlock__title") + yield Input(placeholder="Введите новый порт", id="input_ssh") + yield Button("Изменить", id="button__ssh", classes="chooseBlock__button") + yield Button("На главную", id="button__toOptions", classes="chooseBlock__button") + yield Footer() + + +class NetworkApp(App): + CSS_PATH = "styles.tcss" + BINDINGS = [("Ctrl+C", "action_quit", "Выйти")] + SCREENS = { + "dhcp": DHCPScreen(), + "static": StaticScreen(), + "chooseType": ChooseTypeScreen(), + "chooseInterface": ChooseInterfaceScreen(), + "chooseOption": ChooseOptionScreen(), + "chooseDomain": DomainScreen(), + "addUser": AddUserScreen(), + "updateSystem": UpdateScreen(), + "sudoUser": SudoUserScreen(), + "changeSshPort": ChangeSshPortScreen(), + } + + def __init__(self): + super().__init__() + self.interface = None + self.selectedOption = None + self.userData = { + "username": "", + "password": "", + "fullname": "", + "cityPhone": "", + "mobilePhone": "", + "cabinet": "", + } + + self.sshPort = self.get_sshPort + + self.options = [ + { + "name": "Сетевые настройки", + "value": "", + "tabs": "\t \t", + "screen": "chooseInterface" + }, + { + "name": "Доменное имя ПК", + "value": self.get_hostname(), + "tabs": "\t \t \t", + "screen": "chooseDomain" + }, + { + "name": "Добавление пользователя", + "value": "", + "tabs": "\t \t", + "screen": "addUser" + }, + { + "name": "Обновление системы", + "value": "", + "tabs": "\t \t", + "screen": "updateSystem" + }, + { + "name": "Изменить порт ssh", + "value": self.sshPort(), + "tabs": "\t \t", + "screen": "changeSshPort" + }, + ] + + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "button__dhcp": + self.push_screen("dhcp") + elif event.button.id == "button__static": + self.push_screen("static") + elif event.button.id == "button__saveStatic": + self.saveStatic() + elif event.button.id == "button__testStatic": + self.testStatic() + elif event.button.id == "button__saveDHCP": + self.saveDHCP() + elif event.button.id == "button__testDHCP": + self.testDHCP() + elif event.button.id == "button__chooseInterface": + self.chooseInterface() + elif event.button.id == "button__chooseOption": + self.chooseOption() + elif event.button.id == "button__addUser": + self.addUser() + elif event.button.id == "button__domain": + self.changeDomain() + elif event.button.id == "button__updateYes": + self.updateSystem() + elif event.button.id == "button__toOptions": + self.push_screen("chooseOption") + elif event.button.id == "button__sudoYes": + self.userIsSudo(True) + elif event.button.id == "button__sudoNo": + self.userIsSudo(False) + elif event.button.id == "button__ssh": + self.changeSshPort() + + @on(RadioSet.Changed, "#radio__interface") + def on_radioset_changed_interface(self, event: RadioSet.Changed): + self.interface = event.pressed.label + log(self.interface) + + @on(RadioSet.Changed, "#radio__option") + def on_radioset_changed_option(self, event: RadioSet.Changed): + option_index = int(event.pressed.id.split("_")[-1]) + self.selectedOption = self.options[option_index] + + def saveStatic(self) -> None: + ip = self.query_one("#input_ip", Input).value + mask = self.query_one("#input_mask", Input).value + gateway = self.query_one("#input_gateway", Input).value + if ip and mask and gateway: + interface_config = f""" + auto {self.interface} +iface {self.interface} inet static + address {ip} + netmask {mask} + gateway {gateway} + """ + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + interface_found = False + for line in lines: + if line.strip() == f"auto {self.interface}": + interface_found = True + if interface_found and line.strip().startswith("iface"): + continue + if interface_found and line.strip().startswith( + ("address", "netmask", "gateway", "dns-nameservers")): + continue + if not interface_found or (interface_found and not line.strip()): + f.write(line) + f.write(interface_config.strip() + "\n") + + subprocess.run(["sudo", "systemctl", "restart", "networking"], check=True) + log(f"Static IP {ip} configured successfully") + except subprocess.CalledProcessError as e: + log(f"Error configuring static IP: {e}") + self.push_screen('chooseInterface') + + def saveDHCP(self) -> None: + dns = self.query_one("#input_dns", Input).value + interface_config = f""" + auto {self.interface} + iface {self.interface} inet dhcp + """ + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + interface_found = False + for line in lines: + if line.strip() == f"auto {self.interface}": + interface_found = True + if interface_found and line.strip().startswith("iface"): + continue + if interface_found and line.strip().startswith( + ("address", "netmask", "gateway", "dns-nameservers")): + continue + if not interface_found or (interface_found and not line.strip()): + f.write(line) + if interface_found and line.strip() == "": + interface_found = False + f.write(interface_config.strip() + "\n") + + if dns: + dns_entry = f"nameserver {dns}" + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if dns_entry + "\n" not in resolv_lines: + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + + subprocess.run(["sudo", "systemctl", "restart", "networking"], check=True) + log(f"DHCP configured successfully with DNS {dns}") + except subprocess.CalledProcessError as e: + log(f"Error configuring DHCP: {e}") + self.push_screen('chooseInterface') + + def testStatic(self) -> None: + ip = self.query_one("#input_ip", Input).value + mask = self.query_one("#input_mask", Input).value + gateway = self.query_one("#input_gateway", Input).value + if ip and mask and gateway: + try: + subprocess.run( + ["ping", "-c", "3", ip], + check=True + ) + log(f"Ping to {ip} successful") + except subprocess.CalledProcessError as e: + log(f"Ping to {ip} failed: {e}") + + def testDHCP(self) -> None: + dns = self.query_one("#input_dns", Input).value + if dns: + try: + subprocess.run( + ["ping", "-c", "3", dns], + check=True + ) + log(f"Ping to DNS {dns} successful") + except subprocess.CalledProcessError as e: + log(f"Ping to DNS {dns} failed: {e}") + + + def chooseInterface(self) -> None: + if self.interface is not None: + self.push_screen('chooseType') + + def chooseOption(self) -> None: + if self.selectedOption is not None: + self.push_screen(self.selectedOption['screen']) + + def changeDomain(self) -> None: + domain_input = self.query_one("#input_domain", Input).value + if domain_input: + try: + result = subprocess.run( + ["sudo", "hostnamectl", "set-hostname", domain_input], + capture_output=True, text=True + ) + if result.returncode == 0: + self.options[1]['value'] = domain_input + self.updateOptions() + else: + log(f"Error changing domain: {result.stderr}") + except Exception as e: + log(f"Exception changing domain: {e}") + + def addUser(self) -> None: + if (self.query_one("#input_user_username", Input).value != "") and ( + self.query_one("#input_user_password", Input).value != ""): + for field in self.userData.keys(): + self.userData[field] = self.query_one(f"#input_user_{field}", Input).value + self.query_one(f"#input_user_{field}", Input).value = "" + log(self.userData) + self.push_screen("sudoUser") + + def userIsSudo(self, isAdmin) -> None: + try: + subprocess.run( + ["sudo", "adduser", "--gecos", + f"{self.userData['fullname']},{self.userData['cabinet']},{self.userData['cityPhone']},{self.userData['mobilePhone']}", + "--disabled-login", + self.userData['username']], + check=True + ) + + subprocess.run( + ["sudo", "chpasswd"], + input=f"{self.userData['username']}:{self.userData['password']}", + text=True, + check=True + ) + + subprocess.run( + ["sudo", "mkdir", "-p", f"/home/{self.userData['username']}"], + check=True + ) + subprocess.run( + ["sudo", "chown", f"{self.userData['username']}:{self.userData['username']}", + f"/home/{self.userData['username']}"], + check=True + ) + subprocess.run( + ["sudo", "chmod", "755", f"/home/{self.userData['username']}"], + check=True + ) + + subprocess.run( + ["sudo", "usermod", "-s", "/bin/bash", self.userData['username']], + check=True + ) + + if isAdmin: + subprocess.run( + ["sudo", "usermod", "-aG", "sudo", self.userData['username']], + check=True + ) + + log("User added successfully") + except subprocess.CalledProcessError as e: + log(f"Error adding user: {e}") + self.push_screen("chooseOption") + + def updateSystem(self) -> None: + try: + subprocess.run(["sudo", "apt-get", "update"], check=True) + subprocess.run(["sudo", "apt-get", "upgrade", "-y"], check=True) + log("System updated successfully") + except subprocess.CalledProcessError as e: + log(f"Error updating system: {e}") + + def changeSshPort(self) -> None: + if self.query_one("#input_ssh", Input).value != "": + ssh = self.query_one("#input_ssh", Input).value + self.query_one("#input_ssh", Input).value = "" + self.options[4]["value"] = ssh + try: + # First, uncomment the Port line if it is commented and change the port + subprocess.run( + ["sudo", "sed", "-i", f"/^#Port /c\\Port {ssh}", "/etc/ssh/sshd_config"], + check=True + ) + # Then, ensure that an uncommented Port line is also changed + subprocess.run( + ["sudo", "sed", "-i", f"/^Port /c\\Port {ssh}", "/etc/ssh/sshd_config"], + check=True + ) + subprocess.run(["sudo", "systemctl", "restart", "ssh"], check=True) + log("SSH port changed successfully") + except subprocess.CalledProcessError as e: + log(f"Error changing SSH port: {e}") + self.updateOptions() + + def get_interface(self): + return self.interface + + def get_options(self): + return self.options + + def on_mount(self) -> None: + self.push_screen("chooseOption") + + def get_hostname(self) -> str: + return subprocess.run(["cat", "/etc/hostname"], capture_output=True, text=True).stdout.strip() + + def get_sshPort(self) -> str: + try: + result = subprocess.run( + ["grep", "^Port", "/etc/ssh/sshd_config"], + capture_output=True, text=True + ) + return result.stdout.strip().split()[-1] + except Exception as e: + log(f"Exception getting SSH port: {e}") + return "22" + + def updateOptions(self) -> None: + self.push_screen("chooseOption") + for option in range(len(self.options)): + self.query_one( + f"#radio__option_{option}").label = f"{option + 1}) {self.options[option]['name']}{self.options[option]['tabs']}{self.options[option]['value']}" + + +def main(): + app = NetworkApp() + app.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/build/lib/textualconf/styles.tcss b/build/lib/textualconf/styles.tcss new file mode 100755 index 0000000..c8078b2 --- /dev/null +++ b/build/lib/textualconf/styles.tcss @@ -0,0 +1,66 @@ +Screen { + align: center middle; + background: #000; + color: #fff; +} + +Header { + dock: top; + background: #000; + color: #fff; + border-bottom: solid #fff; +} + +Footer { + dock: bottom; + background: #000; + color: #fff; + border-top: solid #fff; +} + +.chooseBlock { + padding: 1; + min-width: 65; +} + +.centerBlock { + width: auto; + height: auto; + align: center middle; +} + +.chooseBlock__title { + margin: 1 2; + text-align: center; + width: 100%; +} + +Button { + margin: 1 2; + width: 100%; + + background: #000; + color: #fff; + border: solid #fff; +} + +Input { + margin: 1 2; + width: 100%; + + background: #000; + color: #fff; + border: solid #fff; +} + +CustomRadioButton { + color: #fff; + background: #000; + width: 100%; +} + +RadioSet{ + border: none; + background: #000; + width: 100%; +} \ No newline at end of file diff --git a/build_deb.sh b/build_deb.sh new file mode 100755 index 0000000..daf18a5 --- /dev/null +++ b/build_deb.sh @@ -0,0 +1,4 @@ +#/bin/bash +rm -rf deb_dist +rm -rf dist +python3 setup.py --command-packages=stdeb.command bdist_deb \ No newline at end of file diff --git a/deb/0002-postinstall.sh b/deb/0002-postinstall.sh new file mode 100644 index 0000000..95998cb --- /dev/null +++ b/deb/0002-postinstall.sh @@ -0,0 +1,3 @@ +if [ -n "$PS1" ]; then + [ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear ) +fi diff --git a/deb/postinst b/deb/postinst new file mode 100755 index 0000000..62aaaee --- /dev/null +++ b/deb/postinst @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +mkdir -p /var/lib/postinstall + +if [ ! -f /var/lib/postinstall/first_run_done ]; then + chmod +x /usr/share/postinstall/postinstall_start.sh + cp /usr/share/postinstall/postinstall.service /etc/systemd/system/ + systemctl enable postinstall.service + pip install --upgrade textual --break-system-packages +fi + diff --git a/deb/postinstall-autostart.desktop b/deb/postinstall-autostart.desktop new file mode 100644 index 0000000..cbf43df --- /dev/null +++ b/deb/postinstall-autostart.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; +X-GNOME-Autostart-enabled=true diff --git a/deb/postinstall.desktop b/deb/postinstall.desktop new file mode 100644 index 0000000..fb63863 --- /dev/null +++ b/deb/postinstall.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; diff --git a/deb/postinstall.service b/deb/postinstall.service new file mode 100644 index 0000000..f10a39d --- /dev/null +++ b/deb/postinstall.service @@ -0,0 +1,15 @@ +[Unit] +Description=Postinstall TUI Application +After=network.target + +[Service] +ExecStart=/usr/bin/postinstall +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty + +[Install] +WantedBy=multi-user.target diff --git a/deb/postinstall_start.sh b/deb/postinstall_start.sh new file mode 100755 index 0000000..05fdf4d --- /dev/null +++ b/deb/postinstall_start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/postinstall diff --git a/deb/python3-textual_0.1.13-1_all.deb b/deb/python3-textual_0.1.13-1_all.deb new file mode 100644 index 0000000..46bd888 Binary files /dev/null and b/deb/python3-textual_0.1.13-1_all.deb differ diff --git a/deb/textual-0.72.0-py3-none-any.whl b/deb/textual-0.72.0-py3-none-any.whl new file mode 100644 index 0000000..03e3a85 Binary files /dev/null and b/deb/textual-0.72.0-py3-none-any.whl differ diff --git a/deb_dist/postinstall-1.0.1/.pc/.quilt_patches b/deb_dist/postinstall-1.0.1/.pc/.quilt_patches new file mode 100644 index 0000000..6857a8d --- /dev/null +++ b/deb_dist/postinstall-1.0.1/.pc/.quilt_patches @@ -0,0 +1 @@ +debian/patches diff --git a/deb_dist/postinstall-1.0.1/.pc/.quilt_series b/deb_dist/postinstall-1.0.1/.pc/.quilt_series new file mode 100644 index 0000000..c206706 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/.pc/.quilt_series @@ -0,0 +1 @@ +series diff --git a/deb_dist/postinstall-1.0.1/.pc/.version b/deb_dist/postinstall-1.0.1/.pc/.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/.pc/.version @@ -0,0 +1 @@ +2 diff --git a/deb_dist/postinstall-1.0.1/.pc/applied-patches b/deb_dist/postinstall-1.0.1/.pc/applied-patches new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/MANIFEST.in b/deb_dist/postinstall-1.0.1/MANIFEST.in new file mode 100644 index 0000000..7223d6f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +include postinstall/styles.tcss diff --git a/deb_dist/postinstall-1.0.1/PKG-INFO b/deb_dist/postinstall-1.0.1/PKG-INFO new file mode 100644 index 0000000..5450a52 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.0.1 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.0.1/README.md b/deb_dist/postinstall-1.0.1/README.md new file mode 100644 index 0000000..5f89cbf --- /dev/null +++ b/deb_dist/postinstall-1.0.1/README.md @@ -0,0 +1,230 @@ +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/__init__.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/__main__.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/logger.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/__init__.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/add_user.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/autostart.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/database.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/database.py new file mode 100644 index 0000000..20432b9 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/database.py @@ -0,0 +1,134 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu("Выберите базу данных для установки:", + choices=[("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB")]) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") == dialog.OK: + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f=${Status}', package_name], + check=True, capture_output=True, text=True + ) + return 'install ok installed' in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ''): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ['apt-get', 'update'], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log(dialog, ['apt-get', 'install', '-y', package_name], f"Установка {package_name}"): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed('postgresql-16'): + install_package(dialog, 'postgresql-16') + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed('python3-harlequin-postgres'): + install_package(dialog, 'python3-harlequin-postgres') + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed('mariadb-server'): + install_package(dialog, 'mariadb-server') + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed('webmin'): + install_package(dialog, 'webmin') + + # Установка cpanminus + install_package(dialog, 'postgresql-server-dev-16') + install_package(dialog, 'cpanminus') + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(dialog, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(dialog, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/docker.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/docker.py new file mode 100644 index 0000000..5ef61fb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/docker.py @@ -0,0 +1,434 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + if ip != "127.0.0.1": + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + try: + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + config = f.read() + port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + if port_ip_match: + ip, port = port_ip_match.groups() + if ip != "127.0.0.1": + progress_callback(3, "Удаление правила UFW") + UFWManager.remove_rule(port) + except FileNotFoundError: + logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def show(self): + while True: + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init="127.0.0.1" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self._handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show(text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"): + CommandExecutor.run_command("docker swarm init") + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/hostname.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/install_packages.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/install_packages.py new file mode 100644 index 0000000..4cc780f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/install_packages.py @@ -0,0 +1,119 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + +def check_sudo(): + return os.geteuid() == 0 + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log(d, ['apt', 'install', '-y', 'cpanminus'], "Установка cpanminus") + + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(d, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + d.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(d, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/network.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/shell.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/ssh.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/update.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.0.1/build/lib/postinstall/root.py b/deb_dist/postinstall-1.0.1/build/lib/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.0.1/build/lib/tests/__init__.py b/deb_dist/postinstall-1.0.1/build/lib/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/deb_dist/postinstall-1.0.1/build/lib/tests/docker_config.py b/deb_dist/postinstall-1.0.1/build/lib/tests/docker_config.py new file mode 100644 index 0000000..62ddf0a --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/tests/docker_config.py @@ -0,0 +1,132 @@ +import logging +import subprocess +import os +import re + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class DockerConfig: + def check_installed(self, command: str) -> bool: + try: + return ( + subprocess.run( + ["which", command], capture_output=True, text=True + ).returncode + == 0 + ) + except subprocess.SubprocessError as e: + logging.error(f"Error checking if {command} is installed: {e}") + return False + + def install_docker(self): + try: + if not self.check_installed("docker"): + logging.info("Установка Docker...") + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True) + logging.info("Docker успешно установился") + + if not self.check_installed("docker-compose"): + logging.info("Установка Docker Compose...") + subprocess.run( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + logging.info("Docker Compose успешно установился") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Docker: {e}") + raise + + def check_docker_login(self, registry_url): + try: + result = subprocess.run( + ["docker", "login", registry_url, "--password-stdin"], + input=b"\n", + capture_output=True, + text=True, + check=True, + ) + return "login succeeded" in result.stdout.lower() + except subprocess.CalledProcessError as e: + logging.error(f"Error checking Docker login: {e}") + return False + + def login_to_registry(self): + try: + if not self.check_docker_login(REGISTRY_URL): + logging.info("Вход в Docker registry...") + subprocess.run( + ["docker", "login", REGISTRY_URL], + input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(), + check=True, + ) + logging.info("Успешный вход в Docker registry") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при входе в Docker registry: {e}") + raise + + def install_portainer(self, port, login, password, ip): + try: + if not self.check_docker_login(REGISTRY_URL): + self.login_to_registry() + + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + docker_compose_content = f""" +version: '3' + +services: + portainer: + image: dr.gosrv.ru/portainer + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer_data:/data + - ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password + ports: + - "{ip}:{port}:9000" + environment: + - ADMIN_USER={login} + - ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password + +volumes: + portainer_data: + """ + + with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + subprocess.run( + ["htpasswd", "-nb", login, password], + stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"), + check=True, + ) + + subprocess.run( + [ + "docker-compose", + "-f", + DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", + "up", + "-d", + ], + check=True, + ) + logging.info("Portainer успешно установлен") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Portainer: {e}") + raise + except IOError as e: + logging.error(f"Ошибка при создании файла: {e}") + raise + + def validate_input(self, value, pattern, error_message): + if not re.match(pattern, value): + return False + return True diff --git a/deb_dist/postinstall-1.0.1/build/lib/tests/test_docker.py b/deb_dist/postinstall-1.0.1/build/lib/tests/test_docker.py new file mode 100644 index 0000000..b5caf04 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/build/lib/tests/test_docker.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH + +class TestDockerConfig(unittest.TestCase): + def setUp(self): + self.docker_config = DockerConfig() + + @patch("subprocess.run") + def test_check_installed_true(self, mock_run): + mock_run.return_value.returncode = 0 + self.assertTrue(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_check_installed_false(self, mock_run): + mock_run.return_value.returncode = 1 + self.assertFalse(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_install_docker(self, mock_run): + self.docker_config.install_docker() + mock_run.assert_any_call(["apt-get", "update"], check=True) + mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True) + mock_run.assert_any_call( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + + @patch("subprocess.run") + def test_check_docker_login_success(self, mock_run): + mock_run.return_value.stdout = "Login Succeeded" + self.assertTrue(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + def test_check_docker_login_failure(self, mock_run): + mock_run.return_value.stdout = "Login Failed" + self.assertFalse(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + def test_login_to_registry(self, mock_check_login, mock_run): + mock_check_login.return_value = False + self.docker_config.login_to_registry() + mock_run.assert_called_once() + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + @patch("docker_config.DockerConfig.login_to_registry") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run): + mock_check_login.return_value = True + self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1") + mock_open.assert_called() + mock_run.assert_called() + + def test_validate_input_success(self): + self.assertTrue( + self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + def test_validate_input_failure(self): + self.assertFalse( + self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/deb_dist/postinstall-1.0.1/deb/0002-postinstall.sh b/deb_dist/postinstall-1.0.1/deb/0002-postinstall.sh new file mode 100644 index 0000000..95998cb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/0002-postinstall.sh @@ -0,0 +1,3 @@ +if [ -n "$PS1" ]; then + [ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear ) +fi diff --git a/deb_dist/postinstall-1.0.1/deb/postinst b/deb_dist/postinstall-1.0.1/deb/postinst new file mode 100755 index 0000000..62aaaee --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/postinst @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +mkdir -p /var/lib/postinstall + +if [ ! -f /var/lib/postinstall/first_run_done ]; then + chmod +x /usr/share/postinstall/postinstall_start.sh + cp /usr/share/postinstall/postinstall.service /etc/systemd/system/ + systemctl enable postinstall.service + pip install --upgrade textual --break-system-packages +fi + diff --git a/deb_dist/postinstall-1.0.1/deb/postinstall-autostart.desktop b/deb_dist/postinstall-1.0.1/deb/postinstall-autostart.desktop new file mode 100644 index 0000000..cbf43df --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/postinstall-autostart.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; +X-GNOME-Autostart-enabled=true diff --git a/deb_dist/postinstall-1.0.1/deb/postinstall.desktop b/deb_dist/postinstall-1.0.1/deb/postinstall.desktop new file mode 100644 index 0000000..fb63863 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/postinstall.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; diff --git a/deb_dist/postinstall-1.0.1/deb/postinstall.service b/deb_dist/postinstall-1.0.1/deb/postinstall.service new file mode 100644 index 0000000..f10a39d --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/postinstall.service @@ -0,0 +1,15 @@ +[Unit] +Description=Postinstall TUI Application +After=network.target + +[Service] +ExecStart=/usr/bin/postinstall +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty + +[Install] +WantedBy=multi-user.target diff --git a/deb_dist/postinstall-1.0.1/deb/postinstall_start.sh b/deb_dist/postinstall-1.0.1/deb/postinstall_start.sh new file mode 100755 index 0000000..05fdf4d --- /dev/null +++ b/deb_dist/postinstall-1.0.1/deb/postinstall_start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/postinstall diff --git a/deb_dist/postinstall-1.0.1/deb/python3-textual_0.1.13-1_all.deb b/deb_dist/postinstall-1.0.1/deb/python3-textual_0.1.13-1_all.deb new file mode 100644 index 0000000..46bd888 Binary files /dev/null and b/deb_dist/postinstall-1.0.1/deb/python3-textual_0.1.13-1_all.deb differ diff --git a/deb_dist/postinstall-1.0.1/deb/textual-0.72.0-py3-none-any.whl b/deb_dist/postinstall-1.0.1/deb/textual-0.72.0-py3-none-any.whl new file mode 100644 index 0000000..03e3a85 Binary files /dev/null and b/deb_dist/postinstall-1.0.1/deb/textual-0.72.0-py3-none-any.whl differ diff --git a/deb_dist/postinstall-1.0.1/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed b/deb_dist/postinstall-1.0.1/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..5e3ad11 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +postinstall (1.0.1-1) stable; urgency=low + + * source package automatically created by stdeb 0.10.0 + + -- LLC Technologies and Communication Fri, 20 Sep 2024 01:42:42 +0500 diff --git a/deb_dist/postinstall-1.0.1/debian/.debhelper/generated/python3-postinstall/installed-by-dh_installdocs b/deb_dist/postinstall-1.0.1/debian/.debhelper/generated/python3-postinstall/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/debian/changelog b/deb_dist/postinstall-1.0.1/debian/changelog new file mode 100644 index 0000000..5e3ad11 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/changelog @@ -0,0 +1,5 @@ +postinstall (1.0.1-1) stable; urgency=low + + * source package automatically created by stdeb 0.10.0 + + -- LLC Technologies and Communication Fri, 20 Sep 2024 01:42:42 +0500 diff --git a/deb_dist/postinstall-1.0.1/debian/compat b/deb_dist/postinstall-1.0.1/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/compat @@ -0,0 +1 @@ +9 diff --git a/deb_dist/postinstall-1.0.1/debian/control b/deb_dist/postinstall-1.0.1/debian/control new file mode 100644 index 0000000..33340ed --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/control @@ -0,0 +1,33 @@ +Source: postinstall +Maintainer: LLC Technologies and Communication +Section: python +Priority: optional +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9) +Standards-Version: 3.9.1 +Homepage: None + +Package: python3-postinstall +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends} +Description: A Dialog TUI application for postinstall system configuratio + ## Документация Postinstall + . + Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. + ### **1. Сетевые настройки** + . + Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + . + #### **1.1 Выбор сетевого интерфейса** + . + Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + . + ![Номер лицензии](screenshots/network_interface.png) + . + . + #### **1.2 Настройка DHCP** + . + Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + . + #### **1.3 Настройка статического IP** + . + diff --git a/deb_dist/postinstall-1.0.1/debian/files b/deb_dist/postinstall-1.0.1/debian/files new file mode 100644 index 0000000..6118275 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/files @@ -0,0 +1,2 @@ +postinstall_1.0.1-1_amd64.buildinfo python optional +python3-postinstall_1.0.1-1_all.deb python optional diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall.debhelper.log b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.debhelper.log new file mode 100644 index 0000000..de33f70 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.debhelper.log @@ -0,0 +1,19 @@ +dh_update_autotools_config +dh_auto_configure +dh_auto_build +dh_auto_test +dh_prep +dh_auto_install +dh_installdocs +dh_installchangelogs +dh_installinit +dh_perl +dh_link +dh_strip_nondeterminism +dh_compress +dh_fixperms +dh_missing +dh_installdeb +dh_gencontrol +dh_md5sums +dh_builddeb diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall.postinst.debhelper b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.postinst.debhelper new file mode 100644 index 0000000..c31c075 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p python3-postinstall +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-postinstall || true +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall.prerm.debhelper b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.prerm.debhelper new file mode 100644 index 0000000..e6a54f7 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p python3-postinstall +else + dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall.substvars b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.substvars new file mode 100644 index 0000000..b8e1dee --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-dialog, python3:any +misc:Depends= +misc:Pre-Depends= diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/conffiles b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/conffiles new file mode 100644 index 0000000..a294aea --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/conffiles @@ -0,0 +1,2 @@ +/etc/profile.d/0002-postinstall.sh +/etc/systemd/system/postinstall.service diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/control b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/control new file mode 100644 index 0000000..a483355 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/control @@ -0,0 +1,30 @@ +Package: python3-postinstall +Source: postinstall +Version: 1.0.1-1 +Architecture: all +Maintainer: LLC Technologies and Communication +Installed-Size: 128 +Depends: python3-dialog, python3:any +Section: python +Priority: optional +Homepage: None +Description: A Dialog TUI application for postinstall system configuratio + ## Документация Postinstall + . + Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. + ### **1. Сетевые настройки** + . + Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + . + #### **1.1 Выбор сетевого интерфейса** + . + Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + . + ![Номер лицензии](screenshots/network_interface.png) + . + . + #### **1.2 Настройка DHCP** + . + Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + . + #### **1.3 Настройка статического IP** diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/md5sums b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/md5sums new file mode 100644 index 0000000..33a39da --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/md5sums @@ -0,0 +1,22 @@ +3cea98b975936475c5700222859e86a6 usr/bin/postinstall +f37e850ca842f403abbe2bb92458f17b usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/dependency_links.txt +bcaee1a445a35da75a848bd8cee176a7 usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/entry_points.txt +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/requires.txt +147b741cc4a5c60607a1dc9f4fa92ca1 usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/top_level.txt +2a4a742cf1d86768584347780bcd16f6 usr/lib/python3/dist-packages/postinstall/__init__.py +666d344c47c8050391790adb6a9d69bb usr/lib/python3/dist-packages/postinstall/__main__.py +9e3443cf16cce74b2dcf317b122aee4f usr/lib/python3/dist-packages/postinstall/logger.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall/option/__init__.py +ed35c883b020ece0a9933a55383a08a5 usr/lib/python3/dist-packages/postinstall/option/add_user.py +13e13a3b080d38a57fc984bcc09d6291 usr/lib/python3/dist-packages/postinstall/option/autostart.py +de299807f193aa9932cf81c29bdd9b43 usr/lib/python3/dist-packages/postinstall/option/database.py +8c62c95cafe1218aa2eae6fdec6d61c4 usr/lib/python3/dist-packages/postinstall/option/docker.py +55583cfd043bf938df8ca95b368b68bc usr/lib/python3/dist-packages/postinstall/option/hostname.py +71155baa8161e9fc2128deed79317ad9 usr/lib/python3/dist-packages/postinstall/option/install_packages.py +4ad632661661fd0449f5cc1127471ab1 usr/lib/python3/dist-packages/postinstall/option/network.py +39961879cf3f2b241f946c61830ea018 usr/lib/python3/dist-packages/postinstall/option/shell.py +f47b245cb5673e70988e801ee3776d0a usr/lib/python3/dist-packages/postinstall/option/ssh.py +abb826ae4319de07a7b312a38652e0d2 usr/lib/python3/dist-packages/postinstall/option/update.py +633499d822e582c720519dc6b4f80f80 usr/lib/python3/dist-packages/postinstall/root.py +b8bb08b835c7fcfb9bd9c1f3f278d5a2 usr/share/doc/python3-postinstall/changelog.Debian.gz diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/postinst b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/postinst new file mode 100755 index 0000000..00f76af --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p python3-postinstall +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-postinstall || true +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/prerm b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/prerm new file mode 100755 index 0000000..edf4a44 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p python3-postinstall +else + dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh new file mode 100644 index 0000000..95998cb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh @@ -0,0 +1,3 @@ +if [ -n "$PS1" ]; then + [ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear ) +fi diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/systemd/system/postinstall.service b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/systemd/system/postinstall.service new file mode 100644 index 0000000..f10a39d --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/etc/systemd/system/postinstall.service @@ -0,0 +1,15 @@ +[Unit] +Description=Postinstall TUI Application +After=network.target + +[Service] +ExecStart=/usr/bin/postinstall +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty + +[Install] +WantedBy=multi-user.target diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/bin/postinstall b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/bin/postinstall new file mode 100755 index 0000000..887f6ae --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/bin/postinstall @@ -0,0 +1,33 @@ +#! /usr/bin/python3 +# EASY-INSTALL-ENTRY-SCRIPT: 'postinstall==1.0.1','console_scripts','postinstall' +import re +import sys + +# for compatibility with easy_install; see #2198 +__requires__ = 'postinstall==1.0.1' + +try: + from importlib.metadata import distribution +except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + +def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + +globals().setdefault('load_entry_point', importlib_load_entry_point) + + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(load_entry_point('postinstall==1.0.1', 'console_scripts', 'postinstall')()) diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/PKG-INFO b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/PKG-INFO new file mode 100644 index 0000000..5450a52 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.0.1 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/dependency_links.txt b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/entry_points.txt b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/requires.txt b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/requires.txt new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/top_level.txt b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/top_level.txt new file mode 100644 index 0000000..6503a29 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.0.1-py3.11.egg-info/top_level.txt @@ -0,0 +1,2 @@ +postinstall +tests diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/__init__.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py new file mode 100644 index 0000000..20432b9 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py @@ -0,0 +1,134 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu("Выберите базу данных для установки:", + choices=[("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB")]) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") == dialog.OK: + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f=${Status}', package_name], + check=True, capture_output=True, text=True + ) + return 'install ok installed' in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ''): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ['apt-get', 'update'], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log(dialog, ['apt-get', 'install', '-y', package_name], f"Установка {package_name}"): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed('postgresql-16'): + install_package(dialog, 'postgresql-16') + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed('python3-harlequin-postgres'): + install_package(dialog, 'python3-harlequin-postgres') + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed('mariadb-server'): + install_package(dialog, 'mariadb-server') + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed('webmin'): + install_package(dialog, 'webmin') + + # Установка cpanminus + install_package(dialog, 'postgresql-server-dev-16') + install_package(dialog, 'cpanminus') + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(dialog, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(dialog, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py new file mode 100644 index 0000000..5ef61fb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py @@ -0,0 +1,434 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + if ip != "127.0.0.1": + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + try: + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + config = f.read() + port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + if port_ip_match: + ip, port = port_ip_match.groups() + if ip != "127.0.0.1": + progress_callback(3, "Удаление правила UFW") + UFWManager.remove_rule(port) + except FileNotFoundError: + logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def show(self): + while True: + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init="127.0.0.1" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self._handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show(text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"): + CommandExecutor.run_command("docker swarm init") + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py new file mode 100644 index 0000000..4cc780f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py @@ -0,0 +1,119 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + +def check_sudo(): + return os.geteuid() == 0 + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log(d, ['apt', 'install', '-y', 'cpanminus'], "Установка cpanminus") + + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(d, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + d.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(d, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz new file mode 100644 index 0000000..2ba8cd8 Binary files /dev/null and b/deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz differ diff --git a/deb_dist/postinstall-1.0.1/debian/rules b/deb_dist/postinstall-1.0.1/debian/rules new file mode 100755 index 0000000..cc50749 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/rules @@ -0,0 +1,21 @@ +#!/usr/bin/make -f + +# This file was automatically generated by stdeb 0.10.0 at +# Fri, 20 Sep 2024 01:42:42 +0500 + +%: + dh $@ --with python3 --buildsystem=python_distutils + +override_dh_auto_clean: + python3 setup.py clean -a + find . -name \*.pyc -exec rm {} \; + +override_dh_auto_build: + python3 setup.py build --force + +override_dh_auto_install: + python3 setup.py install --force --root=debian/python3-postinstall --no-compile -O0 --install-layout=deb --prefix=/usr + +override_dh_python2: + dh_python2 --no-guessing-versions + diff --git a/deb_dist/postinstall-1.0.1/debian/source/format b/deb_dist/postinstall-1.0.1/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/deb_dist/postinstall-1.0.1/debian/source/options b/deb_dist/postinstall-1.0.1/debian/source/options new file mode 100644 index 0000000..bcc4bbb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore="\.egg-info$" \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/PKG-INFO b/deb_dist/postinstall-1.0.1/postinstall.egg-info/PKG-INFO new file mode 100644 index 0000000..5450a52 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.0.1 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/SOURCES.txt b/deb_dist/postinstall-1.0.1/postinstall.egg-info/SOURCES.txt new file mode 100644 index 0000000..ea74f79 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/SOURCES.txt @@ -0,0 +1,37 @@ +MANIFEST.in +README.md +pyproject.toml +setup.cfg +setup.py +deb/0002-postinstall.sh +deb/postinst +deb/postinstall-autostart.desktop +deb/postinstall.desktop +deb/postinstall.service +deb/postinstall_start.sh +deb/python3-textual_0.1.13-1_all.deb +deb/textual-0.72.0-py3-none-any.whl +postinstall/__init__.py +postinstall/__main__.py +postinstall/logger.py +postinstall/root.py +postinstall.egg-info/PKG-INFO +postinstall.egg-info/SOURCES.txt +postinstall.egg-info/dependency_links.txt +postinstall.egg-info/entry_points.txt +postinstall.egg-info/requires.txt +postinstall.egg-info/top_level.txt +postinstall/option/__init__.py +postinstall/option/add_user.py +postinstall/option/autostart.py +postinstall/option/database.py +postinstall/option/docker.py +postinstall/option/hostname.py +postinstall/option/install_packages.py +postinstall/option/network.py +postinstall/option/shell.py +postinstall/option/ssh.py +postinstall/option/update.py +tests/__init__.py +tests/docker_config.py +tests/test_docker.py \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/dependency_links.txt b/deb_dist/postinstall-1.0.1/postinstall.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/entry_points.txt b/deb_dist/postinstall-1.0.1/postinstall.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/requires.txt b/deb_dist/postinstall-1.0.1/postinstall.egg-info/requires.txt new file mode 100644 index 0000000..f6c5b61 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/requires.txt @@ -0,0 +1 @@ +pythondialog<4.0.0,>=3.5.3 diff --git a/deb_dist/postinstall-1.0.1/postinstall.egg-info/top_level.txt b/deb_dist/postinstall-1.0.1/postinstall.egg-info/top_level.txt new file mode 100644 index 0000000..6503a29 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall.egg-info/top_level.txt @@ -0,0 +1,2 @@ +postinstall +tests diff --git a/deb_dist/postinstall-1.0.1/postinstall/__init__.py b/deb_dist/postinstall-1.0.1/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.0.1/postinstall/__main__.py b/deb_dist/postinstall-1.0.1/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/postinstall/logger.py b/deb_dist/postinstall-1.0.1/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/__init__.py b/deb_dist/postinstall-1.0.1/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/add_user.py b/deb_dist/postinstall-1.0.1/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/autostart.py b/deb_dist/postinstall-1.0.1/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/database.py b/deb_dist/postinstall-1.0.1/postinstall/option/database.py new file mode 100644 index 0000000..20432b9 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/database.py @@ -0,0 +1,134 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu("Выберите базу данных для установки:", + choices=[("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB")]) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") == dialog.OK: + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ['dpkg-query', '-W', '-f=${Status}', package_name], + check=True, capture_output=True, text=True + ) + return 'install ok installed' in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ''): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ['apt-get', 'update'], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log(dialog, ['apt-get', 'install', '-y', package_name], f"Установка {package_name}"): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed('postgresql-16'): + install_package(dialog, 'postgresql-16') + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed('python3-harlequin-postgres'): + install_package(dialog, 'python3-harlequin-postgres') + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed('mariadb-server'): + install_package(dialog, 'mariadb-server') + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed('python3-harlequin'): + install_package(dialog, 'python3-harlequin') + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed('webmin'): + install_package(dialog, 'webmin') + + # Установка cpanminus + install_package(dialog, 'postgresql-server-dev-16') + install_package(dialog, 'cpanminus') + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(dialog, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(dialog, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(dialog, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/docker.py b/deb_dist/postinstall-1.0.1/postinstall/option/docker.py new file mode 100644 index 0000000..5ef61fb --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/docker.py @@ -0,0 +1,434 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + if ip != "127.0.0.1": + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + try: + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + config = f.read() + port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + if port_ip_match: + ip, port = port_ip_match.groups() + if ip != "127.0.0.1": + progress_callback(3, "Удаление правила UFW") + UFWManager.remove_rule(port) + except FileNotFoundError: + logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def show(self): + while True: + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init="127.0.0.1" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self._handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show(text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"): + CommandExecutor.run_command("docker swarm init") + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/hostname.py b/deb_dist/postinstall-1.0.1/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/install_packages.py b/deb_dist/postinstall-1.0.1/postinstall/option/install_packages.py new file mode 100644 index 0000000..4cc780f --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/install_packages.py @@ -0,0 +1,119 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + +def check_sudo(): + return os.geteuid() == 0 + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log(d, ['apt', 'install', '-y', 'cpanminus'], "Установка cpanminus") + + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log(d, ['sudo', 'ufw', 'allow', '10000/tcp'], "Настройка Firewall"): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + d.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log(d, ['systemctl', 'reload', 'webmin'], "Перезагрузка Webmin"): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/network.py b/deb_dist/postinstall-1.0.1/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/shell.py b/deb_dist/postinstall-1.0.1/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/ssh.py b/deb_dist/postinstall-1.0.1/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.0.1/postinstall/option/update.py b/deb_dist/postinstall-1.0.1/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.0.1/postinstall/root.py b/deb_dist/postinstall-1.0.1/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.0.1/pyproject.toml b/deb_dist/postinstall-1.0.1/pyproject.toml new file mode 100644 index 0000000..6dc49c9 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "postinstall" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +pythondialog = "^3.5.3" + + + +[tool.poetry.scripts] +postinstall = "postinstall:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/deb_dist/postinstall-1.0.1/setup.cfg b/deb_dist/postinstall-1.0.1/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/deb_dist/postinstall-1.0.1/setup.py b/deb_dist/postinstall-1.0.1/setup.py new file mode 100644 index 0000000..7a4a6ea --- /dev/null +++ b/deb_dist/postinstall-1.0.1/setup.py @@ -0,0 +1,45 @@ +import os +import shutil +from setuptools import setup, find_packages +import glob + +deb_dist_dir = "deb_dist" +if os.path.exists(deb_dist_dir): + old_versions = glob.glob(os.path.join(deb_dist_dir, "postinstall-0.3.*")) + for old_version in old_versions: + if os.path.isdir(old_version): + shutil.rmtree(old_version) + else: + os.remove(old_version) + +setup( + name="postinstall", + version="1.0.1", + description="A Dialog TUI application for postinstall system configuration", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="LLC Technologies and Communication", + author_email="devteam@texnico.ru", + packages=find_packages(), + include_package_data=True, + package_data={ + # 'postinstall': ['styles.tcss'], + }, + install_requires=["pythondialog>=3.5.3,<4.0.0"], + entry_points={ + "console_scripts": [ + "postinstall=postinstall:main", + ], + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent :: Linux", + ], + python_requires=">=3.11", + data_files=[ + ("/usr/share/postinstall", []), + ("/etc/systemd/system", ["deb/postinstall.service"]), + ("/etc/profile.d", ["deb/0002-postinstall.sh"]), + ], +) diff --git a/deb_dist/postinstall-1.0.1/tests/__init__.py b/deb_dist/postinstall-1.0.1/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/deb_dist/postinstall-1.0.1/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/deb_dist/postinstall-1.0.1/tests/docker_config.py b/deb_dist/postinstall-1.0.1/tests/docker_config.py new file mode 100644 index 0000000..62ddf0a --- /dev/null +++ b/deb_dist/postinstall-1.0.1/tests/docker_config.py @@ -0,0 +1,132 @@ +import logging +import subprocess +import os +import re + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class DockerConfig: + def check_installed(self, command: str) -> bool: + try: + return ( + subprocess.run( + ["which", command], capture_output=True, text=True + ).returncode + == 0 + ) + except subprocess.SubprocessError as e: + logging.error(f"Error checking if {command} is installed: {e}") + return False + + def install_docker(self): + try: + if not self.check_installed("docker"): + logging.info("Установка Docker...") + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True) + logging.info("Docker успешно установился") + + if not self.check_installed("docker-compose"): + logging.info("Установка Docker Compose...") + subprocess.run( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + logging.info("Docker Compose успешно установился") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Docker: {e}") + raise + + def check_docker_login(self, registry_url): + try: + result = subprocess.run( + ["docker", "login", registry_url, "--password-stdin"], + input=b"\n", + capture_output=True, + text=True, + check=True, + ) + return "login succeeded" in result.stdout.lower() + except subprocess.CalledProcessError as e: + logging.error(f"Error checking Docker login: {e}") + return False + + def login_to_registry(self): + try: + if not self.check_docker_login(REGISTRY_URL): + logging.info("Вход в Docker registry...") + subprocess.run( + ["docker", "login", REGISTRY_URL], + input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(), + check=True, + ) + logging.info("Успешный вход в Docker registry") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при входе в Docker registry: {e}") + raise + + def install_portainer(self, port, login, password, ip): + try: + if not self.check_docker_login(REGISTRY_URL): + self.login_to_registry() + + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + docker_compose_content = f""" +version: '3' + +services: + portainer: + image: dr.gosrv.ru/portainer + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer_data:/data + - ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password + ports: + - "{ip}:{port}:9000" + environment: + - ADMIN_USER={login} + - ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password + +volumes: + portainer_data: + """ + + with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + subprocess.run( + ["htpasswd", "-nb", login, password], + stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"), + check=True, + ) + + subprocess.run( + [ + "docker-compose", + "-f", + DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", + "up", + "-d", + ], + check=True, + ) + logging.info("Portainer успешно установлен") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Portainer: {e}") + raise + except IOError as e: + logging.error(f"Ошибка при создании файла: {e}") + raise + + def validate_input(self, value, pattern, error_message): + if not re.match(pattern, value): + return False + return True diff --git a/deb_dist/postinstall-1.0.1/tests/test_docker.py b/deb_dist/postinstall-1.0.1/tests/test_docker.py new file mode 100644 index 0000000..b5caf04 --- /dev/null +++ b/deb_dist/postinstall-1.0.1/tests/test_docker.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH + +class TestDockerConfig(unittest.TestCase): + def setUp(self): + self.docker_config = DockerConfig() + + @patch("subprocess.run") + def test_check_installed_true(self, mock_run): + mock_run.return_value.returncode = 0 + self.assertTrue(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_check_installed_false(self, mock_run): + mock_run.return_value.returncode = 1 + self.assertFalse(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_install_docker(self, mock_run): + self.docker_config.install_docker() + mock_run.assert_any_call(["apt-get", "update"], check=True) + mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True) + mock_run.assert_any_call( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + + @patch("subprocess.run") + def test_check_docker_login_success(self, mock_run): + mock_run.return_value.stdout = "Login Succeeded" + self.assertTrue(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + def test_check_docker_login_failure(self, mock_run): + mock_run.return_value.stdout = "Login Failed" + self.assertFalse(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + def test_login_to_registry(self, mock_check_login, mock_run): + mock_check_login.return_value = False + self.docker_config.login_to_registry() + mock_run.assert_called_once() + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + @patch("docker_config.DockerConfig.login_to_registry") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run): + mock_check_login.return_value = True + self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1") + mock_open.assert_called() + mock_run.assert_called() + + def test_validate_input_success(self): + self.assertTrue( + self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + def test_validate_input_failure(self): + self.assertFalse( + self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/deb_dist/postinstall-1.5.0/.pc/.quilt_patches b/deb_dist/postinstall-1.5.0/.pc/.quilt_patches new file mode 100644 index 0000000..6857a8d --- /dev/null +++ b/deb_dist/postinstall-1.5.0/.pc/.quilt_patches @@ -0,0 +1 @@ +debian/patches diff --git a/deb_dist/postinstall-1.5.0/.pc/.quilt_series b/deb_dist/postinstall-1.5.0/.pc/.quilt_series new file mode 100644 index 0000000..c206706 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/.pc/.quilt_series @@ -0,0 +1 @@ +series diff --git a/deb_dist/postinstall-1.5.0/.pc/.version b/deb_dist/postinstall-1.5.0/.pc/.version new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/.pc/.version @@ -0,0 +1 @@ +2 diff --git a/deb_dist/postinstall-1.5.0/.pc/applied-patches b/deb_dist/postinstall-1.5.0/.pc/applied-patches new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/MANIFEST.in b/deb_dist/postinstall-1.5.0/MANIFEST.in new file mode 100644 index 0000000..7223d6f --- /dev/null +++ b/deb_dist/postinstall-1.5.0/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +include postinstall/styles.tcss diff --git a/deb_dist/postinstall-1.5.0/PKG-INFO b/deb_dist/postinstall-1.5.0/PKG-INFO new file mode 100644 index 0000000..27af7d6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.5.0 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.5.0/README.md b/deb_dist/postinstall-1.5.0/README.md new file mode 100644 index 0000000..5f89cbf --- /dev/null +++ b/deb_dist/postinstall-1.5.0/README.md @@ -0,0 +1,230 @@ +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/__init__.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/__main__.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/logger.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/__init__.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/add_user.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/autostart.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/database.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/database.py new file mode 100644 index 0000000..9fb9a66 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/database.py @@ -0,0 +1,157 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu( + "Выберите базу данных для установки:", + choices=[ + ("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB"), + ], + ) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if ( + dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") + == dialog.OK + ): + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Status}", package_name], + check=True, + capture_output=True, + text=True, + ) + return "install ok installed" in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ""): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log( + dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}" + ): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed("postgresql-16"): + install_package(dialog, "postgresql-16") + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed("python3-harlequin-postgres"): + install_package(dialog, "python3-harlequin-postgres") + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed("mariadb-server"): + install_package(dialog, "mariadb-server") + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed("webmin"): + install_package(dialog, "webmin") + + # Установка cpanminus + install_package(dialog, "postgresql-server-dev-16") + install_package(dialog, "cpanminus") + + run_command_with_log( + dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI" + ): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg" + ): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/docker.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/docker.py new file mode 100644 index 0000000..4f373b3 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/docker.py @@ -0,0 +1,533 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +import socket + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "anon_user" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + # if ip != "127.0.0.1": + if True: + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + # try: + # with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + # config = f.read() + # port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + # if port_ip_match: + # ip, port = port_ip_match.groups() + # # if True: + # # progress_callback(3, "Удаление правила UFW") + # # UFWManager.remove_rule(port) + # except FileNotFoundError: + # logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/base_images/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + if SystemChecker.is_command_installed("docker"): + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + choices.append(("2", "Установить Portainer")) + else: + choices.append(("2", "Удалить Portainer")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def get_default_interface_ip(self): + try: + # Выполнение команды для получения IP-адреса основного интерфейса + command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1" + result = subprocess.check_output(command, shell=True, text=True) + return result.strip() # Возвращаем IP-адрес без лишних символов + except subprocess.CalledProcessError: + return "127.0.0.1" # В случае ошибки вернём стандартный IP + + def show(self): + # Получаем IP-адрес основного сетевого интерфейса + default_ip = self.get_default_interface_ip() + + while True: + # Используем полученный IP-адрес как начальное значение + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init=default_ip + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class NetworkInterfaceInput(UIElement): + def get_network_interfaces(self): + try: + # Get all network interfaces + interfaces = subprocess.check_output( + "ip -o link show", shell=True, text=True + ) + # Extract interface names and their IP addresses + interface_list = [] + for line in interfaces.split("\n"): + if line: + interface = line.split(":")[1].strip() + ip = self.get_ip_address(interface) + if ip: + interface_list.append((interface, ip)) + return interface_list + except subprocess.CalledProcessError: + return [] + + def get_ip_address(self, interface): + try: + ip = subprocess.check_output( + f"ip -o -4 addr list {interface}", shell=True, text=True + ) + return ip.split()[3].split("/")[0] + except subprocess.CalledProcessError: + return None + + def show(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.dialog.msgbox( + "Не удалось получить список сетевых интерфейсов.", width=50, height=10 + ) + return None + + choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces] + code, choice = self.dialog.menu( + "Выберите сетевой интерфейс для Docker Swarm:", choices=choices + ) + + if code == self.dialog.OK: + return choice + return None + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + self.network_interface_input = NetworkInterfaceInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self.handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show( + text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?" + ): + interface = self.network_interface_input.show() + if interface: + ip = interface.split("(")[1].split(")")[0] + if ip: + try: + CommandExecutor.run_command( + f"docker swarm init --advertise-addr {ip}" + ) + self.dialog.msgbox( + f"Docker Swarm успешно инициализирован на интерфейсе {interface}", + width=50, + height=10, + ) + except subprocess.CalledProcessError as e: + self.dialog.msgbox( + f"Ошибка при инициализации Docker Swarm: {str(e)}", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + f"Не удалось получить IP-адрес для интерфейса {interface}.", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + "Инициализация Docker Swarm отменена.", width=50, height=10 + ) + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/hostname.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/install_packages.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/install_packages.py new file mode 100644 index 0000000..f66eccd --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/install_packages.py @@ -0,0 +1,127 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + + +def check_sudo(): + return os.geteuid() == 0 + + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log( + d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + # # Установка модулей DBI и DBD::Pg через cpanminus + # if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + # d.msgbox("Модуль DBI успешно установлен.") + # if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + # d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/network.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/shell.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/ssh.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/update.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.5.0/build/lib/postinstall/root.py b/deb_dist/postinstall-1.5.0/build/lib/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.5.0/build/lib/tests/__init__.py b/deb_dist/postinstall-1.5.0/build/lib/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/deb_dist/postinstall-1.5.0/build/lib/tests/docker_config.py b/deb_dist/postinstall-1.5.0/build/lib/tests/docker_config.py new file mode 100644 index 0000000..62ddf0a --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/tests/docker_config.py @@ -0,0 +1,132 @@ +import logging +import subprocess +import os +import re + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class DockerConfig: + def check_installed(self, command: str) -> bool: + try: + return ( + subprocess.run( + ["which", command], capture_output=True, text=True + ).returncode + == 0 + ) + except subprocess.SubprocessError as e: + logging.error(f"Error checking if {command} is installed: {e}") + return False + + def install_docker(self): + try: + if not self.check_installed("docker"): + logging.info("Установка Docker...") + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True) + logging.info("Docker успешно установился") + + if not self.check_installed("docker-compose"): + logging.info("Установка Docker Compose...") + subprocess.run( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + logging.info("Docker Compose успешно установился") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Docker: {e}") + raise + + def check_docker_login(self, registry_url): + try: + result = subprocess.run( + ["docker", "login", registry_url, "--password-stdin"], + input=b"\n", + capture_output=True, + text=True, + check=True, + ) + return "login succeeded" in result.stdout.lower() + except subprocess.CalledProcessError as e: + logging.error(f"Error checking Docker login: {e}") + return False + + def login_to_registry(self): + try: + if not self.check_docker_login(REGISTRY_URL): + logging.info("Вход в Docker registry...") + subprocess.run( + ["docker", "login", REGISTRY_URL], + input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(), + check=True, + ) + logging.info("Успешный вход в Docker registry") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при входе в Docker registry: {e}") + raise + + def install_portainer(self, port, login, password, ip): + try: + if not self.check_docker_login(REGISTRY_URL): + self.login_to_registry() + + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + docker_compose_content = f""" +version: '3' + +services: + portainer: + image: dr.gosrv.ru/portainer + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer_data:/data + - ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password + ports: + - "{ip}:{port}:9000" + environment: + - ADMIN_USER={login} + - ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password + +volumes: + portainer_data: + """ + + with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + subprocess.run( + ["htpasswd", "-nb", login, password], + stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"), + check=True, + ) + + subprocess.run( + [ + "docker-compose", + "-f", + DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", + "up", + "-d", + ], + check=True, + ) + logging.info("Portainer успешно установлен") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Portainer: {e}") + raise + except IOError as e: + logging.error(f"Ошибка при создании файла: {e}") + raise + + def validate_input(self, value, pattern, error_message): + if not re.match(pattern, value): + return False + return True diff --git a/deb_dist/postinstall-1.5.0/build/lib/tests/test_docker.py b/deb_dist/postinstall-1.5.0/build/lib/tests/test_docker.py new file mode 100644 index 0000000..b5caf04 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/build/lib/tests/test_docker.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH + +class TestDockerConfig(unittest.TestCase): + def setUp(self): + self.docker_config = DockerConfig() + + @patch("subprocess.run") + def test_check_installed_true(self, mock_run): + mock_run.return_value.returncode = 0 + self.assertTrue(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_check_installed_false(self, mock_run): + mock_run.return_value.returncode = 1 + self.assertFalse(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_install_docker(self, mock_run): + self.docker_config.install_docker() + mock_run.assert_any_call(["apt-get", "update"], check=True) + mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True) + mock_run.assert_any_call( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + + @patch("subprocess.run") + def test_check_docker_login_success(self, mock_run): + mock_run.return_value.stdout = "Login Succeeded" + self.assertTrue(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + def test_check_docker_login_failure(self, mock_run): + mock_run.return_value.stdout = "Login Failed" + self.assertFalse(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + def test_login_to_registry(self, mock_check_login, mock_run): + mock_check_login.return_value = False + self.docker_config.login_to_registry() + mock_run.assert_called_once() + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + @patch("docker_config.DockerConfig.login_to_registry") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run): + mock_check_login.return_value = True + self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1") + mock_open.assert_called() + mock_run.assert_called() + + def test_validate_input_success(self): + self.assertTrue( + self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + def test_validate_input_failure(self): + self.assertFalse( + self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/deb_dist/postinstall-1.5.0/deb/0002-postinstall.sh b/deb_dist/postinstall-1.5.0/deb/0002-postinstall.sh new file mode 100644 index 0000000..95998cb --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/0002-postinstall.sh @@ -0,0 +1,3 @@ +if [ -n "$PS1" ]; then + [ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear ) +fi diff --git a/deb_dist/postinstall-1.5.0/deb/postinst b/deb_dist/postinstall-1.5.0/deb/postinst new file mode 100755 index 0000000..62aaaee --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/postinst @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +mkdir -p /var/lib/postinstall + +if [ ! -f /var/lib/postinstall/first_run_done ]; then + chmod +x /usr/share/postinstall/postinstall_start.sh + cp /usr/share/postinstall/postinstall.service /etc/systemd/system/ + systemctl enable postinstall.service + pip install --upgrade textual --break-system-packages +fi + diff --git a/deb_dist/postinstall-1.5.0/deb/postinstall-autostart.desktop b/deb_dist/postinstall-1.5.0/deb/postinstall-autostart.desktop new file mode 100644 index 0000000..cbf43df --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/postinstall-autostart.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; +X-GNOME-Autostart-enabled=true diff --git a/deb_dist/postinstall-1.5.0/deb/postinstall.desktop b/deb_dist/postinstall-1.5.0/deb/postinstall.desktop new file mode 100644 index 0000000..fb63863 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/postinstall.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=PostInstall +Comment=Textual TUI Application for Network Configuration +Exec=pkexec /usr/bin/postinstall +Icon=postinstall +Terminal=true +Type=Application +Categories=Utility;Network; diff --git a/deb_dist/postinstall-1.5.0/deb/postinstall.service b/deb_dist/postinstall-1.5.0/deb/postinstall.service new file mode 100644 index 0000000..f10a39d --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/postinstall.service @@ -0,0 +1,15 @@ +[Unit] +Description=Postinstall TUI Application +After=network.target + +[Service] +ExecStart=/usr/bin/postinstall +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty + +[Install] +WantedBy=multi-user.target diff --git a/deb_dist/postinstall-1.5.0/deb/postinstall_start.sh b/deb_dist/postinstall-1.5.0/deb/postinstall_start.sh new file mode 100755 index 0000000..05fdf4d --- /dev/null +++ b/deb_dist/postinstall-1.5.0/deb/postinstall_start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/postinstall diff --git a/deb_dist/postinstall-1.5.0/deb/python3-textual_0.1.13-1_all.deb b/deb_dist/postinstall-1.5.0/deb/python3-textual_0.1.13-1_all.deb new file mode 100644 index 0000000..46bd888 Binary files /dev/null and b/deb_dist/postinstall-1.5.0/deb/python3-textual_0.1.13-1_all.deb differ diff --git a/deb_dist/postinstall-1.5.0/deb/textual-0.72.0-py3-none-any.whl b/deb_dist/postinstall-1.5.0/deb/textual-0.72.0-py3-none-any.whl new file mode 100644 index 0000000..03e3a85 Binary files /dev/null and b/deb_dist/postinstall-1.5.0/deb/textual-0.72.0-py3-none-any.whl differ diff --git a/deb_dist/postinstall-1.5.0/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed b/deb_dist/postinstall-1.5.0/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..6157f01 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/.debhelper/generated/python3-postinstall/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +postinstall (1.5.0-1) stable; urgency=low + + * source package automatically created by stdeb 0.10.0 + + -- LLC Technologies and Communication Wed, 27 Nov 2024 15:29:20 +0500 diff --git a/deb_dist/postinstall-1.5.0/debian/.debhelper/generated/python3-postinstall/installed-by-dh_installdocs b/deb_dist/postinstall-1.5.0/debian/.debhelper/generated/python3-postinstall/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/debian/changelog b/deb_dist/postinstall-1.5.0/debian/changelog new file mode 100644 index 0000000..6157f01 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/changelog @@ -0,0 +1,5 @@ +postinstall (1.5.0-1) stable; urgency=low + + * source package automatically created by stdeb 0.10.0 + + -- LLC Technologies and Communication Wed, 27 Nov 2024 15:29:20 +0500 diff --git a/deb_dist/postinstall-1.5.0/debian/compat b/deb_dist/postinstall-1.5.0/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/compat @@ -0,0 +1 @@ +9 diff --git a/deb_dist/postinstall-1.5.0/debian/control b/deb_dist/postinstall-1.5.0/debian/control new file mode 100644 index 0000000..33340ed --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/control @@ -0,0 +1,33 @@ +Source: postinstall +Maintainer: LLC Technologies and Communication +Section: python +Priority: optional +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9) +Standards-Version: 3.9.1 +Homepage: None + +Package: python3-postinstall +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends} +Description: A Dialog TUI application for postinstall system configuratio + ## Документация Postinstall + . + Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. + ### **1. Сетевые настройки** + . + Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + . + #### **1.1 Выбор сетевого интерфейса** + . + Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + . + ![Номер лицензии](screenshots/network_interface.png) + . + . + #### **1.2 Настройка DHCP** + . + Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + . + #### **1.3 Настройка статического IP** + . + diff --git a/deb_dist/postinstall-1.5.0/debian/files b/deb_dist/postinstall-1.5.0/debian/files new file mode 100644 index 0000000..d7ac77e --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/files @@ -0,0 +1,2 @@ +postinstall_1.5.0-1_amd64.buildinfo python optional +python3-postinstall_1.5.0-1_all.deb python optional diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall.debhelper.log b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.debhelper.log new file mode 100644 index 0000000..de33f70 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.debhelper.log @@ -0,0 +1,19 @@ +dh_update_autotools_config +dh_auto_configure +dh_auto_build +dh_auto_test +dh_prep +dh_auto_install +dh_installdocs +dh_installchangelogs +dh_installinit +dh_perl +dh_link +dh_strip_nondeterminism +dh_compress +dh_fixperms +dh_missing +dh_installdeb +dh_gencontrol +dh_md5sums +dh_builddeb diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall.postinst.debhelper b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.postinst.debhelper new file mode 100644 index 0000000..c31c075 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p python3-postinstall +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-postinstall || true +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall.prerm.debhelper b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.prerm.debhelper new file mode 100644 index 0000000..e6a54f7 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p python3-postinstall +else + dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall.substvars b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.substvars new file mode 100644 index 0000000..b8e1dee --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-dialog, python3:any +misc:Depends= +misc:Pre-Depends= diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/conffiles b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/conffiles new file mode 100644 index 0000000..a294aea --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/conffiles @@ -0,0 +1,2 @@ +/etc/profile.d/0002-postinstall.sh +/etc/systemd/system/postinstall.service diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/control b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/control new file mode 100644 index 0000000..a8bacfb --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/control @@ -0,0 +1,30 @@ +Package: python3-postinstall +Source: postinstall +Version: 1.5.0-1 +Architecture: all +Maintainer: LLC Technologies and Communication +Installed-Size: 132 +Depends: python3-dialog, python3:any +Section: python +Priority: optional +Homepage: None +Description: A Dialog TUI application for postinstall system configuratio + ## Документация Postinstall + . + Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. + ### **1. Сетевые настройки** + . + Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + . + #### **1.1 Выбор сетевого интерфейса** + . + Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + . + ![Номер лицензии](screenshots/network_interface.png) + . + . + #### **1.2 Настройка DHCP** + . + Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + . + #### **1.3 Настройка статического IP** diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/md5sums b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/md5sums new file mode 100644 index 0000000..9505204 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/md5sums @@ -0,0 +1,22 @@ +04a70127767c205f3dc92b73f521a3ee usr/bin/postinstall +44a9b40eff604b04d07a03b1a9dbb4c6 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/dependency_links.txt +bcaee1a445a35da75a848bd8cee176a7 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/entry_points.txt +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/requires.txt +147b741cc4a5c60607a1dc9f4fa92ca1 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/top_level.txt +2a4a742cf1d86768584347780bcd16f6 usr/lib/python3/dist-packages/postinstall/__init__.py +666d344c47c8050391790adb6a9d69bb usr/lib/python3/dist-packages/postinstall/__main__.py +9e3443cf16cce74b2dcf317b122aee4f usr/lib/python3/dist-packages/postinstall/logger.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall/option/__init__.py +ed35c883b020ece0a9933a55383a08a5 usr/lib/python3/dist-packages/postinstall/option/add_user.py +13e13a3b080d38a57fc984bcc09d6291 usr/lib/python3/dist-packages/postinstall/option/autostart.py +f391e7e0329deeafc4c1bc3798a81e57 usr/lib/python3/dist-packages/postinstall/option/database.py +d1b06abeee3abbcb3047e873c3270465 usr/lib/python3/dist-packages/postinstall/option/docker.py +55583cfd043bf938df8ca95b368b68bc usr/lib/python3/dist-packages/postinstall/option/hostname.py +a6485960a6c9f2c3fed14925d9a5c7fa usr/lib/python3/dist-packages/postinstall/option/install_packages.py +4ad632661661fd0449f5cc1127471ab1 usr/lib/python3/dist-packages/postinstall/option/network.py +39961879cf3f2b241f946c61830ea018 usr/lib/python3/dist-packages/postinstall/option/shell.py +f47b245cb5673e70988e801ee3776d0a usr/lib/python3/dist-packages/postinstall/option/ssh.py +abb826ae4319de07a7b312a38652e0d2 usr/lib/python3/dist-packages/postinstall/option/update.py +633499d822e582c720519dc6b4f80f80 usr/lib/python3/dist-packages/postinstall/root.py +fd2df9873601eb2b957cd774dd5f11a2 usr/share/doc/python3-postinstall/changelog.Debian.gz diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/postinst b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/postinst new file mode 100755 index 0000000..00f76af --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p python3-postinstall +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-postinstall || true +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/prerm b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/prerm new file mode 100755 index 0000000..edf4a44 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p python3-postinstall +else + dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh new file mode 100644 index 0000000..95998cb --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/profile.d/0002-postinstall.sh @@ -0,0 +1,3 @@ +if [ -n "$PS1" ]; then + [ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear ) +fi diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/systemd/system/postinstall.service b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/systemd/system/postinstall.service new file mode 100644 index 0000000..f10a39d --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/etc/systemd/system/postinstall.service @@ -0,0 +1,15 @@ +[Unit] +Description=Postinstall TUI Application +After=network.target + +[Service] +ExecStart=/usr/bin/postinstall +Restart=always +User=root +Environment=PYTHONUNBUFFERED=1 +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/tty + +[Install] +WantedBy=multi-user.target diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/bin/postinstall b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/bin/postinstall new file mode 100755 index 0000000..9e6c860 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/bin/postinstall @@ -0,0 +1,33 @@ +#! /usr/bin/python3 +# EASY-INSTALL-ENTRY-SCRIPT: 'postinstall==1.5.0','console_scripts','postinstall' +import re +import sys + +# for compatibility with easy_install; see #2198 +__requires__ = 'postinstall==1.5.0' + +try: + from importlib.metadata import distribution +except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + +def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + +globals().setdefault('load_entry_point', importlib_load_entry_point) + + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(load_entry_point('postinstall==1.5.0', 'console_scripts', 'postinstall')()) diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/PKG-INFO b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/PKG-INFO new file mode 100644 index 0000000..27af7d6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.5.0 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/dependency_links.txt b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/entry_points.txt b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/requires.txt b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/requires.txt new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/top_level.txt b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/top_level.txt new file mode 100644 index 0000000..6503a29 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/top_level.txt @@ -0,0 +1,2 @@ +postinstall +tests diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/__init__.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py new file mode 100644 index 0000000..9fb9a66 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/database.py @@ -0,0 +1,157 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu( + "Выберите базу данных для установки:", + choices=[ + ("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB"), + ], + ) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if ( + dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") + == dialog.OK + ): + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Status}", package_name], + check=True, + capture_output=True, + text=True, + ) + return "install ok installed" in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ""): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log( + dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}" + ): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed("postgresql-16"): + install_package(dialog, "postgresql-16") + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed("python3-harlequin-postgres"): + install_package(dialog, "python3-harlequin-postgres") + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed("mariadb-server"): + install_package(dialog, "mariadb-server") + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed("webmin"): + install_package(dialog, "webmin") + + # Установка cpanminus + install_package(dialog, "postgresql-server-dev-16") + install_package(dialog, "cpanminus") + + run_command_with_log( + dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI" + ): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg" + ): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py new file mode 100644 index 0000000..4f373b3 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/docker.py @@ -0,0 +1,533 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +import socket + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "anon_user" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + # if ip != "127.0.0.1": + if True: + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + # try: + # with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + # config = f.read() + # port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + # if port_ip_match: + # ip, port = port_ip_match.groups() + # # if True: + # # progress_callback(3, "Удаление правила UFW") + # # UFWManager.remove_rule(port) + # except FileNotFoundError: + # logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/base_images/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + if SystemChecker.is_command_installed("docker"): + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + choices.append(("2", "Установить Portainer")) + else: + choices.append(("2", "Удалить Portainer")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def get_default_interface_ip(self): + try: + # Выполнение команды для получения IP-адреса основного интерфейса + command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1" + result = subprocess.check_output(command, shell=True, text=True) + return result.strip() # Возвращаем IP-адрес без лишних символов + except subprocess.CalledProcessError: + return "127.0.0.1" # В случае ошибки вернём стандартный IP + + def show(self): + # Получаем IP-адрес основного сетевого интерфейса + default_ip = self.get_default_interface_ip() + + while True: + # Используем полученный IP-адрес как начальное значение + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init=default_ip + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class NetworkInterfaceInput(UIElement): + def get_network_interfaces(self): + try: + # Get all network interfaces + interfaces = subprocess.check_output( + "ip -o link show", shell=True, text=True + ) + # Extract interface names and their IP addresses + interface_list = [] + for line in interfaces.split("\n"): + if line: + interface = line.split(":")[1].strip() + ip = self.get_ip_address(interface) + if ip: + interface_list.append((interface, ip)) + return interface_list + except subprocess.CalledProcessError: + return [] + + def get_ip_address(self, interface): + try: + ip = subprocess.check_output( + f"ip -o -4 addr list {interface}", shell=True, text=True + ) + return ip.split()[3].split("/")[0] + except subprocess.CalledProcessError: + return None + + def show(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.dialog.msgbox( + "Не удалось получить список сетевых интерфейсов.", width=50, height=10 + ) + return None + + choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces] + code, choice = self.dialog.menu( + "Выберите сетевой интерфейс для Docker Swarm:", choices=choices + ) + + if code == self.dialog.OK: + return choice + return None + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + self.network_interface_input = NetworkInterfaceInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self.handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show( + text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?" + ): + interface = self.network_interface_input.show() + if interface: + ip = interface.split("(")[1].split(")")[0] + if ip: + try: + CommandExecutor.run_command( + f"docker swarm init --advertise-addr {ip}" + ) + self.dialog.msgbox( + f"Docker Swarm успешно инициализирован на интерфейсе {interface}", + width=50, + height=10, + ) + except subprocess.CalledProcessError as e: + self.dialog.msgbox( + f"Ошибка при инициализации Docker Swarm: {str(e)}", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + f"Не удалось получить IP-адрес для интерфейса {interface}.", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + "Инициализация Docker Swarm отменена.", width=50, height=10 + ) + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py new file mode 100644 index 0000000..f66eccd --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/install_packages.py @@ -0,0 +1,127 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + + +def check_sudo(): + return os.geteuid() == 0 + + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log( + d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + # # Установка модулей DBI и DBD::Pg через cpanminus + # if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + # d.msgbox("Модуль DBI успешно установлен.") + # if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + # d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/lib/python3/dist-packages/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz new file mode 100644 index 0000000..3cfbbaf Binary files /dev/null and b/deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/share/doc/python3-postinstall/changelog.Debian.gz differ diff --git a/deb_dist/postinstall-1.5.0/debian/rules b/deb_dist/postinstall-1.5.0/debian/rules new file mode 100755 index 0000000..5650e03 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/rules @@ -0,0 +1,21 @@ +#!/usr/bin/make -f + +# This file was automatically generated by stdeb 0.10.0 at +# Wed, 27 Nov 2024 15:29:20 +0500 + +%: + dh $@ --with python3 --buildsystem=python_distutils + +override_dh_auto_clean: + python3 setup.py clean -a + find . -name \*.pyc -exec rm {} \; + +override_dh_auto_build: + python3 setup.py build --force + +override_dh_auto_install: + python3 setup.py install --force --root=debian/python3-postinstall --no-compile -O0 --install-layout=deb --prefix=/usr + +override_dh_python2: + dh_python2 --no-guessing-versions + diff --git a/deb_dist/postinstall-1.5.0/debian/source/format b/deb_dist/postinstall-1.5.0/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/deb_dist/postinstall-1.5.0/debian/source/options b/deb_dist/postinstall-1.5.0/debian/source/options new file mode 100644 index 0000000..bcc4bbb --- /dev/null +++ b/deb_dist/postinstall-1.5.0/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore="\.egg-info$" \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/PKG-INFO b/deb_dist/postinstall-1.5.0/postinstall.egg-info/PKG-INFO new file mode 100644 index 0000000..27af7d6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.5.0 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/SOURCES.txt b/deb_dist/postinstall-1.5.0/postinstall.egg-info/SOURCES.txt new file mode 100644 index 0000000..ea74f79 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/SOURCES.txt @@ -0,0 +1,37 @@ +MANIFEST.in +README.md +pyproject.toml +setup.cfg +setup.py +deb/0002-postinstall.sh +deb/postinst +deb/postinstall-autostart.desktop +deb/postinstall.desktop +deb/postinstall.service +deb/postinstall_start.sh +deb/python3-textual_0.1.13-1_all.deb +deb/textual-0.72.0-py3-none-any.whl +postinstall/__init__.py +postinstall/__main__.py +postinstall/logger.py +postinstall/root.py +postinstall.egg-info/PKG-INFO +postinstall.egg-info/SOURCES.txt +postinstall.egg-info/dependency_links.txt +postinstall.egg-info/entry_points.txt +postinstall.egg-info/requires.txt +postinstall.egg-info/top_level.txt +postinstall/option/__init__.py +postinstall/option/add_user.py +postinstall/option/autostart.py +postinstall/option/database.py +postinstall/option/docker.py +postinstall/option/hostname.py +postinstall/option/install_packages.py +postinstall/option/network.py +postinstall/option/shell.py +postinstall/option/ssh.py +postinstall/option/update.py +tests/__init__.py +tests/docker_config.py +tests/test_docker.py \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/dependency_links.txt b/deb_dist/postinstall-1.5.0/postinstall.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/entry_points.txt b/deb_dist/postinstall-1.5.0/postinstall.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/requires.txt b/deb_dist/postinstall-1.5.0/postinstall.egg-info/requires.txt new file mode 100644 index 0000000..f6c5b61 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/requires.txt @@ -0,0 +1 @@ +pythondialog<4.0.0,>=3.5.3 diff --git a/deb_dist/postinstall-1.5.0/postinstall.egg-info/top_level.txt b/deb_dist/postinstall-1.5.0/postinstall.egg-info/top_level.txt new file mode 100644 index 0000000..6503a29 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall.egg-info/top_level.txt @@ -0,0 +1,2 @@ +postinstall +tests diff --git a/deb_dist/postinstall-1.5.0/postinstall/__init__.py b/deb_dist/postinstall-1.5.0/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/deb_dist/postinstall-1.5.0/postinstall/__main__.py b/deb_dist/postinstall-1.5.0/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/postinstall/logger.py b/deb_dist/postinstall-1.5.0/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/__init__.py b/deb_dist/postinstall-1.5.0/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/add_user.py b/deb_dist/postinstall-1.5.0/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/autostart.py b/deb_dist/postinstall-1.5.0/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/database.py b/deb_dist/postinstall-1.5.0/postinstall/option/database.py new file mode 100644 index 0000000..9fb9a66 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/database.py @@ -0,0 +1,157 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu( + "Выберите базу данных для установки:", + choices=[ + ("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB"), + ], + ) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if ( + dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") + == dialog.OK + ): + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Status}", package_name], + check=True, + capture_output=True, + text=True, + ) + return "install ok installed" in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ""): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log( + dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}" + ): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed("postgresql-16"): + install_package(dialog, "postgresql-16") + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed("python3-harlequin-postgres"): + install_package(dialog, "python3-harlequin-postgres") + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed("mariadb-server"): + install_package(dialog, "mariadb-server") + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed("webmin"): + install_package(dialog, "webmin") + + # Установка cpanminus + install_package(dialog, "postgresql-server-dev-16") + install_package(dialog, "cpanminus") + + run_command_with_log( + dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI" + ): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg" + ): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/docker.py b/deb_dist/postinstall-1.5.0/postinstall/option/docker.py new file mode 100644 index 0000000..4f373b3 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/docker.py @@ -0,0 +1,533 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +import socket + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "anon_user" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + # if ip != "127.0.0.1": + if True: + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + # try: + # with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + # config = f.read() + # port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + # if port_ip_match: + # ip, port = port_ip_match.groups() + # # if True: + # # progress_callback(3, "Удаление правила UFW") + # # UFWManager.remove_rule(port) + # except FileNotFoundError: + # logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/base_images/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + if SystemChecker.is_command_installed("docker"): + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + choices.append(("2", "Установить Portainer")) + else: + choices.append(("2", "Удалить Portainer")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def get_default_interface_ip(self): + try: + # Выполнение команды для получения IP-адреса основного интерфейса + command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1" + result = subprocess.check_output(command, shell=True, text=True) + return result.strip() # Возвращаем IP-адрес без лишних символов + except subprocess.CalledProcessError: + return "127.0.0.1" # В случае ошибки вернём стандартный IP + + def show(self): + # Получаем IP-адрес основного сетевого интерфейса + default_ip = self.get_default_interface_ip() + + while True: + # Используем полученный IP-адрес как начальное значение + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init=default_ip + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class NetworkInterfaceInput(UIElement): + def get_network_interfaces(self): + try: + # Get all network interfaces + interfaces = subprocess.check_output( + "ip -o link show", shell=True, text=True + ) + # Extract interface names and their IP addresses + interface_list = [] + for line in interfaces.split("\n"): + if line: + interface = line.split(":")[1].strip() + ip = self.get_ip_address(interface) + if ip: + interface_list.append((interface, ip)) + return interface_list + except subprocess.CalledProcessError: + return [] + + def get_ip_address(self, interface): + try: + ip = subprocess.check_output( + f"ip -o -4 addr list {interface}", shell=True, text=True + ) + return ip.split()[3].split("/")[0] + except subprocess.CalledProcessError: + return None + + def show(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.dialog.msgbox( + "Не удалось получить список сетевых интерфейсов.", width=50, height=10 + ) + return None + + choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces] + code, choice = self.dialog.menu( + "Выберите сетевой интерфейс для Docker Swarm:", choices=choices + ) + + if code == self.dialog.OK: + return choice + return None + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + self.network_interface_input = NetworkInterfaceInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self.handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show( + text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?" + ): + interface = self.network_interface_input.show() + if interface: + ip = interface.split("(")[1].split(")")[0] + if ip: + try: + CommandExecutor.run_command( + f"docker swarm init --advertise-addr {ip}" + ) + self.dialog.msgbox( + f"Docker Swarm успешно инициализирован на интерфейсе {interface}", + width=50, + height=10, + ) + except subprocess.CalledProcessError as e: + self.dialog.msgbox( + f"Ошибка при инициализации Docker Swarm: {str(e)}", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + f"Не удалось получить IP-адрес для интерфейса {interface}.", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + "Инициализация Docker Swarm отменена.", width=50, height=10 + ) + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/hostname.py b/deb_dist/postinstall-1.5.0/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/install_packages.py b/deb_dist/postinstall-1.5.0/postinstall/option/install_packages.py new file mode 100644 index 0000000..f66eccd --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/install_packages.py @@ -0,0 +1,127 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + + +def check_sudo(): + return os.geteuid() == 0 + + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log( + d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + # # Установка модулей DBI и DBD::Pg через cpanminus + # if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + # d.msgbox("Модуль DBI успешно установлен.") + # if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + # d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + + +def run_install_packages(): + install_packages() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/network.py b/deb_dist/postinstall-1.5.0/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/shell.py b/deb_dist/postinstall-1.5.0/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/ssh.py b/deb_dist/postinstall-1.5.0/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/deb_dist/postinstall-1.5.0/postinstall/option/update.py b/deb_dist/postinstall-1.5.0/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/deb_dist/postinstall-1.5.0/postinstall/root.py b/deb_dist/postinstall-1.5.0/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/deb_dist/postinstall-1.5.0/pyproject.toml b/deb_dist/postinstall-1.5.0/pyproject.toml new file mode 100644 index 0000000..6dc49c9 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "postinstall" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +pythondialog = "^3.5.3" + + + +[tool.poetry.scripts] +postinstall = "postinstall:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/deb_dist/postinstall-1.5.0/setup.cfg b/deb_dist/postinstall-1.5.0/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/deb_dist/postinstall-1.5.0/setup.py b/deb_dist/postinstall-1.5.0/setup.py new file mode 100644 index 0000000..634b525 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/setup.py @@ -0,0 +1,45 @@ +import os +import shutil +from setuptools import setup, find_packages +import glob + +deb_dist_dir = "deb_dist" +if os.path.exists(deb_dist_dir): + old_versions = glob.glob(os.path.join(deb_dist_dir, "postinstall-0.3.*")) + for old_version in old_versions: + if os.path.isdir(old_version): + shutil.rmtree(old_version) + else: + os.remove(old_version) + +setup( + name="postinstall", + version="1.5.0", + description="A Dialog TUI application for postinstall system configuration", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="LLC Technologies and Communication", + author_email="devteam@texnico.ru", + packages=find_packages(), + include_package_data=True, + package_data={ + # 'postinstall': ['styles.tcss'], + }, + install_requires=["pythondialog>=3.5.3,<4.0.0"], + entry_points={ + "console_scripts": [ + "postinstall=postinstall:main", + ], + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent :: Linux", + ], + python_requires=">=3.11", + data_files=[ + ("/usr/share/postinstall", []), + ("/etc/systemd/system", ["deb/postinstall.service"]), + ("/etc/profile.d", ["deb/0002-postinstall.sh"]), + ], +) diff --git a/deb_dist/postinstall-1.5.0/tests/__init__.py b/deb_dist/postinstall-1.5.0/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/deb_dist/postinstall-1.5.0/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/deb_dist/postinstall-1.5.0/tests/docker_config.py b/deb_dist/postinstall-1.5.0/tests/docker_config.py new file mode 100644 index 0000000..62ddf0a --- /dev/null +++ b/deb_dist/postinstall-1.5.0/tests/docker_config.py @@ -0,0 +1,132 @@ +import logging +import subprocess +import os +import re + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class DockerConfig: + def check_installed(self, command: str) -> bool: + try: + return ( + subprocess.run( + ["which", command], capture_output=True, text=True + ).returncode + == 0 + ) + except subprocess.SubprocessError as e: + logging.error(f"Error checking if {command} is installed: {e}") + return False + + def install_docker(self): + try: + if not self.check_installed("docker"): + logging.info("Установка Docker...") + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True) + logging.info("Docker успешно установился") + + if not self.check_installed("docker-compose"): + logging.info("Установка Docker Compose...") + subprocess.run( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + logging.info("Docker Compose успешно установился") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Docker: {e}") + raise + + def check_docker_login(self, registry_url): + try: + result = subprocess.run( + ["docker", "login", registry_url, "--password-stdin"], + input=b"\n", + capture_output=True, + text=True, + check=True, + ) + return "login succeeded" in result.stdout.lower() + except subprocess.CalledProcessError as e: + logging.error(f"Error checking Docker login: {e}") + return False + + def login_to_registry(self): + try: + if not self.check_docker_login(REGISTRY_URL): + logging.info("Вход в Docker registry...") + subprocess.run( + ["docker", "login", REGISTRY_URL], + input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(), + check=True, + ) + logging.info("Успешный вход в Docker registry") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при входе в Docker registry: {e}") + raise + + def install_portainer(self, port, login, password, ip): + try: + if not self.check_docker_login(REGISTRY_URL): + self.login_to_registry() + + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + docker_compose_content = f""" +version: '3' + +services: + portainer: + image: dr.gosrv.ru/portainer + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer_data:/data + - ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password + ports: + - "{ip}:{port}:9000" + environment: + - ADMIN_USER={login} + - ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password + +volumes: + portainer_data: + """ + + with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + subprocess.run( + ["htpasswd", "-nb", login, password], + stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"), + check=True, + ) + + subprocess.run( + [ + "docker-compose", + "-f", + DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", + "up", + "-d", + ], + check=True, + ) + logging.info("Portainer успешно установлен") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Portainer: {e}") + raise + except IOError as e: + logging.error(f"Ошибка при создании файла: {e}") + raise + + def validate_input(self, value, pattern, error_message): + if not re.match(pattern, value): + return False + return True diff --git a/deb_dist/postinstall-1.5.0/tests/test_docker.py b/deb_dist/postinstall-1.5.0/tests/test_docker.py new file mode 100644 index 0000000..b5caf04 --- /dev/null +++ b/deb_dist/postinstall-1.5.0/tests/test_docker.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH + +class TestDockerConfig(unittest.TestCase): + def setUp(self): + self.docker_config = DockerConfig() + + @patch("subprocess.run") + def test_check_installed_true(self, mock_run): + mock_run.return_value.returncode = 0 + self.assertTrue(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_check_installed_false(self, mock_run): + mock_run.return_value.returncode = 1 + self.assertFalse(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_install_docker(self, mock_run): + self.docker_config.install_docker() + mock_run.assert_any_call(["apt-get", "update"], check=True) + mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True) + mock_run.assert_any_call( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + + @patch("subprocess.run") + def test_check_docker_login_success(self, mock_run): + mock_run.return_value.stdout = "Login Succeeded" + self.assertTrue(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + def test_check_docker_login_failure(self, mock_run): + mock_run.return_value.stdout = "Login Failed" + self.assertFalse(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + def test_login_to_registry(self, mock_check_login, mock_run): + mock_check_login.return_value = False + self.docker_config.login_to_registry() + mock_run.assert_called_once() + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + @patch("docker_config.DockerConfig.login_to_registry") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run): + mock_check_login.return_value = True + self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1") + mock_open.assert_called() + mock_run.assert_called() + + def test_validate_input_success(self): + self.assertTrue( + self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + def test_validate_input_failure(self): + self.assertFalse( + self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/deb_dist/postinstall_1.0.1-1.debian.tar.xz b/deb_dist/postinstall_1.0.1-1.debian.tar.xz new file mode 100644 index 0000000..873699a Binary files /dev/null and b/deb_dist/postinstall_1.0.1-1.debian.tar.xz differ diff --git a/deb_dist/postinstall_1.0.1-1.dsc b/deb_dist/postinstall_1.0.1-1.dsc new file mode 100644 index 0000000..31a415f --- /dev/null +++ b/deb_dist/postinstall_1.0.1-1.dsc @@ -0,0 +1,20 @@ +Format: 3.0 (quilt) +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.0.1-1 +Maintainer: LLC Technologies and Communication +Homepage: None +Standards-Version: 3.9.1 +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9) +Package-List: + python3-postinstall deb python optional arch=all +Checksums-Sha1: + b5530b5836f38903278d51436bbbbd7245af97e3 641057 postinstall_1.0.1.orig.tar.gz + 81cfbc0cee0e6e97d059c992756e2415c76833ca 1884 postinstall_1.0.1-1.debian.tar.xz +Checksums-Sha256: + 9f6a9e22a1ec990c468172a8b84c3ef80ec846fad878fc0dadfdaa5f53a8557d 641057 postinstall_1.0.1.orig.tar.gz + f179e817b6da2616ed00d2a04b0d54077cd2eb53f7891b203ea854dafe528a6b 1884 postinstall_1.0.1-1.debian.tar.xz +Files: + 95a485d5268238e2fc5dce166cf96937 641057 postinstall_1.0.1.orig.tar.gz + 5c4db3dcbf376480afdb8fe2515f4ae4 1884 postinstall_1.0.1-1.debian.tar.xz diff --git a/deb_dist/postinstall_1.0.1-1_amd64.buildinfo b/deb_dist/postinstall_1.0.1-1_amd64.buildinfo new file mode 100644 index 0000000..7dd38a0 --- /dev/null +++ b/deb_dist/postinstall_1.0.1-1_amd64.buildinfo @@ -0,0 +1,209 @@ +Format: 1.0 +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.0.1-1 +Checksums-Md5: + 4d29ab553dc92f7e4f5148bf75dbc54d 21780 python3-postinstall_1.0.1-1_all.deb +Checksums-Sha1: + f50b092717196c205250ee50829aecaaab0855ad 21780 python3-postinstall_1.0.1-1_all.deb +Checksums-Sha256: + 36d415607c9dcaaa269316122dd227636940a4efe7679bd105151c7d9a6057d9 21780 python3-postinstall_1.0.1-1_all.deb +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Fri, 20 Sep 2024 01:42:45 +0500 +Build-Tainted-By: + merged-usr-via-aliased-dirs + usr-local-has-programs +Installed-Build-Depends: + autoconf (= 2.71-3), + automake (= 1:1.16.5-1.3), + autopoint (= 0.21-12), + autotools-dev (= 20220109.1), + base-files (= 12.4+deb12u7), + base-passwd (= 3.6.1), + bash (= 5.2.15-2+b7), + binutils (= 2.40-2), + binutils-common (= 2.40-2), + binutils-x86-64-linux-gnu (= 2.40-2), + bsdextrautils (= 2.38.1-5+deb12u1), + bsdutils (= 1:2.38.1-5+deb12u1), + build-essential (= 12.9), + bzip2 (= 1.0.8-5+b1), + clang-14 (= 1:14.0.6-12), + coreutils (= 9.1-1), + cpp (= 4:12.2.0-3), + cpp-12 (= 12.2.0-14), + dash (= 0.5.12-2), + debconf (= 1.5.82), + debhelper (= 13.11.4), + debianutils (= 5.7-0.5~deb12u1), + dh-autoreconf (= 20), + dh-strip-nondeterminism (= 1.13.1-1), + diffutils (= 1:3.8-4), + dpkg (= 1.21.22), + dpkg-dev (= 1.21.22), + dwz (= 0.15-1), + file (= 1:5.44-3), + findutils (= 4.9.0-4), + g++ (= 4:12.2.0-3), + g++-12 (= 12.2.0-14), + gawk (= 1:5.2.1-2), + gcc (= 4:12.2.0-3), + gcc-12 (= 12.2.0-14), + gcc-12-base (= 12.2.0-14), + gettext (= 0.21-12), + gettext-base (= 0.21-12), + grep (= 3.8-5), + groff-base (= 1.22.4-10), + gzip (= 1.12-1), + hostname (= 3.23+nmu1), + init-system-helpers (= 1.65.2), + install-info (= 6.8-6+b1), + intltool-debian (= 0.35.0+20060710.6), + libacl1 (= 2.3.1-3), + libarchive-zip-perl (= 1.68-1), + libasan8 (= 12.2.0-14), + libatomic1 (= 12.2.0-14), + libattr1 (= 1:2.5.1-4), + libaudit-common (= 1:3.0.9-1), + libaudit1 (= 1:3.0.9-1), + libbinutils (= 2.40-2), + libblkid1 (= 2.38.1-5+deb12u1), + libbsd0 (= 0.11.7-2), + libbz2-1.0 (= 1.0.8-5+b1), + libc-bin (= 2.36-9+deb12u8), + libc-dev-bin (= 2.36-9+deb12u8), + libc6 (= 2.36-9+deb12u8), + libc6-dev (= 2.36-9+deb12u8), + libcap-ng0 (= 0.8.3-1+b3), + libcap2 (= 1:2.66-4), + libcc1-0 (= 12.2.0-14), + libclang-common-14-dev (= 1:14.0.6-12), + libclang-cpp14 (= 1:14.0.6-12), + libclang1-14 (= 1:14.0.6-12), + libcom-err2 (= 1.47.0-2), + libcrypt-dev (= 1:4.4.33-2), + libcrypt1 (= 1:4.4.33-2), + libctf-nobfd0 (= 2.40-2), + libctf0 (= 2.40-2), + libdb5.3 (= 5.3.28+dfsg2-1), + libdebconfclient0 (= 0.270), + libdebhelper-perl (= 13.11.4), + libdpkg-perl (= 1.21.22), + libedit2 (= 3.1-20221030-2), + libelf1 (= 0.188-2.1), + libexpat1 (= 2.5.0-1+deb12u1), + libffi8 (= 3.4.4-1), + libfile-stripnondeterminism-perl (= 1.13.1-1), + libgc1 (= 1:8.2.2-3), + libgcc-12-dev (= 12.2.0-14), + libgcc-s1 (= 12.2.0-14), + libgcrypt20 (= 1.10.1-3), + libgdbm-compat4 (= 1.23-3), + libgdbm6 (= 1.23-3), + libgmp10 (= 2:6.2.1+dfsg1-1.1), + libgomp1 (= 12.2.0-14), + libgpg-error0 (= 1.46-1), + libgprofng0 (= 2.40-2), + libgssapi-krb5-2 (= 1.20.1-2+deb12u2), + libicu72 (= 72.1-3), + libisl23 (= 0.25-1.1), + libitm1 (= 12.2.0-14), + libjansson4 (= 2.14-2), + libk5crypto3 (= 1.20.1-2+deb12u2), + libkeyutils1 (= 1.6.3-2), + libkrb5-3 (= 1.20.1-2+deb12u2), + libkrb5support0 (= 1.20.1-2+deb12u2), + libllvm14 (= 1:14.0.6-12), + liblsan0 (= 12.2.0-14), + liblz4-1 (= 1.9.4-1), + liblzma5 (= 5.4.1-0.2), + libmagic-mgc (= 1:5.44-3), + libmagic1 (= 1:5.44-3), + libmd0 (= 1.0.4-2), + libmount1 (= 2.38.1-5+deb12u1), + libmpc3 (= 1.3.1-1), + libmpfr6 (= 4.2.0-1), + libncursesw6 (= 6.4-4), + libnsl-dev (= 1.3.0-2), + libnsl2 (= 1.3.0-2), + libobjc-12-dev (= 12.2.0-14), + libobjc4 (= 12.2.0-14), + libpam-modules (= 1.5.2-6+deb12u1), + libpam-modules-bin (= 1.5.2-6+deb12u1), + libpam-runtime (= 1.5.2-6+deb12u1), + libpam0g (= 1.5.2-6+deb12u1), + libpcre2-8-0 (= 10.42-1), + libperl5.36 (= 5.36.0-7+deb12u1), + libpipeline1 (= 1.5.7-1), + libpython3-stdlib (= 3.11.2-1+b1), + libpython3.11-minimal (= 3.11.2-6+deb12u3), + libpython3.11-stdlib (= 3.11.2-6+deb12u3), + libquadmath0 (= 12.2.0-14), + libreadline8 (= 8.2-1.3), + libseccomp2 (= 2.5.4-1+deb12u1), + libselinux1 (= 3.4-1+b6), + libsigsegv2 (= 2.14-1), + libsmartcols1 (= 2.38.1-5+deb12u1), + libsqlite3-0 (= 3.40.1-2), + libssl3 (= 3.0.14-1~deb12u2), + libstdc++-12-dev (= 12.2.0-14), + libstdc++6 (= 12.2.0-14), + libsub-override-perl (= 0.09-4), + libsystemd0 (= 252.30-1~deb12u2), + libtinfo6 (= 6.4-4), + libtirpc-common (= 1.3.3+ds-1), + libtirpc-dev (= 1.3.3+ds-1), + libtirpc3 (= 1.3.3+ds-1), + libtool (= 2.4.7-7~deb12u1), + libtsan2 (= 12.2.0-14), + libubsan1 (= 12.2.0-14), + libuchardet0 (= 0.0.7-1), + libudev1 (= 252.30-1~deb12u2), + libunistring2 (= 1.0-2), + libuuid1 (= 2.38.1-5+deb12u1), + libxml2 (= 2.9.14+dfsg-1.3~deb12u1), + libz3-4 (= 4.8.12-3.1), + libzstd1 (= 1.5.4+dfsg2-5), + linux-libc-dev (= 6.1.106-3), + llvm-14-linker-tools (= 1:14.0.6-12), + login (= 1:4.13+dfsg1-1+b1), + m4 (= 1.4.19-3), + mailcap (= 3.70+nmu1), + make (= 4.3-4.1), + man-db (= 2.11.2-2), + mawk (= 1.3.4.20200120-3.1), + media-types (= 10.0.0), + mime-support (= 3.66), + ncurses-base (= 6.4-4), + ncurses-bin (= 6.4-4), + patch (= 2.7.6-7), + perl (= 5.36.0-7+deb12u1), + perl-base (= 5.36.0-7+deb12u1), + perl-modules-5.36 (= 5.36.0-7+deb12u1), + po-debconf (= 1.0.21+nmu1), + python3 (= 3.11.2-1+b1), + python3-all (= 3.11.2-1+b1), + python3-distutils (= 3.11.2-3), + python3-lib2to3 (= 3.11.2-3), + python3-minimal (= 3.11.2-1+b1), + python3-pkg-resources (= 66.1.1-1), + python3-setuptools (= 66.1.1-1), + python3.11 (= 3.11.2-6+deb12u3), + python3.11-minimal (= 3.11.2-6+deb12u3), + readline-common (= 8.2-1.3), + rpcsvc-proto (= 1.4.3-1), + sed (= 4.9-1), + sensible-utils (= 0.0.17+nmu1), + sysvinit-utils (= 3.06-4), + tar (= 1.34+dfsg-1.2+deb12u1), + usr-is-merged (= 37~deb12u1), + util-linux (= 2.38.1-5+deb12u1), + util-linux-extra (= 2.38.1-5+deb12u1), + xz-utils (= 5.4.1-0.2), + zlib1g (= 1:1.2.13.dfsg-1) +Environment: + DEB_BUILD_OPTIONS="parallel=4" + LANG="ru_RU.UTF-8" + SOURCE_DATE_EPOCH="1726778562" diff --git a/deb_dist/postinstall_1.0.1-1_amd64.changes b/deb_dist/postinstall_1.0.1-1_amd64.changes new file mode 100644 index 0000000..f112b0c --- /dev/null +++ b/deb_dist/postinstall_1.0.1-1_amd64.changes @@ -0,0 +1,25 @@ +Format: 1.8 +Date: Fri, 20 Sep 2024 01:42:42 +0500 +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.0.1-1 +Distribution: stable +Urgency: low +Maintainer: LLC Technologies and Communication +Changed-By: LLC Technologies and Communication +Description: + python3-postinstall - A Dialog TUI application for postinstall system configuratio +Changes: + postinstall (1.0.1-1) stable; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + 97f2019a1b607d231cafd5c840c543a86d910d08 5987 postinstall_1.0.1-1_amd64.buildinfo + f50b092717196c205250ee50829aecaaab0855ad 21780 python3-postinstall_1.0.1-1_all.deb +Checksums-Sha256: + e8da3a908aeb4640672c4816ec1e601167e2ed97f9a8a0f6e3974ff9d022f74c 5987 postinstall_1.0.1-1_amd64.buildinfo + 36d415607c9dcaaa269316122dd227636940a4efe7679bd105151c7d9a6057d9 21780 python3-postinstall_1.0.1-1_all.deb +Files: + 30fca05fd77b8fe8f6df6fb37dc60026 5987 python optional postinstall_1.0.1-1_amd64.buildinfo + 4d29ab553dc92f7e4f5148bf75dbc54d 21780 python optional python3-postinstall_1.0.1-1_all.deb diff --git a/deb_dist/postinstall_1.0.1-1_source.buildinfo b/deb_dist/postinstall_1.0.1-1_source.buildinfo new file mode 100644 index 0000000..fc1b32f --- /dev/null +++ b/deb_dist/postinstall_1.0.1-1_source.buildinfo @@ -0,0 +1,208 @@ +Format: 1.0 +Source: postinstall +Architecture: source +Version: 1.0.1-1 +Checksums-Md5: + 8fe491e941fe5c73fc0476571bbc062e 893 postinstall_1.0.1-1.dsc +Checksums-Sha1: + 386c431a749a68a52ca2b11223e1c5e05fc263a0 893 postinstall_1.0.1-1.dsc +Checksums-Sha256: + 46e93afa885acee292a277a99e5538b2a2d169e18fe4ea9825a34a1ef058ed29 893 postinstall_1.0.1-1.dsc +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Fri, 20 Sep 2024 01:42:42 +0500 +Build-Tainted-By: + merged-usr-via-aliased-dirs + usr-local-has-programs +Installed-Build-Depends: + autoconf (= 2.71-3), + automake (= 1:1.16.5-1.3), + autopoint (= 0.21-12), + autotools-dev (= 20220109.1), + base-files (= 12.4+deb12u7), + base-passwd (= 3.6.1), + bash (= 5.2.15-2+b7), + binutils (= 2.40-2), + binutils-common (= 2.40-2), + binutils-x86-64-linux-gnu (= 2.40-2), + bsdextrautils (= 2.38.1-5+deb12u1), + bsdutils (= 1:2.38.1-5+deb12u1), + build-essential (= 12.9), + bzip2 (= 1.0.8-5+b1), + clang-14 (= 1:14.0.6-12), + coreutils (= 9.1-1), + cpp (= 4:12.2.0-3), + cpp-12 (= 12.2.0-14), + dash (= 0.5.12-2), + debconf (= 1.5.82), + debhelper (= 13.11.4), + debianutils (= 5.7-0.5~deb12u1), + dh-autoreconf (= 20), + dh-strip-nondeterminism (= 1.13.1-1), + diffutils (= 1:3.8-4), + dpkg (= 1.21.22), + dpkg-dev (= 1.21.22), + dwz (= 0.15-1), + file (= 1:5.44-3), + findutils (= 4.9.0-4), + g++ (= 4:12.2.0-3), + g++-12 (= 12.2.0-14), + gawk (= 1:5.2.1-2), + gcc (= 4:12.2.0-3), + gcc-12 (= 12.2.0-14), + gcc-12-base (= 12.2.0-14), + gettext (= 0.21-12), + gettext-base (= 0.21-12), + grep (= 3.8-5), + groff-base (= 1.22.4-10), + gzip (= 1.12-1), + hostname (= 3.23+nmu1), + init-system-helpers (= 1.65.2), + install-info (= 6.8-6+b1), + intltool-debian (= 0.35.0+20060710.6), + libacl1 (= 2.3.1-3), + libarchive-zip-perl (= 1.68-1), + libasan8 (= 12.2.0-14), + libatomic1 (= 12.2.0-14), + libattr1 (= 1:2.5.1-4), + libaudit-common (= 1:3.0.9-1), + libaudit1 (= 1:3.0.9-1), + libbinutils (= 2.40-2), + libblkid1 (= 2.38.1-5+deb12u1), + libbsd0 (= 0.11.7-2), + libbz2-1.0 (= 1.0.8-5+b1), + libc-bin (= 2.36-9+deb12u8), + libc-dev-bin (= 2.36-9+deb12u8), + libc6 (= 2.36-9+deb12u8), + libc6-dev (= 2.36-9+deb12u8), + libcap-ng0 (= 0.8.3-1+b3), + libcap2 (= 1:2.66-4), + libcc1-0 (= 12.2.0-14), + libclang-common-14-dev (= 1:14.0.6-12), + libclang-cpp14 (= 1:14.0.6-12), + libclang1-14 (= 1:14.0.6-12), + libcom-err2 (= 1.47.0-2), + libcrypt-dev (= 1:4.4.33-2), + libcrypt1 (= 1:4.4.33-2), + libctf-nobfd0 (= 2.40-2), + libctf0 (= 2.40-2), + libdb5.3 (= 5.3.28+dfsg2-1), + libdebconfclient0 (= 0.270), + libdebhelper-perl (= 13.11.4), + libdpkg-perl (= 1.21.22), + libedit2 (= 3.1-20221030-2), + libelf1 (= 0.188-2.1), + libexpat1 (= 2.5.0-1+deb12u1), + libffi8 (= 3.4.4-1), + libfile-stripnondeterminism-perl (= 1.13.1-1), + libgc1 (= 1:8.2.2-3), + libgcc-12-dev (= 12.2.0-14), + libgcc-s1 (= 12.2.0-14), + libgcrypt20 (= 1.10.1-3), + libgdbm-compat4 (= 1.23-3), + libgdbm6 (= 1.23-3), + libgmp10 (= 2:6.2.1+dfsg1-1.1), + libgomp1 (= 12.2.0-14), + libgpg-error0 (= 1.46-1), + libgprofng0 (= 2.40-2), + libgssapi-krb5-2 (= 1.20.1-2+deb12u2), + libicu72 (= 72.1-3), + libisl23 (= 0.25-1.1), + libitm1 (= 12.2.0-14), + libjansson4 (= 2.14-2), + libk5crypto3 (= 1.20.1-2+deb12u2), + libkeyutils1 (= 1.6.3-2), + libkrb5-3 (= 1.20.1-2+deb12u2), + libkrb5support0 (= 1.20.1-2+deb12u2), + libllvm14 (= 1:14.0.6-12), + liblsan0 (= 12.2.0-14), + liblz4-1 (= 1.9.4-1), + liblzma5 (= 5.4.1-0.2), + libmagic-mgc (= 1:5.44-3), + libmagic1 (= 1:5.44-3), + libmd0 (= 1.0.4-2), + libmount1 (= 2.38.1-5+deb12u1), + libmpc3 (= 1.3.1-1), + libmpfr6 (= 4.2.0-1), + libncursesw6 (= 6.4-4), + libnsl-dev (= 1.3.0-2), + libnsl2 (= 1.3.0-2), + libobjc-12-dev (= 12.2.0-14), + libobjc4 (= 12.2.0-14), + libpam-modules (= 1.5.2-6+deb12u1), + libpam-modules-bin (= 1.5.2-6+deb12u1), + libpam-runtime (= 1.5.2-6+deb12u1), + libpam0g (= 1.5.2-6+deb12u1), + libpcre2-8-0 (= 10.42-1), + libperl5.36 (= 5.36.0-7+deb12u1), + libpipeline1 (= 1.5.7-1), + libpython3-stdlib (= 3.11.2-1+b1), + libpython3.11-minimal (= 3.11.2-6+deb12u3), + libpython3.11-stdlib (= 3.11.2-6+deb12u3), + libquadmath0 (= 12.2.0-14), + libreadline8 (= 8.2-1.3), + libseccomp2 (= 2.5.4-1+deb12u1), + libselinux1 (= 3.4-1+b6), + libsigsegv2 (= 2.14-1), + libsmartcols1 (= 2.38.1-5+deb12u1), + libsqlite3-0 (= 3.40.1-2), + libssl3 (= 3.0.14-1~deb12u2), + libstdc++-12-dev (= 12.2.0-14), + libstdc++6 (= 12.2.0-14), + libsub-override-perl (= 0.09-4), + libsystemd0 (= 252.30-1~deb12u2), + libtinfo6 (= 6.4-4), + libtirpc-common (= 1.3.3+ds-1), + libtirpc-dev (= 1.3.3+ds-1), + libtirpc3 (= 1.3.3+ds-1), + libtool (= 2.4.7-7~deb12u1), + libtsan2 (= 12.2.0-14), + libubsan1 (= 12.2.0-14), + libuchardet0 (= 0.0.7-1), + libudev1 (= 252.30-1~deb12u2), + libunistring2 (= 1.0-2), + libuuid1 (= 2.38.1-5+deb12u1), + libxml2 (= 2.9.14+dfsg-1.3~deb12u1), + libz3-4 (= 4.8.12-3.1), + libzstd1 (= 1.5.4+dfsg2-5), + linux-libc-dev (= 6.1.106-3), + llvm-14-linker-tools (= 1:14.0.6-12), + login (= 1:4.13+dfsg1-1+b1), + m4 (= 1.4.19-3), + mailcap (= 3.70+nmu1), + make (= 4.3-4.1), + man-db (= 2.11.2-2), + mawk (= 1.3.4.20200120-3.1), + media-types (= 10.0.0), + mime-support (= 3.66), + ncurses-base (= 6.4-4), + ncurses-bin (= 6.4-4), + patch (= 2.7.6-7), + perl (= 5.36.0-7+deb12u1), + perl-base (= 5.36.0-7+deb12u1), + perl-modules-5.36 (= 5.36.0-7+deb12u1), + po-debconf (= 1.0.21+nmu1), + python3 (= 3.11.2-1+b1), + python3-all (= 3.11.2-1+b1), + python3-distutils (= 3.11.2-3), + python3-lib2to3 (= 3.11.2-3), + python3-minimal (= 3.11.2-1+b1), + python3-pkg-resources (= 66.1.1-1), + python3-setuptools (= 66.1.1-1), + python3.11 (= 3.11.2-6+deb12u3), + python3.11-minimal (= 3.11.2-6+deb12u3), + readline-common (= 8.2-1.3), + rpcsvc-proto (= 1.4.3-1), + sed (= 4.9-1), + sensible-utils (= 0.0.17+nmu1), + sysvinit-utils (= 3.06-4), + tar (= 1.34+dfsg-1.2+deb12u1), + usr-is-merged (= 37~deb12u1), + util-linux (= 2.38.1-5+deb12u1), + util-linux-extra (= 2.38.1-5+deb12u1), + xz-utils (= 5.4.1-0.2), + zlib1g (= 1:1.2.13.dfsg-1) +Environment: + DEB_BUILD_OPTIONS="parallel=4" + LANG="ru_RU.UTF-8" + SOURCE_DATE_EPOCH="1726778562" diff --git a/deb_dist/postinstall_1.0.1-1_source.changes b/deb_dist/postinstall_1.0.1-1_source.changes new file mode 100644 index 0000000..e0e947b --- /dev/null +++ b/deb_dist/postinstall_1.0.1-1_source.changes @@ -0,0 +1,28 @@ +Format: 1.8 +Date: Fri, 20 Sep 2024 01:42:42 +0500 +Source: postinstall +Architecture: source +Version: 1.0.1-1 +Distribution: stable +Urgency: low +Maintainer: LLC Technologies and Communication +Changed-By: LLC Technologies and Communication +Changes: + postinstall (1.0.1-1) stable; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + 386c431a749a68a52ca2b11223e1c5e05fc263a0 893 postinstall_1.0.1-1.dsc + b5530b5836f38903278d51436bbbbd7245af97e3 641057 postinstall_1.0.1.orig.tar.gz + 81cfbc0cee0e6e97d059c992756e2415c76833ca 1884 postinstall_1.0.1-1.debian.tar.xz + ccb514d4a6a78091d786e051796fc996239d24f7 5920 postinstall_1.0.1-1_source.buildinfo +Checksums-Sha256: + 46e93afa885acee292a277a99e5538b2a2d169e18fe4ea9825a34a1ef058ed29 893 postinstall_1.0.1-1.dsc + 9f6a9e22a1ec990c468172a8b84c3ef80ec846fad878fc0dadfdaa5f53a8557d 641057 postinstall_1.0.1.orig.tar.gz + f179e817b6da2616ed00d2a04b0d54077cd2eb53f7891b203ea854dafe528a6b 1884 postinstall_1.0.1-1.debian.tar.xz + 81958e03870a4adbff8bb65ce8e7e8652d5e67d6a12abf84ac412e96fcc6a346 5920 postinstall_1.0.1-1_source.buildinfo +Files: + 8fe491e941fe5c73fc0476571bbc062e 893 python optional postinstall_1.0.1-1.dsc + 95a485d5268238e2fc5dce166cf96937 641057 python optional postinstall_1.0.1.orig.tar.gz + 5c4db3dcbf376480afdb8fe2515f4ae4 1884 python optional postinstall_1.0.1-1.debian.tar.xz + a4e56151bb0744a09dbb26304eeeb598 5920 python optional postinstall_1.0.1-1_source.buildinfo diff --git a/deb_dist/postinstall_1.0.1.orig.tar.gz b/deb_dist/postinstall_1.0.1.orig.tar.gz new file mode 100644 index 0000000..1b3cbbe Binary files /dev/null and b/deb_dist/postinstall_1.0.1.orig.tar.gz differ diff --git a/deb_dist/postinstall_1.5.0-1.debian.tar.xz b/deb_dist/postinstall_1.5.0-1.debian.tar.xz new file mode 100644 index 0000000..45110fe Binary files /dev/null and b/deb_dist/postinstall_1.5.0-1.debian.tar.xz differ diff --git a/deb_dist/postinstall_1.5.0-1.dsc b/deb_dist/postinstall_1.5.0-1.dsc new file mode 100644 index 0000000..0ddbd31 --- /dev/null +++ b/deb_dist/postinstall_1.5.0-1.dsc @@ -0,0 +1,20 @@ +Format: 3.0 (quilt) +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.5.0-1 +Maintainer: LLC Technologies and Communication +Homepage: None +Standards-Version: 3.9.1 +Build-Depends: python3-setuptools, python3-all, debhelper (>= 9) +Package-List: + python3-postinstall deb python optional arch=all +Checksums-Sha1: + ac329e39abab33ddf9964dfcb5ad03aab59f0ab0 642170 postinstall_1.5.0.orig.tar.gz + caf27637149d91b9f87a356f21e014eb6017427d 1888 postinstall_1.5.0-1.debian.tar.xz +Checksums-Sha256: + 06b3ddd18f5d21a9318e41696285bddeb246c82cb44f4413f64412bd2f59d0a1 642170 postinstall_1.5.0.orig.tar.gz + 510c8eb025289583a1849af810279915f6924535a565f2989b5fe8c6fcc6051e 1888 postinstall_1.5.0-1.debian.tar.xz +Files: + 522aede680a8d15bbfd49ffd6efa2dbb 642170 postinstall_1.5.0.orig.tar.gz + 68eeb9e6d47fb6b1bd638ab5fded4d29 1888 postinstall_1.5.0-1.debian.tar.xz diff --git a/deb_dist/postinstall_1.5.0-1_amd64.buildinfo b/deb_dist/postinstall_1.5.0-1_amd64.buildinfo new file mode 100644 index 0000000..a637203 --- /dev/null +++ b/deb_dist/postinstall_1.5.0-1_amd64.buildinfo @@ -0,0 +1,224 @@ +Format: 1.0 +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.5.0-1 +Checksums-Md5: + c5a5f53d837a705a43ffc760cf8ea748 22508 python3-postinstall_1.5.0-1_all.deb +Checksums-Sha1: + f30ec4a957ae3e303eab4fb952fb5c4bf136c0e2 22508 python3-postinstall_1.5.0-1_all.deb +Checksums-Sha256: + 756e032653ef19c4947cf1ef82e8e8afbf1b881c2001a34a299f464ee0704868 22508 python3-postinstall_1.5.0-1_all.deb +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Wed, 27 Nov 2024 15:29:26 +0500 +Build-Tainted-By: + merged-usr-via-aliased-dirs + usr-local-has-configs + usr-local-has-libraries + usr-local-has-programs +Installed-Build-Depends: + autoconf (= 2.71-3), + automake (= 1:1.16.5-1.3), + autopoint (= 0.21-12), + autotools-dev (= 20220109.1), + base-files (= 12.4+deb12u8), + base-passwd (= 3.6.1), + bash (= 5.2.15-2+b7), + binutils (= 2.40-2), + binutils-common (= 2.40-2), + binutils-x86-64-linux-gnu (= 2.40-2), + bsdextrautils (= 2.38.1-5+deb12u2), + bsdutils (= 1:2.38.1-5+deb12u2), + build-essential (= 12.9), + bzip2 (= 1.0.8-5+b1), + clang-14 (= 1:14.0.6-12), + coreutils (= 9.1-1), + cpp (= 4:12.2.0-3), + cpp-11 (= 11.3.0-12), + cpp-12 (= 12.2.0-14), + dash (= 0.5.12-2), + debconf (= 1.5.82), + debhelper (= 13.11.4), + debianutils (= 5.7-0.5~deb12u1), + dh-autoreconf (= 20), + dh-strip-nondeterminism (= 1.13.1-1), + diffutils (= 1:3.8-4), + dpkg (= 1.21.22), + dpkg-dev (= 1.21.22), + dwz (= 0.15-1), + file (= 1:5.44-3), + findutils (= 4.9.0-4), + g++ (= 4:12.2.0-3), + g++-12 (= 12.2.0-14), + gawk (= 1:5.2.1-2), + gcc (= 4:12.2.0-3), + gcc-11 (= 11.3.0-12), + gcc-11-base (= 11.3.0-12), + gcc-12 (= 12.2.0-14), + gcc-12-base (= 12.2.0-14), + gettext (= 0.21-12), + gettext-base (= 0.21-12), + grep (= 3.8-5), + groff-base (= 1.22.4-10), + gzip (= 1.12-1), + hostname (= 3.23+nmu1), + init-system-helpers (= 1.65.2), + install-info (= 6.8-6+b1), + intltool-debian (= 0.35.0+20060710.6), + libacl1 (= 2.3.1-3), + libarchive-zip-perl (= 1.68-1), + libasan6 (= 11.3.0-12), + libasan8 (= 12.2.0-14), + libatomic1 (= 12.2.0-14), + libattr1 (= 1:2.5.1-4), + libaudit-common (= 1:3.0.9-1), + libaudit1 (= 1:3.0.9-1), + libbinutils (= 2.40-2), + libblkid1 (= 2.38.1-5+deb12u2), + libbsd0 (= 0.11.7-2), + libbz2-1.0 (= 1.0.8-5+b1), + libc-bin (= 2.36-9+deb12u9), + libc-dev-bin (= 2.36-9+deb12u9), + libc6 (= 2.36-9+deb12u9), + libc6-dev (= 2.36-9+deb12u9), + libcap-ng0 (= 0.8.3-1+b3), + libcap2 (= 1:2.66-4), + libcc1-0 (= 12.2.0-14), + libclang-common-14-dev (= 1:14.0.6-12), + libclang-cpp14 (= 1:14.0.6-12), + libclang1-14 (= 1:14.0.6-12), + libcom-err2 (= 1.47.0-2), + libcrypt-dev (= 1:4.4.33-2), + libcrypt1 (= 1:4.4.33-2), + libctf-nobfd0 (= 2.40-2), + libctf0 (= 2.40-2), + libdb5.3 (= 5.3.28+dfsg2-1), + libdebconfclient0 (= 0.270), + libdebhelper-perl (= 13.11.4), + libdpkg-perl (= 1.21.22), + libedit2 (= 3.1-20221030-2), + libelf1 (= 0.188-2.1), + libexpat1 (= 2.5.0-1+deb12u1), + libffi8 (= 3.4.4-1), + libfile-stripnondeterminism-perl (= 1.13.1-1), + libgc1 (= 1:8.2.2-3), + libgcc-11-dev (= 11.3.0-12), + libgcc-12-dev (= 12.2.0-14), + libgcc-s1 (= 12.2.0-14), + libgcrypt20 (= 1.10.1-3), + libgdbm-compat4 (= 1.23-3), + libgdbm6 (= 1.23-3), + libgmp10 (= 2:6.2.1+dfsg1-1.1), + libgomp1 (= 12.2.0-14), + libgpg-error0 (= 1.46-1), + libgprofng0 (= 2.40-2), + libgssapi-krb5-2 (= 1.20.1-2+deb12u2), + libicu72 (= 72.1-3), + libisl23 (= 0.25-1.1), + libitm1 (= 12.2.0-14), + libjansson4 (= 2.14-2), + libk5crypto3 (= 1.20.1-2+deb12u2), + libkeyutils1 (= 1.6.3-2), + libkrb5-3 (= 1.20.1-2+deb12u2), + libkrb5support0 (= 1.20.1-2+deb12u2), + libllvm14 (= 1:14.0.6-12), + liblsan0 (= 12.2.0-14), + liblz4-1 (= 1.9.4-1), + liblzma5 (= 5.4.1-0.2), + libmagic-mgc (= 1:5.44-3), + libmagic1 (= 1:5.44-3), + libmd0 (= 1.0.4-2), + libmount1 (= 2.38.1-5+deb12u2), + libmpc3 (= 1.3.1-1), + libmpfr6 (= 4.2.0-1), + libncursesw6 (= 6.4-4), + libnsl-dev (= 1.3.0-2), + libnsl2 (= 1.3.0-2), + libobjc-12-dev (= 12.2.0-14), + libobjc4 (= 12.2.0-14), + libpam-modules (= 1.5.2-6+deb12u1), + libpam-modules-bin (= 1.5.2-6+deb12u1), + libpam-runtime (= 1.5.2-6+deb12u1), + libpam0g (= 1.5.2-6+deb12u1), + libpcre2-8-0 (= 10.42-1), + libperl5.36 (= 5.36.0-7+deb12u1), + libpipeline1 (= 1.5.7-1), + libpython3-stdlib (= 3.11.2-1+b1), + libpython3.11-minimal (= 3.11.2-6+deb12u4), + libpython3.11-stdlib (= 3.11.2-6+deb12u4), + libquadmath0 (= 12.2.0-14), + libreadline8 (= 8.2-1.3), + libseccomp2 (= 2.5.4-1+deb12u1), + libselinux1 (= 3.4-1+b6), + libsigsegv2 (= 2.14-1), + libsmartcols1 (= 2.38.1-5+deb12u2), + libsqlite3-0 (= 3.40.1-2+deb12u1), + libssl3 (= 3.0.15-1~deb12u1), + libstdc++-12-dev (= 12.2.0-14), + libstdc++6 (= 12.2.0-14), + libsub-override-perl (= 0.09-4), + libsystemd0 (= 252.31-1~deb12u1), + libtinfo6 (= 6.4-4), + libtirpc-common (= 1.3.3+ds-1), + libtirpc-dev (= 1.3.3+ds-1), + libtirpc3 (= 1.3.3+ds-1), + libtool (= 2.4.7-7~deb12u1), + libtsan0 (= 11.3.0-12), + libtsan2 (= 12.2.0-14), + libubsan1 (= 12.2.0-14), + libuchardet0 (= 0.0.7-1), + libudev1 (= 252.31-1~deb12u1), + libunistring2 (= 1.0-2), + libuuid1 (= 2.38.1-5+deb12u2), + libxml2 (= 2.9.14+dfsg-1.3~deb12u1), + libz3-4 (= 4.8.12-3.1), + libzstd1 (= 1.5.4+dfsg2-5), + linux-libc-dev (= 6.1.115-1), + llvm-14-linker-tools (= 1:14.0.6-12), + login (= 1:4.13+dfsg1-1+b1), + m4 (= 1.4.19-3), + make (= 4.3-4.1), + man-db (= 2.11.2-2), + mawk (= 1.3.4.20200120-3.1), + media-types (= 10.0.0), + ncurses-base (= 6.4-4), + ncurses-bin (= 6.4-4), + patch (= 2.7.6-7), + perl (= 5.36.0-7+deb12u1), + perl-base (= 5.36.0-7+deb12u1), + perl-modules-5.36 (= 5.36.0-7+deb12u1), + po-debconf (= 1.0.21+nmu1), + python3 (= 3.11.2-1+b1), + python3-all (= 3.11.2-1+b1), + python3-distutils (= 3.11.2-3), + python3-lib2to3 (= 3.11.2-3), + python3-minimal (= 3.11.2-1+b1), + python3-pkg-resources (= 66.1.1-1), + python3-setuptools (= 66.1.1-1), + python3.11 (= 3.11.2-6+deb12u4), + python3.11-minimal (= 3.11.2-6+deb12u4), + readline-common (= 8.2-1.3), + rpcsvc-proto (= 1.4.3-1), + sed (= 4.9-1), + sensible-utils (= 0.0.17+nmu1), + sysvinit-utils (= 3.06-4), + tar (= 1.34+dfsg-1.2+deb12u1), + usr-is-merged (= 37~deb12u1), + util-linux (= 2.38.1-5+deb12u2), + util-linux-extra (= 2.38.1-5+deb12u2), + xz-utils (= 5.4.1-0.2), + zlib1g (= 1:1.2.13.dfsg-1) +Environment: + DEB_BUILD_OPTIONS="parallel=4" + LANG="ru_RU.UTF-8" + LC_ADDRESS="ru_RU.UTF-8" + LC_IDENTIFICATION="ru_RU.UTF-8" + LC_MEASUREMENT="ru_RU.UTF-8" + LC_MONETARY="ru_RU.UTF-8" + LC_NAME="ru_RU.UTF-8" + LC_NUMERIC="ru_RU.UTF-8" + LC_PAPER="ru_RU.UTF-8" + LC_TELEPHONE="ru_RU.UTF-8" + LC_TIME="ru_RU.UTF-8" + SOURCE_DATE_EPOCH="1732703360" diff --git a/deb_dist/postinstall_1.5.0-1_amd64.changes b/deb_dist/postinstall_1.5.0-1_amd64.changes new file mode 100644 index 0000000..46e6c0a --- /dev/null +++ b/deb_dist/postinstall_1.5.0-1_amd64.changes @@ -0,0 +1,25 @@ +Format: 1.8 +Date: Wed, 27 Nov 2024 15:29:20 +0500 +Source: postinstall +Binary: python3-postinstall +Architecture: all +Version: 1.5.0-1 +Distribution: stable +Urgency: low +Maintainer: LLC Technologies and Communication +Changed-By: LLC Technologies and Communication +Description: + python3-postinstall - A Dialog TUI application for postinstall system configuratio +Changes: + postinstall (1.5.0-1) stable; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + a0136e16531fbf9ee97aac1a761d97f1601300ab 6389 postinstall_1.5.0-1_amd64.buildinfo + f30ec4a957ae3e303eab4fb952fb5c4bf136c0e2 22508 python3-postinstall_1.5.0-1_all.deb +Checksums-Sha256: + 2d0278c25ceb64d65e237022ca7b2beb4ac7354b865573f3a062d6abacf6bf9f 6389 postinstall_1.5.0-1_amd64.buildinfo + 756e032653ef19c4947cf1ef82e8e8afbf1b881c2001a34a299f464ee0704868 22508 python3-postinstall_1.5.0-1_all.deb +Files: + d877f3362587b58dcac81a4c53976b88 6389 python optional postinstall_1.5.0-1_amd64.buildinfo + c5a5f53d837a705a43ffc760cf8ea748 22508 python optional python3-postinstall_1.5.0-1_all.deb diff --git a/deb_dist/postinstall_1.5.0-1_source.buildinfo b/deb_dist/postinstall_1.5.0-1_source.buildinfo new file mode 100644 index 0000000..52bf7ae --- /dev/null +++ b/deb_dist/postinstall_1.5.0-1_source.buildinfo @@ -0,0 +1,223 @@ +Format: 1.0 +Source: postinstall +Architecture: source +Version: 1.5.0-1 +Checksums-Md5: + c80476d28e9327b87b3373f984400eb7 893 postinstall_1.5.0-1.dsc +Checksums-Sha1: + 1beaad3f713cfd536c16b5fcd4a9cddfa6529b3a 893 postinstall_1.5.0-1.dsc +Checksums-Sha256: + 68a17ae036b380bd834c60f12097d769aea5019bff7f868863e4474960b24b41 893 postinstall_1.5.0-1.dsc +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Wed, 27 Nov 2024 15:29:21 +0500 +Build-Tainted-By: + merged-usr-via-aliased-dirs + usr-local-has-configs + usr-local-has-libraries + usr-local-has-programs +Installed-Build-Depends: + autoconf (= 2.71-3), + automake (= 1:1.16.5-1.3), + autopoint (= 0.21-12), + autotools-dev (= 20220109.1), + base-files (= 12.4+deb12u8), + base-passwd (= 3.6.1), + bash (= 5.2.15-2+b7), + binutils (= 2.40-2), + binutils-common (= 2.40-2), + binutils-x86-64-linux-gnu (= 2.40-2), + bsdextrautils (= 2.38.1-5+deb12u2), + bsdutils (= 1:2.38.1-5+deb12u2), + build-essential (= 12.9), + bzip2 (= 1.0.8-5+b1), + clang-14 (= 1:14.0.6-12), + coreutils (= 9.1-1), + cpp (= 4:12.2.0-3), + cpp-11 (= 11.3.0-12), + cpp-12 (= 12.2.0-14), + dash (= 0.5.12-2), + debconf (= 1.5.82), + debhelper (= 13.11.4), + debianutils (= 5.7-0.5~deb12u1), + dh-autoreconf (= 20), + dh-strip-nondeterminism (= 1.13.1-1), + diffutils (= 1:3.8-4), + dpkg (= 1.21.22), + dpkg-dev (= 1.21.22), + dwz (= 0.15-1), + file (= 1:5.44-3), + findutils (= 4.9.0-4), + g++ (= 4:12.2.0-3), + g++-12 (= 12.2.0-14), + gawk (= 1:5.2.1-2), + gcc (= 4:12.2.0-3), + gcc-11 (= 11.3.0-12), + gcc-11-base (= 11.3.0-12), + gcc-12 (= 12.2.0-14), + gcc-12-base (= 12.2.0-14), + gettext (= 0.21-12), + gettext-base (= 0.21-12), + grep (= 3.8-5), + groff-base (= 1.22.4-10), + gzip (= 1.12-1), + hostname (= 3.23+nmu1), + init-system-helpers (= 1.65.2), + install-info (= 6.8-6+b1), + intltool-debian (= 0.35.0+20060710.6), + libacl1 (= 2.3.1-3), + libarchive-zip-perl (= 1.68-1), + libasan6 (= 11.3.0-12), + libasan8 (= 12.2.0-14), + libatomic1 (= 12.2.0-14), + libattr1 (= 1:2.5.1-4), + libaudit-common (= 1:3.0.9-1), + libaudit1 (= 1:3.0.9-1), + libbinutils (= 2.40-2), + libblkid1 (= 2.38.1-5+deb12u2), + libbsd0 (= 0.11.7-2), + libbz2-1.0 (= 1.0.8-5+b1), + libc-bin (= 2.36-9+deb12u9), + libc-dev-bin (= 2.36-9+deb12u9), + libc6 (= 2.36-9+deb12u9), + libc6-dev (= 2.36-9+deb12u9), + libcap-ng0 (= 0.8.3-1+b3), + libcap2 (= 1:2.66-4), + libcc1-0 (= 12.2.0-14), + libclang-common-14-dev (= 1:14.0.6-12), + libclang-cpp14 (= 1:14.0.6-12), + libclang1-14 (= 1:14.0.6-12), + libcom-err2 (= 1.47.0-2), + libcrypt-dev (= 1:4.4.33-2), + libcrypt1 (= 1:4.4.33-2), + libctf-nobfd0 (= 2.40-2), + libctf0 (= 2.40-2), + libdb5.3 (= 5.3.28+dfsg2-1), + libdebconfclient0 (= 0.270), + libdebhelper-perl (= 13.11.4), + libdpkg-perl (= 1.21.22), + libedit2 (= 3.1-20221030-2), + libelf1 (= 0.188-2.1), + libexpat1 (= 2.5.0-1+deb12u1), + libffi8 (= 3.4.4-1), + libfile-stripnondeterminism-perl (= 1.13.1-1), + libgc1 (= 1:8.2.2-3), + libgcc-11-dev (= 11.3.0-12), + libgcc-12-dev (= 12.2.0-14), + libgcc-s1 (= 12.2.0-14), + libgcrypt20 (= 1.10.1-3), + libgdbm-compat4 (= 1.23-3), + libgdbm6 (= 1.23-3), + libgmp10 (= 2:6.2.1+dfsg1-1.1), + libgomp1 (= 12.2.0-14), + libgpg-error0 (= 1.46-1), + libgprofng0 (= 2.40-2), + libgssapi-krb5-2 (= 1.20.1-2+deb12u2), + libicu72 (= 72.1-3), + libisl23 (= 0.25-1.1), + libitm1 (= 12.2.0-14), + libjansson4 (= 2.14-2), + libk5crypto3 (= 1.20.1-2+deb12u2), + libkeyutils1 (= 1.6.3-2), + libkrb5-3 (= 1.20.1-2+deb12u2), + libkrb5support0 (= 1.20.1-2+deb12u2), + libllvm14 (= 1:14.0.6-12), + liblsan0 (= 12.2.0-14), + liblz4-1 (= 1.9.4-1), + liblzma5 (= 5.4.1-0.2), + libmagic-mgc (= 1:5.44-3), + libmagic1 (= 1:5.44-3), + libmd0 (= 1.0.4-2), + libmount1 (= 2.38.1-5+deb12u2), + libmpc3 (= 1.3.1-1), + libmpfr6 (= 4.2.0-1), + libncursesw6 (= 6.4-4), + libnsl-dev (= 1.3.0-2), + libnsl2 (= 1.3.0-2), + libobjc-12-dev (= 12.2.0-14), + libobjc4 (= 12.2.0-14), + libpam-modules (= 1.5.2-6+deb12u1), + libpam-modules-bin (= 1.5.2-6+deb12u1), + libpam-runtime (= 1.5.2-6+deb12u1), + libpam0g (= 1.5.2-6+deb12u1), + libpcre2-8-0 (= 10.42-1), + libperl5.36 (= 5.36.0-7+deb12u1), + libpipeline1 (= 1.5.7-1), + libpython3-stdlib (= 3.11.2-1+b1), + libpython3.11-minimal (= 3.11.2-6+deb12u4), + libpython3.11-stdlib (= 3.11.2-6+deb12u4), + libquadmath0 (= 12.2.0-14), + libreadline8 (= 8.2-1.3), + libseccomp2 (= 2.5.4-1+deb12u1), + libselinux1 (= 3.4-1+b6), + libsigsegv2 (= 2.14-1), + libsmartcols1 (= 2.38.1-5+deb12u2), + libsqlite3-0 (= 3.40.1-2+deb12u1), + libssl3 (= 3.0.15-1~deb12u1), + libstdc++-12-dev (= 12.2.0-14), + libstdc++6 (= 12.2.0-14), + libsub-override-perl (= 0.09-4), + libsystemd0 (= 252.31-1~deb12u1), + libtinfo6 (= 6.4-4), + libtirpc-common (= 1.3.3+ds-1), + libtirpc-dev (= 1.3.3+ds-1), + libtirpc3 (= 1.3.3+ds-1), + libtool (= 2.4.7-7~deb12u1), + libtsan0 (= 11.3.0-12), + libtsan2 (= 12.2.0-14), + libubsan1 (= 12.2.0-14), + libuchardet0 (= 0.0.7-1), + libudev1 (= 252.31-1~deb12u1), + libunistring2 (= 1.0-2), + libuuid1 (= 2.38.1-5+deb12u2), + libxml2 (= 2.9.14+dfsg-1.3~deb12u1), + libz3-4 (= 4.8.12-3.1), + libzstd1 (= 1.5.4+dfsg2-5), + linux-libc-dev (= 6.1.115-1), + llvm-14-linker-tools (= 1:14.0.6-12), + login (= 1:4.13+dfsg1-1+b1), + m4 (= 1.4.19-3), + make (= 4.3-4.1), + man-db (= 2.11.2-2), + mawk (= 1.3.4.20200120-3.1), + media-types (= 10.0.0), + ncurses-base (= 6.4-4), + ncurses-bin (= 6.4-4), + patch (= 2.7.6-7), + perl (= 5.36.0-7+deb12u1), + perl-base (= 5.36.0-7+deb12u1), + perl-modules-5.36 (= 5.36.0-7+deb12u1), + po-debconf (= 1.0.21+nmu1), + python3 (= 3.11.2-1+b1), + python3-all (= 3.11.2-1+b1), + python3-distutils (= 3.11.2-3), + python3-lib2to3 (= 3.11.2-3), + python3-minimal (= 3.11.2-1+b1), + python3-pkg-resources (= 66.1.1-1), + python3-setuptools (= 66.1.1-1), + python3.11 (= 3.11.2-6+deb12u4), + python3.11-minimal (= 3.11.2-6+deb12u4), + readline-common (= 8.2-1.3), + rpcsvc-proto (= 1.4.3-1), + sed (= 4.9-1), + sensible-utils (= 0.0.17+nmu1), + sysvinit-utils (= 3.06-4), + tar (= 1.34+dfsg-1.2+deb12u1), + usr-is-merged (= 37~deb12u1), + util-linux (= 2.38.1-5+deb12u2), + util-linux-extra (= 2.38.1-5+deb12u2), + xz-utils (= 5.4.1-0.2), + zlib1g (= 1:1.2.13.dfsg-1) +Environment: + DEB_BUILD_OPTIONS="parallel=4" + LANG="ru_RU.UTF-8" + LC_ADDRESS="ru_RU.UTF-8" + LC_IDENTIFICATION="ru_RU.UTF-8" + LC_MEASUREMENT="ru_RU.UTF-8" + LC_MONETARY="ru_RU.UTF-8" + LC_NAME="ru_RU.UTF-8" + LC_NUMERIC="ru_RU.UTF-8" + LC_PAPER="ru_RU.UTF-8" + LC_TELEPHONE="ru_RU.UTF-8" + LC_TIME="ru_RU.UTF-8" + SOURCE_DATE_EPOCH="1732703360" diff --git a/deb_dist/postinstall_1.5.0-1_source.changes b/deb_dist/postinstall_1.5.0-1_source.changes new file mode 100644 index 0000000..4152fa3 --- /dev/null +++ b/deb_dist/postinstall_1.5.0-1_source.changes @@ -0,0 +1,28 @@ +Format: 1.8 +Date: Wed, 27 Nov 2024 15:29:20 +0500 +Source: postinstall +Architecture: source +Version: 1.5.0-1 +Distribution: stable +Urgency: low +Maintainer: LLC Technologies and Communication +Changed-By: LLC Technologies and Communication +Changes: + postinstall (1.5.0-1) stable; urgency=low + . + * source package automatically created by stdeb 0.10.0 +Checksums-Sha1: + 1beaad3f713cfd536c16b5fcd4a9cddfa6529b3a 893 postinstall_1.5.0-1.dsc + ac329e39abab33ddf9964dfcb5ad03aab59f0ab0 642170 postinstall_1.5.0.orig.tar.gz + caf27637149d91b9f87a356f21e014eb6017427d 1888 postinstall_1.5.0-1.debian.tar.xz + 788868c5acbe37a32956391e7f3478016a94e7b6 6322 postinstall_1.5.0-1_source.buildinfo +Checksums-Sha256: + 68a17ae036b380bd834c60f12097d769aea5019bff7f868863e4474960b24b41 893 postinstall_1.5.0-1.dsc + 06b3ddd18f5d21a9318e41696285bddeb246c82cb44f4413f64412bd2f59d0a1 642170 postinstall_1.5.0.orig.tar.gz + 510c8eb025289583a1849af810279915f6924535a565f2989b5fe8c6fcc6051e 1888 postinstall_1.5.0-1.debian.tar.xz + 4aefe14739f0abb8df18d8a2f94feec5af51fc71fd807c6e90c8b1c6d4f6eb04 6322 postinstall_1.5.0-1_source.buildinfo +Files: + c80476d28e9327b87b3373f984400eb7 893 python optional postinstall_1.5.0-1.dsc + 522aede680a8d15bbfd49ffd6efa2dbb 642170 python optional postinstall_1.5.0.orig.tar.gz + 68eeb9e6d47fb6b1bd638ab5fded4d29 1888 python optional postinstall_1.5.0-1.debian.tar.xz + 098b481348dd76a9581202b8011bf577 6322 python optional postinstall_1.5.0-1_source.buildinfo diff --git a/deb_dist/postinstall_1.5.0.orig.tar.gz b/deb_dist/postinstall_1.5.0.orig.tar.gz new file mode 100644 index 0000000..ab4de09 Binary files /dev/null and b/deb_dist/postinstall_1.5.0.orig.tar.gz differ diff --git a/deb_dist/python3-postinstall_1.0.1-1_all.deb b/deb_dist/python3-postinstall_1.0.1-1_all.deb new file mode 100644 index 0000000..b5458af Binary files /dev/null and b/deb_dist/python3-postinstall_1.0.1-1_all.deb differ diff --git a/deb_dist/python3-postinstall_1.5.0-1_all.deb b/deb_dist/python3-postinstall_1.5.0-1_all.deb new file mode 100644 index 0000000..2a5f93e Binary files /dev/null and b/deb_dist/python3-postinstall_1.5.0-1_all.deb differ diff --git a/deps/linkify_it_py-2.0.3-py3-none-any.whl b/deps/linkify_it_py-2.0.3-py3-none-any.whl new file mode 100644 index 0000000..171b7d6 Binary files /dev/null and b/deps/linkify_it_py-2.0.3-py3-none-any.whl differ diff --git a/deps/markdown_it_py-3.0.0-py3-none-any.whl b/deps/markdown_it_py-3.0.0-py3-none-any.whl new file mode 100644 index 0000000..2691698 Binary files /dev/null and b/deps/markdown_it_py-3.0.0-py3-none-any.whl differ diff --git a/deps/mdit_py_plugins-0.4.1-py3-none-any.whl b/deps/mdit_py_plugins-0.4.1-py3-none-any.whl new file mode 100644 index 0000000..6f0c076 Binary files /dev/null and b/deps/mdit_py_plugins-0.4.1-py3-none-any.whl differ diff --git a/deps/mdurl-0.1.2-py3-none-any.whl b/deps/mdurl-0.1.2-py3-none-any.whl new file mode 100644 index 0000000..6b8b6ab Binary files /dev/null and b/deps/mdurl-0.1.2-py3-none-any.whl differ diff --git a/deps/pygments-2.18.0-py3-none-any.whl b/deps/pygments-2.18.0-py3-none-any.whl new file mode 100644 index 0000000..e67ab03 Binary files /dev/null and b/deps/pygments-2.18.0-py3-none-any.whl differ diff --git a/deps/rich-13.7.1-py3-none-any.whl b/deps/rich-13.7.1-py3-none-any.whl new file mode 100644 index 0000000..0ed0599 Binary files /dev/null and b/deps/rich-13.7.1-py3-none-any.whl differ diff --git a/deps/textual-0.72.0-py3-none-any.whl b/deps/textual-0.72.0-py3-none-any.whl new file mode 100644 index 0000000..03e3a85 Binary files /dev/null and b/deps/textual-0.72.0-py3-none-any.whl differ diff --git a/deps/typing_extensions-4.12.2-py3-none-any.whl b/deps/typing_extensions-4.12.2-py3-none-any.whl new file mode 100644 index 0000000..f6cc799 Binary files /dev/null and b/deps/typing_extensions-4.12.2-py3-none-any.whl differ diff --git a/deps/uc_micro_py-1.0.3-py3-none-any.whl b/deps/uc_micro_py-1.0.3-py3-none-any.whl new file mode 100644 index 0000000..b417bea Binary files /dev/null and b/deps/uc_micro_py-1.0.3-py3-none-any.whl differ diff --git a/dist/postinstall-1.0.1.tar.gz b/dist/postinstall-1.0.1.tar.gz new file mode 100644 index 0000000..1b3cbbe Binary files /dev/null and b/dist/postinstall-1.0.1.tar.gz differ diff --git a/dist/postinstall-1.5.0.tar.gz b/dist/postinstall-1.5.0.tar.gz new file mode 100644 index 0000000..ab4de09 Binary files /dev/null and b/dist/postinstall-1.5.0.tar.gz differ diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..62d4c86 --- /dev/null +++ b/documentation.md @@ -0,0 +1,880 @@ +# Описание + +Программа `postinstall` предназначен для автоматизации различных задач по настройке и конфигурации системы после установки. Он предоставляет интерфейс для управления настройками сети, обновлениями системы, установкой программного обеспечения, настройкой SSH, изменением доменного имени и многими другими функциями. + +# Основные директории и файлы + +- **`postinstall/`**: Главная директория проекта. + - **`__init__.py`**: Инициализирует пакет `postinstall`. + - **`logger.py`**: Определяет функции для ведения логов. + - **`__main__.py`**: Главный исполняемый скрипт, запускающий основные функции проекта. + - **`root.py`**: Сценарий для управления правами суперпользователя. + - **`option/`**: Поддиректория, содержащая скрипты для различных опций настройки и установки. + +## postinstall/__init__.py + +Этот скрипт представляет собой основную точку входа для выполнения конфигурационных задач системы. Он использует библиотеку `dialog` для взаимодействия с пользователем через текстовые диалоговые окна и выполняет различные конфигурационные задачи, такие как обновление системы, настройка сети, добавление пользователей, установка пакетов и другие. Скрипт включает в себя функции для чтения и изменения системных конфигураций и резервного копирования. + +### Импортируемые модули и функции + +- `os`, `subprocess`, `sys`, `signal`: Стандартные модули Python для работы с операционной системой, выполнения команд и обработки сигналов. +- `Dialog` из `dialog`: Для создания текстовых диалоговых окон. +- `check_root` из `postinstall.root`: Проверка прав суперпользователя. +- Модули из `postinstall.option`: + - `network`: Настройка сетевых параметров. + - `update`: Обновление системы. + - `add_user`: Добавление пользователя. + - `install_packages`: Установка программ. + - `ssh`: Настройка SSH. + - `hostname`: Изменение имени хоста. + - `shell`: Выбор оболочки. + - `database`: Настройка базы данных. + - `docker`: Настройка Docker. +- `json`: Для работы с JSON-данными. +- `logger` из `postinstall.logger`: Для ведения логов. + +### Константы + +- `SSH_CONFIG_PATH`: Путь к конфигурационному файлу SSH. +- `HOSTNAME_FILE`: Путь к файлу с именем хоста. +- `AUTOSTART_FILE`: Путь к файлу автозапуска. +- `BACKUPS_FILE`: Путь к файлу резервных копий. +- `UFW_OPENSSH_CONFIG_PATH`: Путь к конфигурации UFW для OpenSSH. + +### Классы и методы + +#### `MenuScreen` + +- **Методы:** + - `__init__(self)`: Инициализация меню, получение текущих значений конфигурации (SSH порт, имя хоста, автозапуск и т.д.). + - `show_menu(self)`: Отображение главного меню и обработка выбора пользователя. + - `set_temp_network(self, settings)`: Установка временных сетевых настроек. + - `check_auto_start(self)`: Проверка состояния автозапуска. + - `set_auto_start(self, enable: bool)`: Включение или отключение автозапуска. + - `update_ssh_port(self, new_port)`: Обновление порта SSH. + - `update_hostname(self, new_hostname)`: Обновление имени хоста. + - `update_autostart(self, new_state)`: Обновление состояния автозапуска. + +### Функции + +#### `choose(option, menu)` + +- **Описание:** Обработка выбранной пользователем опции из меню. +- **Аргументы:** + - `option`: Выбранная опция меню. + - `menu`: Объект класса `MenuScreen`. +- **Возвращаемое значение:** + - `False`, если произошла ошибка или требуется завершение работы. + +#### `signal_handler(sig, frame)` + +- **Описание:** Обработчик сигналов для корректного завершения работы приложения. +- **Аргументы:** + - `sig`: Сигнал. + - `frame`: Контекст сигнала. + +#### `main()` + +- **Описание:** Основная функция, которая запускает приложение, обрабатывает сигналы и отображает меню. + +### Примечания + +- Скрипт требует прав суперпользователя для выполнения некоторых команд. +- Перед сохранением изменений в системных файлах скрипт делает резервные копии, что позволяет восстановить предыдущие состояния конфигураций при необходимости. + + +## postinstall/logger.py + +Модуль `logger.py` предназначен для настройки и управления логированием в приложении. Он обеспечивает запись логов в файл и добавление информации о текущем пользователе в каждый лог-запись. + +### Импортируемые модули + +- `logging`: Стандартный модуль для ведения логов в Python. +- `getpass`: Модуль для получения имени текущего пользователя. + +### Функции + +#### `setup_logger()` + +Настраивает логирование для приложения. + +**Возвращаемое значение:** + +- `logging.Logger` - Настроенный экземпляр логирования. + +**Подробности:** + +1. **Создание логов:** + + - `logger = logging.getLogger()` + - Устанавливается уровень логирования на `INFO`. +2. **Настройка формата:** + + - Формат лог-сообщений: `%(asctime)s - User: %(user)s - %(message)s` + - `%(asctime)s` - Время записи. + - `%(user)s` - Имя пользователя, делавшего запись. + - `%(message)s` - Сообщение лога. +3. **Создание и настройка обработчика файла:** + + - `file_handler = logging.FileHandler("/var/log/postinstall.log")` + - Обработчик записывает логи в файл `/var/log/postinstall.log`. + - Форматтер применяет формат к записям. +4. **Создание и применение фильтра:** + + - `UserFilter` - Класс фильтра, добавляющий имя пользователя к каждой записи лога. + - Фильтр используется для добавления пользовательской информации к записям. +5. **Добавление обработчика и фильтра к логам:** + + - Обработчик и фильтр добавляются к логам. + +**Примечание:** + +- Файл журнала будет создан в `/var/log/`. + +### Переменные + +#### `logger` + +- Настроенный экземпляр логгера, возвращаемый функцией `setup_logger()`. + + +## postinstall/root.py + +Модуль `root.py` предназначен для проверки, что скрипт запущен с правами суперпользователя (root). Это важно для обеспечения выполнения программы. Postinstall работает только с правами суперпользователя для обеспечения успешной работы всех модулей и функций. +### Импортируемые модули + +- `os`: Модуль для взаимодействия с операционной системой. +- `sys`: Модуль для работы с параметрами и функциями интерпретатора Python. + +### Функции + +#### `check_root()` + +Проверяет, что скрипт запущен от имени суперпользователя. + +**Возвращаемое значение:** + +- Нет (в случае успеха) - Если скрипт запущен с правами суперпользователя, функция просто завершает выполнение без ошибок. +- `sys.exit(1)` - Если скрипт не запущен с правами суперпользователя, функция завершает выполнение скрипта с кодом ошибки `1`. + +**Подробности:** + +1. **Получение идентификатора пользователя:** + + - `os.geteuid()` - Возвращает идентификатор эффективного пользователя текущего процесса. +2. **Проверка прав:** + + - Если идентификатор эффективного пользователя не равен `0` (что соответствует root), выводится сообщение об ошибке: `"Запустите скрипт с правами суперпользователя"`. + - Скрипт завершается с кодом ошибки `1` с помощью `sys.exit(1)`. + +## option/add_user.py + +Скрипт `add_user.py` предназначен для добавления нового пользователя в систему Linux через текстовый интерфейс. Также предоставляет возможность скрывать или показывать вводимый пароль и проверяет, существует ли уже пользователь в системе. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд в командной строке и взаимодействия с процессами. +- `dialog.Dialog`: Для создания текстовых диалогов. +- `pwd`: Для проверки существования пользователя в системе. +- `postinstall.logger`: Для ведения логов, импортирован как `logger`. + +### Функции + +#### `get_password_with_toggle(d, title)` + +Функция для ввода пароля с возможностью переключения видимости пароля (показ/скрытие). + +**Параметры:** + +- `d` (Dialog): Объект `Dialog`, используемый для создания диалоговых окон. +- `title` (str): Заголовок диалогового окна. + +**Процесс:** + +1. Функция отображает диалоговое окно для ввода пароля с возможностью переключения между скрытым и открытым отображением пароля. +2. Пользователь может нажать кнопку "Показать пароль" или "Скрыть пароль" для изменения состояния видимости пароля. +3. Если пользователь нажимает "Отмена", функция возвращает `None`. +4. Возвращает введённый пароль, если пользователь подтверждает ввод. + +#### `add_user()` + +Основная функция для добавления нового пользователя в систему. + +**Процесс:** + +1. **Создание формы для ввода данных:** + + - Поля формы включают имя пользователя, полное имя, мобильный телефон, городской телефон и кабинет. + - После ввода данных в форму, проверяется, существует ли уже пользователь с таким именем. +2. **Проверка существования пользователя:** + + - Используется модуль `pwd` для проверки существования пользователя. + - Если пользователь уже существует, выводится сообщение об ошибке. +3. **Запрос пароля:** + + - Используется `get_password_with_toggle` для ввода пароля с возможностью его показать/скрыть. + - Пароль также проверяется на совпадение с подтверждением пароля. +4. **Запрос прав администратора:** + + - Диалоговое окно спрашивает, нужно ли сделать пользователя администратором. +5. **Создание пользователя:** + + - Выполняется команда `useradd` для создания пользователя с указанными параметрами. + - Устанавливается пароль для нового пользователя с помощью команды `passwd`. + - Если пользователь назначается администратором, добавляется в группу `sudo`. +6. **Логирование и сообщение:** + + - Записывается информация о созданном пользователе в журнал. + - Отображается сообщение об успешном добавлении пользователя или об ошибке. + +#### `run_add_user()` + +Функция-обёртка для вызова функции `add_user`. + +### Запуск скрипта + +При запуске скрипта напрямую (`if __name__ == "__main__":`), вызывается функция `run_add_user`, которая в свою очередь вызывает `add_user`. + +### Обработка ошибок + +- **`subprocess.CalledProcessError`**: Ловит ошибки выполнения команд в командной строке и записывает их в журнал. +- Сообщения об ошибках также отображаются в диалоговом окне для пользователя. + + +## option/autostart.py + +Скрипт `autostart.py` отвечает за управление настройками автозапуска при следующем запуске системы. Скрипт использует файл в файловой системе для хранения состояния автозапуска. + +### Импортируемые модули + +- `os`: Для работы с файловой системой (проверка наличия файлов, создание и удаление файлов). +- `dialog.Dialog`: Для создания текстовых диалогов. +- `postinstall.logger`: Для ведения логов, импортирован как `logger`. + +### Константы + +- `AUTOSTART_FILE`: Путь к файлу, который указывает на состояние автозапуска. Если файл существует, автозапуск отключен. Если файл отсутствует, автозапуск включен. + +### Классы и методы + +#### Класс `AutoStartScreen` + +##### Методы + +- **`__init__()`** + + Конструктор класса `AutoStartScreen`. Инициализирует объект `Dialog` для создания текстовых диалогов. + +- **`check_auto_start() -> bool`** + + Проверяет текущее состояние автозапуска. Возвращает `True`, если автозапуск включен (файл `AUTOSTART_FILE` отсутствует), и `False`, если автозапуск отключен (файл присутствует). + +- **`set_auto_start(enable: bool) -> None`** + + Изменяет состояние автозапуска в зависимости от значения `enable`. + + - **Если `enable` равно `True`:** Удаляет файл `AUTOSTART_FILE`, если он существует. Это включает автозапуск. + - **Если `enable` равно `False`:** Создаёт файл `AUTOSTART_FILE`, чтобы отключить автозапуск. В файл записывается комментарий. + + В случае ошибок при удалении или создании файла записываются сообщения об ошибках в журнал. + +- **`show_screen()`** + + Отображает диалоговое окно для выбора действия по настройке автозапуска. Используется `checklist` для отображения опций и состояния текущего автозапуска. + + - Если пользователь выбирает опцию "Автозапуск при следующем запуске", состояние автозапуска изменяется в соответствии с выбором. + - После применения изменений отображается сообщение об успешном сохранении изменений. + - Если пользователь отменяет изменения, в журнал записывается информация об отмене. + +### Функция `main()` + +Создаёт экземпляр `AutoStartScreen` и вызывает метод `show_screen()` для отображения диалогового окна с настройками автозапуска. + +### Запуск скрипта + +При запуске скрипта напрямую (`if __name__ == "__main__":`), вызывается функция `main`, которая инициирует работу с настройками автозапуска. + + +## option/install_packages.py + +Скрипт `install_packages.py` предназначен для установки пакетов на систему Linux. Он предоставляет пользователю интерфейс для выбора базы данных и установки связанных с ней пакетов, а также возможности установки Webmin и дополнительных компонентов. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд оболочки и обработки их вывода. +- `dialog.Dialog`: Для создания текстовых диалогов. + +### Функции + +#### `main()` + +Основная функция, которая управляет процессом установки: + +1. Создаёт объект `Dialog` для создания текстовых диалогов. +2. Отображает диалоговое окно для выбора базы данных (PostgreSQL или MariaDB). + - Если пользователь выбирает PostgreSQL или MariaDB, вызываются соответствующие функции для установки пакетов. + - Независимо от выбранной базы данных, предлагается установка Webmin. + - Если пользователь соглашается на установку Webmin, вызывается функция `install_webmin`. + +#### `is_installed(package_name)` + +Проверяет, установлен ли пакет с именем `package_name`: + +- Выполняет команду `dpkg-query -W -f=${Status} package_name` для получения статуса пакета. +- Если вывод команды содержит строку `'install ok installed'`, возвращает `True`. +- В случае ошибки возвращает `False`. + +#### `run_command_with_log(dialog, command, title)` + +Запускает команду и выводит лог её выполнения в диалоговое окно: + +- Создаёт процесс для выполнения команды с помощью `subprocess.Popen`. +- Читает стандартный вывод и стандартный ошибки процесса, собирая их в список `log_output`. +- Отображает результат в диалоговом окне с заголовком `title`. +- Возвращает `True`, если команда выполнена успешно, иначе `False`. + +#### `install_package(dialog, package_name)` + +Устанавливает указанный пакет с выводом лога: + +- Отображает сообщение о начале установки пакета. +- Обновляет список пакетов с помощью команды `apt-get update`. +- Устанавливает пакет с помощью `apt-get install -y package_name`. +- Отображает сообщение об успешной установке или ошибке. + +#### `install_postgresql(dialog)` + +Устанавливает PostgreSQL и связанный пакет Harlequin: + +- Проверяет и устанавливает пакеты `postgresql-16`, `python3-harlequin`, и `python3-harlequin-postgres`, если они не установлены. + +#### `install_mariadb(dialog)` + +Устанавливает MariaDB: + +- Проверяет и устанавливает пакеты `mariadb-server` и `python3-harlequin`, если они не установлены. + +#### `install_webmin(dialog)` + +Устанавливает Webmin и связанные пакеты: + +- Проверяет, установлен ли Webmin. Если нет, выполняет установку: + - Устанавливает Webmin и дополнительные пакеты (`postgresql-server-dev-16` и `cpanminus`). + - Разрешает порт `10000/tcp` в `ufw` (брандмауэр). + - Устанавливает Perl-модули `DBI` и `DBD::Pg` через `cpanm`. + - Перезагружает Webmin. +- Отображает сообщения о каждом шаге процесса установки. + +### Запуск скрипта + +При запуске скрипта напрямую (`if __name__ == "__main__":`), вызывается функция `main`, которая инициирует процесс установки пакетов. + + +## option/docker.py + +Скрипт `docker_config.py` управляет установкой и удалением Docker и Portainer. Логирование выполняется через модуль `logger` из `postinstall.logger`. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд оболочки и обработки их вывода. +- `os`: Для работы с файловой системой. +- `re`: Для работы с регулярными выражениями. +- `shlex`: Для безопасного разбора команд. +- `abc`, `abstractmethod`: Для создания абстрактных базовых классов и методов. +- `Optional`, `List`, `Callable`: Для аннотаций типов. +- `Dialog`: Для создания текстовых диалогов. +- `logger`: Для логирования. + +### Константы + +- `REGISTRY_URL`: URL Docker registry. +- `REGISTRY_LOGIN`: Логин для Docker registry. +- `REGISTRY_PASSWORD`: Пароль для Docker registry. +- `DOCKER_COMPOSE_FILE_PATH`: Путь к файлу `docker-compose.yml`. + +### Классы и их методы + +#### `CommandExecutor` + +- **Методы:** + - `run_command(command: str, shell: bool = False, input_data: Optional[str] = None, ignore_errors: List[int] = []) -> subprocess.CompletedProcess`: Выполняет команду и возвращает результат выполнения. Обрабатывает ошибки и логирует их. + +#### `SystemChecker` + +- **Методы:** + - `is_command_installed(command: str) -> bool`: Проверяет, установлен ли командный инструмент. + +#### `ProgressGauge` + +- **Методы:** + - `__init__(self, dialog: Dialog, title: str, total_steps: int)`: Инициализирует объект с диалогом и настройками для отображения индикатора выполнения. + - `update(self, step: int, message: str)`: Обновляет индикатор выполнения. + - `__enter__(self)`: Запускает индикатор выполнения. + - `__exit__(self, exc_type, exc_val, exc_tb)`: Останавливает индикатор выполнения. + +#### `DockerManager` + +- **Методы:** + - `install(progress_callback: Callable[[int, str], None])`: Устанавливает Docker и Docker Compose. + - `uninstall(progress_callback: Callable[[int, str], None])`: Удаляет Docker и Docker Compose. + - `login_to_registry()`: Выполняет вход в Docker registry. + +#### `UFWManager` + +- **Методы:** + - `add_rule(port: str)`: Добавляет правило для открытия порта в UFW. + - `remove_rule(port: str)`: Удаляет правило для порта в UFW. + +#### `PortainerManager` + +- **Методы:** + - `install(port: str, password: str, ip: str, progress_callback: Callable[[int, str], None])`: Устанавливает Portainer с указанными параметрами. + - `uninstall(progress_callback: Callable[[int, str], None])`: Удаляет Portainer. + - `_generate_password_hash(password: str) -> str`: Генерирует хеш пароля для Portainer. + - `_create_docker_compose_file(port: str, password_hash: str, ip: str)`: Создаёт файл `docker-compose.yml` для Portainer. + +#### `InputValidator` + +- **Методы:** + - `validate(value: str, pattern: str) -> bool`: Проверяет значение на соответствие регулярному выражению. + +#### `UIElement` + +- **Методы:** + - `__init__(self, dialog: Dialog)`: Инициализирует элемент пользовательского интерфейса. + - `show(self)`: Абстрактный метод для отображения UI элемента. + +#### `Menu` + +- **Методы:** + - `show(self)`: Отображает меню для выбора действия (установка или удаление Docker). + +#### `YesNoUI` + +- **Методы:** + - `show(self, text: str)`: Отображает диалоговое окно с вопросом и возвращает ответ пользователя. + +#### `PasswordInput` + +- **Методы:** + - `show(self)`: Отображает диалоговое окно для ввода пароля и возвращает его, если пароль соответствует требованиям. + +#### `PortInput` + +- **Методы:** + - `show(self)`: Отображает диалоговое окно для ввода порта и возвращает его, если порт корректен. + +#### `IPInput` + +- **Методы:** + - `show(self)`: Отображает диалоговое окно для ввода IP-адреса и возвращает его, если IP корректен. + +#### `DockerConfigUI` + +- **Методы:** + - `__init__(self)`: Инициализирует UI для конфигурации Docker. + - `run(self)`: Запускает основной цикл работы UI. + - `_handle_docker(self)`: Обрабатывает установку или удаление Docker. + - `handle_portainer(self)`: Обрабатывает установку или удаление Portainer. + - `_install_portainer(self)`: Устанавливает Portainer с запросом необходимых данных у пользователя. + +### Запуск скрипта + +При запуске скрипта напрямую вызывается функция `run`, которая создаёт объект `DockerConfigUI` и запускает его метод `run`, инициируя процесс конфигурации Docker и Portainer. + +## option/hostname.py + +Скрипт `change_hostname.py` предназначен для изменения доменного имени (hostname) системы. Скрипт читает текущее доменное имя, позволяет пользователю ввести новое доменное имя, проверяет его валидность и применяет изменения. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд оболочки. +- `Dialog`: Для создания текстовых диалогов. +- `os`: Для работы с файловой системой. + +### Константы + +- `HOSTNAME_FILE`: Путь к файлу, содержащему текущее доменное имя. +- `HOSTS_FILE`: Путь к файлу `hosts`, где также может быть указано доменное имя. + +### Функции и их назначения + +#### `get_current_hostname()` + +- **Описание:** Читает текущее доменное имя из файла `/etc/hostname`. +- **Возвращаемое значение:** Возвращает текущее доменное имя или `"localhost"` в случае ошибки. + +#### `change_hostname()` + +- **Описание:** Отображает диалоговые окна для ввода нового доменного имени. Проверяет, совпадает ли новое имя с текущим, и если нет, проверяет его валидность. Запрашивает подтверждение изменения. +- **Возвращаемое значение:** Возвращает новое доменное имя, если изменения подтверждены, или `None`, если изменения отменены или имя недопустимо. + +#### `is_valid_hostname(hostname)` + +- **Описание:** Проверяет, соответствует ли введённое доменное имя требованиям. +- **Параметры:** + - `hostname`: Проверяемое доменное имя. +- **Возвращаемое значение:** Возвращает `True`, если имя допустимо, и `False` в противном случае. + +#### `apply_hostname_change(new_hostname)` + +- **Описание:** Применяет изменение доменного имени, обновляя файлы `/etc/hostname` и `/etc/hosts`. +- **Параметры:** + - `new_hostname`: Новое доменное имя. +- **Возвращаемое значение:** Возвращает `True`, если изменения применены успешно, и `False` в случае ошибки. + +#### `run_change_hostname()` + +- **Описание:** Вызывает функцию `change_hostname` и возвращает результат её выполнения. +- **Возвращаемое значение:** Возвращает результат выполнения `change_hostname`. + +### Основной блок выполнения + +- При запуске скрипта напрямую вызывается функция `run_change_hostname()`, которая запускает процесс изменения доменного имени. +- Если новое доменное имя получено и изменения подтверждены, оно будет выведено на экран. В противном случае будет выведено сообщение об отмене изменения. + +### Логирование и ошибки + +- Ошибки при чтении файлов или выполнении команд оболочки обрабатываются и выводятся пользователю через диалоговые окна. +- Успешное изменение доменного имени или отмена изменений также сообщаются пользователю через диалоговые окна. + +## option/network.py + +Скрипт `network.py` предназначен для настройки сетевых интерфейсов в системе. Скрипт позволяет пользователю выбирать сетевой интерфейс и конфигурировать его для работы с DHCP, статическим IP или CIDR. После настройки, скрипт применяет изменения и обновляет конфигурационные файлы. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд оболочки. +- `ipaddress`: Для работы с IP-адресами и сетями. +- `Dialog`: Для создания текстовых диалогов. + +### Класс `NetworkConfig` + +#### Метод `__init__(self)` + +- **Описание:** Инициализирует объект `NetworkConfig`. Создает объект `Dialog` и пустой словарь для хранения временных сетевых настроек. + +#### Метод `select_interface(self)` + +- **Описание:** + 1. Получает список доступных сетевых интерфейсов. + 2. Позволяет пользователю выбрать интерфейс из списка. + 3. Показывает текущие настройки выбранного интерфейса. +- **Возвращаемое значение:** Имя выбранного интерфейса или `None`, если интерфейс не выбран. + +#### Метод `get_network_interfaces()` + +- **Описание:** Получает список доступных сетевых интерфейсов. +- **Возвращаемое значение:** Список имен интерфейсов или пустой список в случае ошибки. + +#### Метод `show_current_settings(self, interface)` + +- **Описание:** + 1. Получает текущие настройки для указанного интерфейса. + 2. Показывает информацию о типе настройки (DHCP или Static) и текущем IP-адресе. +- **Возвращаемое значение:** Нет. + +#### Метод `configure_dhcp(self, interface)` + +- **Описание:** + 1. Позволяет пользователю ввести настройки для DHCP (опционально DNS). + 2. Сохраняет настройки в временный словарь. +- **Возвращаемое значение:** Нет. + +#### Метод `configure_static(self, interface)` + +- **Описание:** + 1. Позволяет пользователю ввести настройки для статического IP (IP-адрес, маска подсети, шлюз и опционально DNS). + 2. Сохраняет настройки в временный словарь. +- **Возвращаемое значение:** Нет. + +#### Метод `configure_cidr(self, interface)` + +- **Описание:** + 1. Позволяет пользователю ввести CIDR, шлюз и опционально DNS. + 2. Распознает IP-адрес и маску из CIDR и сохраняет настройки в временный словарь. +- **Возвращаемое значение:** Нет. + +#### Метод `run(self)` + +- **Описание:** + 1. Позволяет пользователю выбрать интерфейс и действия для его настройки. + 2. Запускает соответствующие методы для настройки (DHCP, статический IP, CIDR). + 3. Возвращает словарь временных сетевых настроек. +- **Возвращаемое значение:** Словарь временных сетевых настроек. + +#### Метод `save_and_apply_network_settings(self)` + +- **Описание:** + 1. Сохраняет и применяет временные сетевые настройки. + 2. Обновляет файл конфигурации интерфейсов и файл DNS. + 3. Перезапускает сетевую службу. +- **Возвращаемое значение:** `True` при успешном применении настроек и `False` в случае ошибки. + +#### Метод `save_dhcp(self, interface, settings)` + +- **Описание:** Сохраняет настройки DHCP для указанного интерфейса в конфигурационный файл. +- **Возвращаемое значение:** Нет. + +#### Метод `save_static(self, interface, settings)` + +- **Описание:** Сохраняет статические настройки IP для указанного интерфейса в конфигурационный файл. +- **Возвращаемое значение:** Нет. + +#### Метод `update_interface_config(self, interface, interface_config)` + +- **Описание:** Обновляет конфигурационный файл `/etc/network/interfaces` с учетом новых настроек интерфейса. +- **Возвращаемое значение:** Нет. + +#### Метод `update_resolv_conf(self, dns)` + +- **Описание:** Обновляет файл `/etc/resolv.conf` с новым DNS-сервером. +- **Возвращаемое значение:** Нет. + +### Основной блок выполнения + +- При запуске скрипта напрямую создается объект `NetworkConfig` и вызывается метод `run()`, который запускает процесс настройки сетевых интерфейсов. + +### Примечания + +- Скрипт требует прав суперпользователя для внесения изменений в конфигурационные файлы и перезапуска сетевой службы. +- Для корректной работы необходимо наличие утилиты `dialog` и доступа к командам `ip` и `systemctl`. +- При использовании CIDR, маска подсети автоматически рассчитывается на основе IP-адреса и CIDR. + +## option/shell.py + +Скрипт `shell.py` предназначен для изменения командной оболочки для пользователей системы. Скрипт позволяет выбирать между установкой `zsh` (или другой оболочки) и изменением оболочки для всех пользователей системы или для конкретных пользователей. + +### Импортируемые модули + +- `subprocess`: Для выполнения команд оболочки. +- `Dialog`: Для создания текстовых диалогов. +- `pwd`: Для получения информации о пользователях. +- `os`: Для проверки существования файлов и путей. +- `logger` из `postinstall.logger`: Для ведения логов. + +### Функции + +#### Функция `get_users()` + +- **Описание:** Получает список пользователей системы с UID >= 1000 и добавляет "root" в список, если его нет. +- **Возвращаемое значение:** Список имен пользователей. + +#### Функция `install_zsh()` + +- **Описание:** + 1. Обновляет список пакетов. + 2. Устанавливает `zsh`. +- **Возвращаемое значение:** `True` при успешной установке и `False` в случае ошибки. + +#### Функция `change_shell()` + +- **Описание:** Основная функция для изменения оболочки. + 1. Создает объект `Dialog`. + 2. Получает список пользователей с помощью `get_users()`. + 3. Позволяет пользователю выбрать способ изменения оболочки (для всех пользователей или для конкретных). + 4. Позволяет выбрать новую оболочку (bash или zsh). + 5. Если выбран `zsh` и он не установлен, вызывает `install_zsh()` для его установки. + 6. Применяет изменения: изменяет оболочку для всех или выбранных пользователей с помощью команды `chsh`. +- **Возвращаемое значение:** Нет. + +#### Функция `run_change_shell()` + +- **Описание:** Запускает функцию `change_shell()`. +- **Возвращаемое значение:** Нет. + +### Основной блок выполнения + +- При запуске скрипта напрямую вызывается функция `run_change_shell()`, которая запускает процесс изменения оболочки. + +### Примечания + +- Установка `zsh` происходит только если выбранная оболочка `zsh` не установлена на системе. +- Логи об ошибках и успешных действиях сохраняются с помощью объекта `logger`. + +## option/ssh.py + +Скрипт `ssh.py` предназначен для управления конфигурацией SSH-сервиса на системе. Он позволяет: + +1. Управлять состоянием службы SSH (запуск, остановка). +2. Изменять порт SSH. +3. Изменять метод аутентификации SSH. + +### Константы + +- `SSH_CONFIG_PATH`: Путь к конфигурационному файлу SSH (`/etc/ssh/sshd_config`). +- `SSH_AUTH_CONFIG_PATH`: Путь к файлу конфигурации аутентификации SSH (`/etc/ssh/sshd_config.d/auth.conf`). +- `UFW_OPENSSH_CONFIG_PATH`: Путь к файлу конфигурации UFW для OpenSSH (`/etc/ufw/applications.d/openssh-server`). + +### Класс `SSHConfig` + +#### Методы + +##### `__init__(self)` + +- **Описание:** Инициализация класса. Создает объект `Dialog` и получает текущие настройки порта и метода аутентификации SSH. +- **Возвращаемое значение:** Нет. + +##### `get_sshd_status(self)` + +- **Описание:** Получает статус службы SSH. +- **Возвращаемое значение:** Строка, представляющая статус службы (`active`, `inactive`, или `unknown`). + +##### `toggle_sshd_service(self, action)` + +- **Описание:** Запускает или останавливает службу SSH в зависимости от переданного действия (`start` или `stop`). +- **Возвращаемое значение:** Нет. + +##### `get_current_ssh_port(self)` + +- **Описание:** Получает текущий порт SSH из конфигурационного файла. +- **Возвращаемое значение:** Текущий порт SSH (или "22", если порт не найден). + +##### `change_ssh_port(self)` + +- **Описание:** Позволяет пользователю ввести новый порт SSH и подтверждает изменение. +- **Возвращаемое значение:** Нет. + +##### `get_current_auth_method(self)` + +- **Описание:** Получает текущий метод аутентификации SSH из конфигурационного файла. +- **Возвращаемое значение:** Строка, представляющая метод аутентификации (`"По ключу и паролю"`, `"Только по ключу"`, или `"Только по паролю"`). + +##### `change_auth_method(self)` + +- **Описание:** Позволяет пользователю выбрать метод аутентификации SSH. +- **Возвращаемое значение:** Нет. + +##### `apply_changes(self)` + +- **Описание:** Применяет изменения порта и/или метода аутентификации, если они были сделаны. +- **Возвращаемое значение:** Нет. + +##### `main_menu(self)` + +- **Описание:** Основное меню, которое позволяет пользователю выбрать действие: включение/выключение службы SSH, изменение порта SSH, изменение метода аутентификации. +- **Возвращаемое значение:** `True`, если были внесены изменения, иначе `False`. + +### Функция `run_ssh_config()` + +- **Описание:** Создает экземпляр `SSHConfig`, запускает основное меню и возвращает объект конфигурации и флаг, указывающий на наличие изменений. +- **Возвращаемое значение:** Кортеж `(SSHConfig, changes_made)`. + +### Основной блок выполнения + +- При запуске скрипта напрямую вызывается функция `run_ssh_config()`. Она запускает основной процесс управления конфигурацией SSH и применяет изменения, если они были внесены. + +### Примечания + +- Используются команды `sed` и `ufw` для обновления конфигураций, а также перезапуск службы SSH для применения изменений. +- Логи об ошибках и успешных действиях сохраняются с помощью модуля `logging`. + +## option/update.py + +Скрипт `update.py` предназначен для обновления системы. Он выполняет обновление списка пакетов и обновление установленных пакетов до последних доступных версий. +### Функции + +#### `run_update()` + +- **Описание:** Выполняет обновление системы. Запрашивает подтверждение у пользователя, выполняет команды обновления через `apt`, и отображает процесс обновления с помощью индикатора `gauge` библиотеки `dialog`. + +- **Шаги выполнения:** + + 1. **Запрос подтверждения:** Появляется диалоговое окно с запросом на подтверждение обновления системы. Если пользователь не подтверждает, функция завершает выполнение. + 2. **Инициализация индикатора прогресса:** Запускается индикатор прогресса с начальным значением 0%. + 3. **Выполнение команд обновления:** + - Выполняются команды `apt update` и `apt upgrade -y` с обновлением индикатора прогресса в зависимости от выполнения команд. + - Вывод команд отображается в диалоговом окне. + - Если команда завершается с ошибкой, возникает исключение `subprocess.CalledProcessError`, и выводится сообщение об ошибке. + 4. **Обработка ошибок:** + - Если происходит ошибка при выполнении команды, логируется ошибка, и отображается сообщение об ошибке. + - Если возникает неожиданная ошибка, также логируется и отображается соответствующее сообщение. + 5. **Завершение обновления:** Обновление системы завершено, отображается сообщение об успешном завершении. + +### Основной блок выполнения + +- При запуске скрипта напрямую вызывается функция `run_update()`. + +## option/database.py + +Скрипт `database.py` предназначен для установки и настройки баз данных PostgreSQL и MariaDB, а также для установки Webmin и необходимых дополнительных пакетов. + +### Основные функции + +1. **Выбор базы данных для установки:** + + - Пользователь может выбрать установку PostgreSQL или MariaDB через диалоговое окно. +2. **Установка PostgreSQL:** + + - Проверка и установка PostgreSQL и сопутствующих пакетов, включая `python3-harlequin` и `python3-harlequin-postgres`. +3. **Установка MariaDB:** + + - Проверка и установка MariaDB и `python3-harlequin`. +4. **Установка Webmin:** + + - Проверка и установка Webmin, разрешение порта 10000/tcp в ufw, установка модулей DBI и DBD::Pg через `cpanminus`, и перезагрузка Webmin. + +### Основные функции и методы + +#### `main()` + +Основная функция, которая запускает диалоговый интерфейс для выбора базы данных и выполнения соответствующих установок. + +#### `is_installed(package_name: str) -> bool` + +Проверяет, установлен ли указанный пакет. + +**Параметры:** + +- `package_name`: Название пакета для проверки. + +**Возвращает:** + +- `True`, если пакет установлен, иначе `False`. + +#### `run_command_with_log(dialog: Dialog, command: list[str], title: str) -> bool` + +Запускает указанную команду и отображает лог выполнения в диалоговом окне. + +**Параметры:** + +- `dialog`: Экземпляр диалогового окна для отображения логов. +- `command`: Список команд и параметров для выполнения. +- `title`: Заголовок диалогового окна. + +**Возвращает:** + +- `True`, если команда выполнена успешно, иначе `False`. + +#### `install_package(dialog: Dialog, package_name: str)` + +Устанавливает указанный пакет и отображает статус установки в диалоговом окне. + +**Параметры:** + +- `dialog`: Экземпляр диалогового окна. +- `package_name`: Название пакета для установки. + +#### `install_postgresql(dialog: Dialog)` + +Устанавливает PostgreSQL и сопутствующие пакеты. + +**Параметры:** + +- `dialog`: Экземпляр диалогового окна. + +#### `install_mariadb(dialog: Dialog)` + +Устанавливает MariaDB и `python3-harlequin`. + +**Параметры:** + +- `dialog`: Экземпляр диалогового окна. + +#### `install_webmin(dialog: Dialog)` + +Устанавливает Webmin и выполняет дополнительные настройки, такие как разрешение порта в ufw, установка модулей через `cpanminus`, и перезагрузка Webmin. + +**Параметры:** + +- `dialog`: Экземпляр диалогового окна. \ No newline at end of file diff --git a/gcm-diagnose.log b/gcm-diagnose.log new file mode 100644 index 0000000..de2350e --- /dev/null +++ b/gcm-diagnose.log @@ -0,0 +1,217 @@ +Diagnose log at 2024-08-21T06:08:40Z + +AppPath: /usr/local/bin/git-credential-manager +InstallDir: /usr/local/share/gcm-core/ +Version: 2.5.1+90d5f897f164a8fe07d2aa3bf7db439b0b4e03ea + +------------ +Diagnostic: Environment +Skipped: False +Success: True +Exception: None +Log: +OSType: Linux +OSVersion: Debian GNU/Linux 12 (bookworm) +Reading environment variables... OK + Variables: +XDG_DATA_DIRS=/usr/share/xfce4:/home/rzzx/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share +XDG_CONFIG_DIRS=/etc/xdg +XDG_SESSION_TYPE=x11 +XDG_SESSION_DESKTOP=xfce +SSH_AGENT_PID=2477 +DESKTOP_SESSION=xfce +VSCODE_GIT_ASKPASS_NODE=/usr/share/codium/codium +LC_IDENTIFICATION=ru_RU.UTF-8 +LC_MEASUREMENT=ru_RU.UTF-8 +SHELL=/usr/bin/zsh +TERM_PROGRAM_VERSION=1.91.1 +CHROME_DESKTOP=codium-url-handler.desktop +XDG_SEAT=seat0 +QT_ACCESSIBILITY=1 +NVM_CD_FLAGS=-q +GTK_MODULES=gail:atk-bridge +PWD=/home/rzzx/dev/osg_git/osgos_postinstall +XDG_SESSION_ID=2 +ZSH=/home/rzzx/.oh-my-zsh +TERM_PROGRAM=vscode +XDG_GREETER_DATA_DIR=/var/lib/lightdm/data/rzzx +VIRTUAL_ENV=/home/rzzx/dev/osg_git/osgos_postinstall/.venv +DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus +LANG=ru_RU.UTF-8 +poetry_active=1 +LC_MONETARY=ru_RU.UTF-8 +USER_ZDOTDIR=/home/rzzx +GOBIN=/home/rzzx/go/bin +GDMSESSION=xfce +VSCODE_NONCE=1ab35f99-5009-470f-bf62-127196eacfea +XDG_CURRENT_DESKTOP=XFCE +VSCODE_GIT_ASKPASS_EXTRA_ARGS= +_P9K_TTY=/dev/pts/7 +poetry_dir=/home/rzzx/dev/osg_git/osgos_postinstall +CLUTTER_IM_MODULE=ibus +XDG_MENU_PREFIX=xfce- +XDG_SESSION_CLASS=user +XDG_VTNR=7 +PATH=/home/rzzx/.nvm/versions/node/v20.16.0/bin:/home/rzzx/dev/osg_git/osgos_postinstall/.venv/bin:/home/rzzx/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/home/rzzx/.local/bin:/home/rzzx/.spoof-dpi/bin:/usr/local/go/bin:/home/rzzx/go:/usr/local/go/bin +XMODIFIERS=@im=ibus +LC_TIME=ru_RU.UTF-8 +NVM_BIN=/home/rzzx/.nvm/versions/node/v20.16.0/bin +QT_IM_MODULE=ibus +DIRHISTORY_SIZE=30 +LC_PAPER=ru_RU.UTF-8 +NVM_INC=/home/rzzx/.nvm/versions/node/v20.16.0/include/node +HOME=/home/rzzx +XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0 +VSCODE_GIT_IPC_HANDLE=/run/user/1000/vscode-git-1ae20f14c4.sock +ZDOTDIR=/home/rzzx +_P9K_SSH_TTY=/dev/pts/7 +SESSION_MANAGER=local/blackview:@/tmp/.ICE-unix/2331,unix/blackview:/tmp/.ICE-unix/2331 +XDG_RUNTIME_DIR=/run/user/1000 +LC_NUMERIC=ru_RU.UTF-8 +ORIGINAL_XDG_CURRENT_DESKTOP=XFCE +SHLVL=1 +P9K_SSH=0 +PS1=(postinstall-py3.11) %m%# +VIRTUAL_ENV_PROMPT=postinstall-py3.11 +GDK_BACKEND=x11 +LC_NAME=ru_RU.UTF-8 +LSCOLORS=Gxfxcxdxbxegedabagacad +GIT_TRACE2_PARENT_SID=f245dd56-049b-498a-ae4c-d772ad12f9c5 +LC_TELEPHONE=ru_RU.UTF-8 +XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0 +_=/usr/local/bin/git-credential-manager +LESS=-R +SSH_AUTH_SOCK=/tmp/ssh-XXXXXXsJ1U3j/agent.2331 +PAGER=less +LC_ADDRESS=ru_RU.UTF-8 +USER=rzzx +COLORTERM=truecolor +GTK3_MODULES=xapp-gtk3-module +LOGNAME=rzzx +NVM_DIR=/home/rzzx/.nvm +DISPLAY=:0.0 +VSCODE_GIT_ASKPASS_MAIN=/usr/share/codium/resources/app/extensions/git/dist/askpass-main.js +TERM=xterm-256color +P9K_TTY=old +GTK_IM_MODULE=ibus +XAUTHORITY=/home/rzzx/.Xauthority +GIT_ASKPASS=/usr/share/codium/resources/app/extensions/git/dist/askpass.sh +VSCODE_INJECTION=1 +EDITOR=nvim +GOPATH=/home/rzzx/go +LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.swp=00;90:*.tmp=00;90:*.dpkg-dist=00;90:*.dpkg-old=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90: +OLDPWD=/home/rzzx/dev/osg_git/osgos_postinstall +GOROOT=/usr/local/go + + +------------ +Diagnostic: File system +Skipped: False +Success: True +Exception: None +Log: +Temporary directory is '/tmp/'... +Checking basic file I/O... +Writing to temporary file '/tmp/d75d4a1ebcd28538294e82a2'... OK +Reading from temporary file '/tmp/d75d4a1ebcd28538294e82a2'... OK +Deleting temporary file '/tmp/d75d4a1ebcd28538294e82a2'... OK +Testing IFileSystem instance... +UserHomePath: /home/rzzx +UserDataDirectoryPath: /home/rzzx/.gcm +GetCurrentDirectory(): /home/rzzx/dev/osg_git/osgos_postinstall + +------------ +Diagnostic: Networking +Skipped: False +Success: True +Exception: None +Log: +Checking networking and HTTP stack... +Creating HTTP client... OK +IsNetworkAvailable: True +Sending HEAD request to http://example.com...Sending HEAD request to https://example.com... OK + OK +Acquiring free TCP port... OK +Testing local HTTP loopback connections... +Creating new HTTP listener for http://localhost:40751/... OK +Waiting for loopback connection... OK +Writing response... OK +Waiting for response data... OK +Loopback connection data OK + +------------ +Diagnostic: Git +Skipped: False +Success: True +Exception: None +Log: +Getting Git version... OK +Git version is '2.39.2' +Locating current repository...Git repository at '/home/rzzx/dev/osg_git/osgos_postinstall/.git' + OK +Listing all Git configuration... OK +Git configuration: +file:/home/rzzx/.gitconfig http.sslverify=false +file:/home/rzzx/.gitconfig user.name=ravonzz +file:/home/rzzx/.gitconfig user.email=vovash174@gmail.com +file:/home/rzzx/.gitconfig credential.helper=/usr/bin/git-credential-manager +file:/home/rzzx/.gitconfig credential.helper= +file:/home/rzzx/.gitconfig credential.helper=/usr/local/bin/git-credential-manager +file:/home/rzzx/.gitconfig credential.credentialstore=cache +file:/home/rzzx/.gitconfig credential.http://git.texnico.ru:3000.provider=generic +file:/home/rzzx/.gitconfig credential.https://dev.azure.com.usehttppath=true +file:/home/rzzx/.gitconfig credential.https://git.gossys.ru.provider=generic +file:.git/config core.repositoryformatversion=0 +file:.git/config core.filemode=true +file:.git/config core.bare=false +file:.git/config core.logallrefupdates=true +file:.git/config remote.origin.url=https://git.gossys.ru/merelden/osgos_postinstall.git +file:.git/config remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* +file:.git/config branch.master.remote=origin +file:.git/config branch.master.merge=refs/heads/master +file:.git/config branch.master.vscode-merge-base=origin/master +file:.git/config branch.main.remote=origin +file:.git/config branch.main.merge=refs/heads/main +file:.git/config branch.main.vscode-merge-base=origin/master + + +------------ +Diagnostic: Credential storage +Skipped: False +Success: True +Exception: None +Log: +ICredentialStore instance is of type: CredentialStore +Writing test credential... OK +Reading test credential... OK +Deleting test credential... OK + +------------ +Diagnostic: Microsoft authentication (AAD/MSA) +Skipped: False +Success: True +Exception: None +Log: +Broker is not enabled. +Flow type is: Auto +Gathering MSAL token cache data... OK +CacheDirectory: /home/rzzx/.local/.IdentityService +CacheFileName: msal.cache +CacheFilePath: /home/rzzx/.local/.IdentityService/msal.cache +KeyringCollection: +KeyringSchemaName: +KeyringSecretLabel: +KeyringAttribute1: (,) +KeyringAttribute2: (,) +Creating cache helper... OK +Verifying MSAL token cache persistence... OK + +------------ +Diagnostic: GitHub API +Skipped: False +Success: True +Exception: None +Log: +Using 'https://github.com/' as API target. +Querying '/meta' endpoint... OK + diff --git a/linkify_it_py-2.0.3-py3-none-any.whl b/linkify_it_py-2.0.3-py3-none-any.whl new file mode 100644 index 0000000..171b7d6 Binary files /dev/null and b/linkify_it_py-2.0.3-py3-none-any.whl differ diff --git a/markdown_it_py-3.0.0-py3-none-any.whl b/markdown_it_py-3.0.0-py3-none-any.whl new file mode 100644 index 0000000..2691698 Binary files /dev/null and b/markdown_it_py-3.0.0-py3-none-any.whl differ diff --git a/mdit_py_plugins-0.4.1-py3-none-any.whl b/mdit_py_plugins-0.4.1-py3-none-any.whl new file mode 100644 index 0000000..6f0c076 Binary files /dev/null and b/mdit_py_plugins-0.4.1-py3-none-any.whl differ diff --git a/mdurl-0.1.2-py3-none-any.whl b/mdurl-0.1.2-py3-none-any.whl new file mode 100644 index 0000000..6b8b6ab Binary files /dev/null and b/mdurl-0.1.2-py3-none-any.whl differ diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..cc6e6f6 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,17 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "pythondialog" +version = "3.5.3" +description = "A Python interface to the UNIX dialog utility and mostly-compatible programs" +optional = false +python-versions = ">=3" +files = [ + {file = "pythondialog-3.5.3-py3-none-any.whl", hash = "sha256:b9ac084a1ba5db75242ef623ea6a770f0e74aa634869151be15a2d1c50daaab2"}, + {file = "pythondialog-3.5.3.tar.gz", hash = "sha256:b2a34a8af0a6625ccbdf45cd343b854fc6c1a85231dadc80b8805db836756323"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "5b4a26c3e4feb0d37b6e45b934576e0be565c1a0e6e5b9ff9b0e83fff2b5a8fa" diff --git a/postinstall-0.8.51.tar.gz b/postinstall-0.8.51.tar.gz new file mode 100644 index 0000000..fadb8b6 Binary files /dev/null and b/postinstall-0.8.51.tar.gz differ diff --git a/postinstall-0.8.6.tar.gz b/postinstall-0.8.6.tar.gz new file mode 100644 index 0000000..1e0acd4 Binary files /dev/null and b/postinstall-0.8.6.tar.gz differ diff --git a/postinstall-0.9.9.tar.gz b/postinstall-0.9.9.tar.gz new file mode 100644 index 0000000..8531f92 Binary files /dev/null and b/postinstall-0.9.9.tar.gz differ diff --git a/postinstall-1.0.0.tar.gz b/postinstall-1.0.0.tar.gz new file mode 100644 index 0000000..d6290bf Binary files /dev/null and b/postinstall-1.0.0.tar.gz differ diff --git a/postinstall-1.0.1.tar.gz b/postinstall-1.0.1.tar.gz new file mode 100644 index 0000000..1b3cbbe Binary files /dev/null and b/postinstall-1.0.1.tar.gz differ diff --git a/postinstall-1.0.2.tar.gz b/postinstall-1.0.2.tar.gz new file mode 100644 index 0000000..bca8918 Binary files /dev/null and b/postinstall-1.0.2.tar.gz differ diff --git a/postinstall-1.0.3.tar.gz b/postinstall-1.0.3.tar.gz new file mode 100644 index 0000000..ae056eb Binary files /dev/null and b/postinstall-1.0.3.tar.gz differ diff --git a/postinstall-1.1.0.tar.gz b/postinstall-1.1.0.tar.gz new file mode 100644 index 0000000..0e1e543 Binary files /dev/null and b/postinstall-1.1.0.tar.gz differ diff --git a/postinstall-1.2.0.tar.gz b/postinstall-1.2.0.tar.gz new file mode 100644 index 0000000..1be0c26 Binary files /dev/null and b/postinstall-1.2.0.tar.gz differ diff --git a/postinstall-1.3.0.tar.gz b/postinstall-1.3.0.tar.gz new file mode 100644 index 0000000..ea06214 Binary files /dev/null and b/postinstall-1.3.0.tar.gz differ diff --git a/postinstall-1.4.0.tar.gz b/postinstall-1.4.0.tar.gz new file mode 100644 index 0000000..4c27ad5 Binary files /dev/null and b/postinstall-1.4.0.tar.gz differ diff --git a/postinstall-1.5.0.tar.gz b/postinstall-1.5.0.tar.gz new file mode 100644 index 0000000..ab4de09 Binary files /dev/null and b/postinstall-1.5.0.tar.gz differ diff --git a/postinstall.egg-info/PKG-INFO b/postinstall.egg-info/PKG-INFO new file mode 100644 index 0000000..27af7d6 --- /dev/null +++ b/postinstall.egg-info/PKG-INFO @@ -0,0 +1,243 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 1.5.0 +Summary: A Dialog TUI application for postinstall system configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent :: Linux +Requires-Python: >=3.11 +Description-Content-Type: text/markdown +Requires-Dist: pythondialog<4.0.0,>=3.5.3 + +## Документация Postinstall + +Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон. +### **1. Сетевые настройки** + +Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство. + +#### **1.1 Выбор сетевого интерфейса** + +Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки. + +![Номер лицензии](screenshots/network_interface.png) + + +#### **1.2 Настройка DHCP** + +Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`. + +#### **1.3 Настройка статического IP** + +Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса. + +#### **1.4 Настройка CIDR** + +Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети. + +![Номер лицензии](screenshots/choose_settings.png) + + +#### **1.5 Сохранение и отмена изменений** + +Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений". + +--- + +### **2. Обновление системы** + +Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений. + +#### **2.1 Обновление списков пакетов** + +Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев. + +#### **2.2 Установка обновлений** + +После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений. + +![Номер лицензии](screenshots/update.png) + + +--- + +### **3. Добавить пользователя** + +В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа. + +#### **3.1 Создание нового пользователя** + +Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников. + +![Номер лицензии](screenshots/add_user.png) + + +#### **3.2 Установка пароля** + +После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных. + +![Номер лицензии](screenshots/user_password.png) + + +#### **3.3 Назначение прав администратора** + +При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи. + +![Номер лицензии](screenshots/user_admin.png) + + +--- + +### **4. Установка программного обеспечения** + +Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения. + +#### **4.1 Выбор и установка пакетов** + +В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели. + +![Номер лицензии](screenshots/install_package.png) + + +--- + +### **5. Настройки SSH** + +SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию. + +#### **5.1 Включение/отключение SSH** + +Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ. + +#### **5.2 Управление ключами SSH** + +Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак. + +#### **5.3 Изменение порта SSH** + +Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`. + +![Номер лицензии](screenshots/ssh.png) + + +--- + +### **6. Изменить доменное имя ПК** + +В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети. + +#### **6.1 Изменение доменного имени** + +В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`. + +![Номер лицензии](screenshots/domain.png) + + +--- + +### **7. Выбор оболочки** + +Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы. + +#### **7.1 Установка оболочки по умолчанию** + +Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh. + +#### **7.2 Выбор оболочки** + +Вы можете выбрать оболочку из списка доступных в системе. + +![Номер лицензии](screenshots/user_terminal.png) + + + +![Номер лицензии](screenshots/choose_terminal.png) + + +--- + +### **8. Автозапуск при следующем запуске** + +Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы. + +#### **8.1 Управление автозапуском Postinstall** + +Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы. + +--- + +### **9. Настройка базы данных** + +Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin. + +#### **9.1 Установка и настройка PostgreSQL** + +Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных. + +#### **9.2 Логирование выполняемых действий** + +Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации. + +![Номер лицензии](screenshots/db_installing.png) + + +![Номер лицензии](screenshots/log_db.png) + + +![Номер лицензии](screenshots/log_install_db.png) + +#### **9.2 Установка дополнительных модулей** + +В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных. + +![Номер лицензии](screenshots/webmin.png) + + + + +--- + +### **10. Настройка Docker** + +Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации. + +![Номер лицензии](screenshots/install_docker.png) + + +![Номер лицензии](screenshots/progressbar_docker.png) + + +#### **10.1 Установка дополнительных модулей Docker** + +После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо. + +![Номер лицензии](screenshots/docker_swarm.png) + + +![Номер лицензии](screenshots/install_portainer.png) + + +#### **10.2 Автоматическая настройка модулей** +При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса. + +![Номер лицензии](screenshots/password_portainer.png) + + +![Номер лицензии](screenshots/port_portainer.png) + + +![Номер лицензии](screenshots/ip_portainer.png) + + +--- +### **11. Откат последних изменений** + +Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель. + +--- + +### **12. Сохранить и выйти** + +Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения. diff --git a/postinstall.egg-info/SOURCES.txt b/postinstall.egg-info/SOURCES.txt new file mode 100644 index 0000000..372b373 --- /dev/null +++ b/postinstall.egg-info/SOURCES.txt @@ -0,0 +1,36 @@ +MANIFEST.in +README.md +pyproject.toml +setup.py +deb/0002-postinstall.sh +deb/postinst +deb/postinstall-autostart.desktop +deb/postinstall.desktop +deb/postinstall.service +deb/postinstall_start.sh +deb/python3-textual_0.1.13-1_all.deb +deb/textual-0.72.0-py3-none-any.whl +postinstall/__init__.py +postinstall/__main__.py +postinstall/logger.py +postinstall/root.py +postinstall.egg-info/PKG-INFO +postinstall.egg-info/SOURCES.txt +postinstall.egg-info/dependency_links.txt +postinstall.egg-info/entry_points.txt +postinstall.egg-info/requires.txt +postinstall.egg-info/top_level.txt +postinstall/option/__init__.py +postinstall/option/add_user.py +postinstall/option/autostart.py +postinstall/option/database.py +postinstall/option/docker.py +postinstall/option/hostname.py +postinstall/option/install_packages.py +postinstall/option/network.py +postinstall/option/shell.py +postinstall/option/ssh.py +postinstall/option/update.py +tests/__init__.py +tests/docker_config.py +tests/test_docker.py \ No newline at end of file diff --git a/postinstall.egg-info/dependency_links.txt b/postinstall.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/postinstall.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/postinstall.egg-info/entry_points.txt b/postinstall.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/postinstall.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/postinstall.egg-info/requires.txt b/postinstall.egg-info/requires.txt new file mode 100644 index 0000000..f6c5b61 --- /dev/null +++ b/postinstall.egg-info/requires.txt @@ -0,0 +1 @@ +pythondialog<4.0.0,>=3.5.3 diff --git a/postinstall.egg-info/top_level.txt b/postinstall.egg-info/top_level.txt new file mode 100644 index 0000000..6503a29 --- /dev/null +++ b/postinstall.egg-info/top_level.txt @@ -0,0 +1,2 @@ +postinstall +tests diff --git a/postinstall/__init__.py b/postinstall/__init__.py new file mode 100644 index 0000000..b76fb93 --- /dev/null +++ b/postinstall/__init__.py @@ -0,0 +1,395 @@ +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() diff --git a/postinstall/__main__.py b/postinstall/__main__.py new file mode 100644 index 0000000..25f0dba --- /dev/null +++ b/postinstall/__main__.py @@ -0,0 +1,3 @@ +import postinstall + +postinstall.main() \ No newline at end of file diff --git a/postinstall/__pycache__/__init__.cpython-311.pyc b/postinstall/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f306922 Binary files /dev/null and b/postinstall/__pycache__/__init__.cpython-311.pyc differ diff --git a/postinstall/__pycache__/__main__.cpython-311.pyc b/postinstall/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..b7e1e3c Binary files /dev/null and b/postinstall/__pycache__/__main__.cpython-311.pyc differ diff --git a/postinstall/__pycache__/common.cpython-311.pyc b/postinstall/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000..5846cad Binary files /dev/null and b/postinstall/__pycache__/common.cpython-311.pyc differ diff --git a/postinstall/__pycache__/logger.cpython-311.pyc b/postinstall/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000..712d03a Binary files /dev/null and b/postinstall/__pycache__/logger.cpython-311.pyc differ diff --git a/postinstall/__pycache__/main.cpython-311.pyc b/postinstall/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..8b9f65d Binary files /dev/null and b/postinstall/__pycache__/main.cpython-311.pyc differ diff --git a/postinstall/__pycache__/root.cpython-311.pyc b/postinstall/__pycache__/root.cpython-311.pyc new file mode 100644 index 0000000..b5bfac6 Binary files /dev/null and b/postinstall/__pycache__/root.cpython-311.pyc differ diff --git a/postinstall/logger.py b/postinstall/logger.py new file mode 100644 index 0000000..f162b07 --- /dev/null +++ b/postinstall/logger.py @@ -0,0 +1,25 @@ +import logging +import getpass + + +def setup_logger(): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s") + + file_handler = logging.FileHandler("/var/log/postinstall.log") + file_handler.setFormatter(formatter) + + class UserFilter(logging.Filter): + def filter(self, record): + record.user = getpass.getuser() + return True + + file_handler.addFilter(UserFilter()) + logger.addHandler(file_handler) + + return logger + + +logger = setup_logger() diff --git a/postinstall/option/__init__.py b/postinstall/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/postinstall/option/__pycache__/__init__.cpython-311.pyc b/postinstall/option/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0e11324 Binary files /dev/null and b/postinstall/option/__pycache__/__init__.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/add_user.cpython-311.pyc b/postinstall/option/__pycache__/add_user.cpython-311.pyc new file mode 100644 index 0000000..b16b82d Binary files /dev/null and b/postinstall/option/__pycache__/add_user.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/database.cpython-311.pyc b/postinstall/option/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..b1b3618 Binary files /dev/null and b/postinstall/option/__pycache__/database.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/docker.cpython-311.pyc b/postinstall/option/__pycache__/docker.cpython-311.pyc new file mode 100644 index 0000000..12ecac5 Binary files /dev/null and b/postinstall/option/__pycache__/docker.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/hostname.cpython-311.pyc b/postinstall/option/__pycache__/hostname.cpython-311.pyc new file mode 100644 index 0000000..18a99c1 Binary files /dev/null and b/postinstall/option/__pycache__/hostname.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/install_packages.cpython-311.pyc b/postinstall/option/__pycache__/install_packages.cpython-311.pyc new file mode 100644 index 0000000..ed66611 Binary files /dev/null and b/postinstall/option/__pycache__/install_packages.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/network.cpython-311.pyc b/postinstall/option/__pycache__/network.cpython-311.pyc new file mode 100644 index 0000000..d23da49 Binary files /dev/null and b/postinstall/option/__pycache__/network.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/shell.cpython-311.pyc b/postinstall/option/__pycache__/shell.cpython-311.pyc new file mode 100644 index 0000000..9ef4686 Binary files /dev/null and b/postinstall/option/__pycache__/shell.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/ssh.cpython-311.pyc b/postinstall/option/__pycache__/ssh.cpython-311.pyc new file mode 100644 index 0000000..241ddcc Binary files /dev/null and b/postinstall/option/__pycache__/ssh.cpython-311.pyc differ diff --git a/postinstall/option/__pycache__/update.cpython-311.pyc b/postinstall/option/__pycache__/update.cpython-311.pyc new file mode 100644 index 0000000..cd12650 Binary files /dev/null and b/postinstall/option/__pycache__/update.cpython-311.pyc differ diff --git a/postinstall/option/add_user.py b/postinstall/option/add_user.py new file mode 100644 index 0000000..563c09e --- /dev/null +++ b/postinstall/option/add_user.py @@ -0,0 +1,132 @@ +import subprocess +from dialog import Dialog +import pwd +from postinstall.logger import logger + +logging = logger + + +def get_password_with_toggle(d, title): + """Функция для ввода пароля с возможностью показать/скрыть пароль""" + password_hidden = True + password = "" + + # Фиксированная ширина окна + window_width = 60 + + while True: + if password_hidden: + # Используем insecure=True для показа скрытого пароля снежинками + code, entered_password = d.passwordbox( + title, insecure=True, init=password, width=window_width, + extra_button=True, extra_label="Показать пароль" + ) + else: + # Используем insecure=False для показа пароля открытым текстом + code, entered_password = d.inputbox( + title, init=password, width=window_width, + extra_button=True, extra_label="Скрыть пароль" + ) + + if code == d.EXTRA: + password_hidden = not password_hidden + password = entered_password # Сохраняем введённый пароль + continue # Перезапускаем окно с новым состоянием + + if code == d.CANCEL: + return None + + # Обновляем сохранённый пароль + password = entered_password + return password + + +def add_user(): + d = Dialog(dialog="dialog") + + fields = [ + ("Имя пользователя", 1, 1, "", 1, 20, 30, 0), + ("Полное имя", 2, 1, "", 2, 20, 30, 0), + ("Мобильный телефон", 3, 1, "", 3, 20, 30, 0), + ("Городской телефон", 4, 1, "", 4, 20, 30, 0), + ("Кабинет", 5, 1, "", 5, 20, 30, 0), + ] + + code, values = d.form("Введите данные нового пользователя:", fields) + + if code != d.OK: + return # Пользователь отменил операцию, выходим из функции + + username, fullname, mobile, city_phone, cabinet = values + + # Проверка существования пользователя + try: + pwd.getpwnam(username) + d.msgbox(f"Пользователь {username} уже существует.") + return + except KeyError: + pass # Пользователь не существует, продолжаем + + # Запрос пароля с возможностью его показать/скрыть + password = get_password_with_toggle(d, "Введите пароль для нового пользователя") + if not password: + return # Пользователь отменил операцию, выходим из функции + + # Запрос подтверждения пароля с возможностью его показать/скрыть + confirm_password = get_password_with_toggle(d, "Подтвердите пароль") + if not confirm_password: + return # Пользователь отменил операцию, выходим из функции + + if password != confirm_password: + d.msgbox("Пароли не совпадают. Попробуйте снова.") + return + + # Запрос прав администратора + code = d.yesno("Сделать пользователя администратором?") + is_admin = code == d.OK + + try: + # Создание пользователя + subprocess.run( + [ + "sudo", + "useradd", + "-m", + "-c", + f"{fullname},{cabinet},{city_phone},{mobile}", + "-s", + "/bin/bash", + username, + ], + check=True, + ) + + # Установка пароля + process = subprocess.Popen( + ["sudo", "passwd", username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate(input=f"{password}\n{password}\n".encode()) + + # Добавление в группу sudo, если пользователь - администратор + if is_admin: + subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True) + + logging.info( + f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}" + ) + + d.msgbox(f"Пользователь {username} успешно добавлен.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при добавлении пользователя: {e}") + d.msgbox(f"Ошибка при добавлении пользователя: {e}") + + +def run_add_user(): + add_user() + + +if __name__ == "__main__": + run_add_user() diff --git a/postinstall/option/autostart.py b/postinstall/option/autostart.py new file mode 100644 index 0000000..a43ff95 --- /dev/null +++ b/postinstall/option/autostart.py @@ -0,0 +1,59 @@ +import os +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + +AUTOSTART_FILE = "/usr/share/postinstall/.norestart" + + +class AutoStartScreen: + def __init__(self): + self.d = Dialog(dialog="dialog") + + def check_auto_start(self) -> bool: + return not os.path.exists(AUTOSTART_FILE) + + def set_auto_start(self, enable: bool) -> None: + try: + if enable: + try: + if os.path.exists(AUTOSTART_FILE): + os.remove(AUTOSTART_FILE) + + except Exception as e: + logging.error(f"Ошибка при включении автозапуска: {e}") + else: + with open(AUTOSTART_FILE, "w") as f: + f.write("# Это файл создан для отключения автозапуска\n") + except Exception as e: + logging.error(f"Ошибка при изменении автозапуска: {e}") + + def show_screen(self): + auto_start_enabled = self.check_auto_start() + + code, tags = self.d.checklist( + "Выберите действие", + choices=[ + ("autostart", "Автозапуск при следующем запуске", auto_start_enabled) + ], + title="Настройки автозапуска", + backtitle="Postinstall Configuration", + ok_label="Применить", + cancel_label="Назад", + ) + + if code == self.d.OK: + self.set_auto_start("autostart" in tags) + self.d.msgbox("Изменения сохранены.") + else: + logging.info("Изменения отменены пользователем.") + + +def main(): + auto_start_screen = AutoStartScreen() + auto_start_screen.show_screen() + + +if __name__ == "__main__": + main() diff --git a/postinstall/option/database.py b/postinstall/option/database.py new file mode 100644 index 0000000..9fb9a66 --- /dev/null +++ b/postinstall/option/database.py @@ -0,0 +1,157 @@ +import subprocess +from dialog import Dialog + + +def main(): + dialog = Dialog(dialog="dialog") + + # Диалоговое окно с выбором базы данных + code, choice = dialog.menu( + "Выберите базу данных для установки:", + choices=[ + ("PostgreSQL", "Установить PostgreSQL"), + ("MariaDB", "Установить MariaDB"), + ], + ) + + if code == dialog.OK: + dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён") + if choice == "PostgreSQL": + install_postgresql(dialog) + elif choice == "MariaDB": + install_mariadb(dialog) + + # Независимо от выбранной базы данных, предложить установку Webmin + if ( + dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет") + == dialog.OK + ): + install_webmin(dialog) + else: + dialog.msgbox("Выбор базы данных отменён", title="Отмена") + + +def is_installed(package_name): + """Проверяет, установлен ли пакет""" + try: + result = subprocess.run( + ["dpkg-query", "-W", "-f=${Status}", package_name], + check=True, + capture_output=True, + text=True, + ) + return "install ok installed" in result.stdout + except subprocess.CalledProcessError: + return False + + +def run_command_with_log(dialog, command, title): + """Запуск команды с выводом лога в диалоговое окно""" + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + log_output = [] + for line in iter(process.stdout.readline, ""): + log_output.append(line.strip()) + + stderr_output = process.stderr.read().strip() + if stderr_output: + log_output.append(stderr_output) + + process.stdout.close() + process.stderr.close() + process.wait() + + dialog.msgbox("\n".join(log_output), title=title, height=20, width=60) + return process.returncode == 0 + + +def install_package(dialog, package_name): + """Устанавливает указанный пакет с выводом лога""" + dialog.infobox(f"Начало установки {package_name}...") + + if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"): + dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.") + return + + if not run_command_with_log( + dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}" + ): + dialog.msgbox(f"Ошибка при установке {package_name}.") + return + + dialog.msgbox(f"{package_name} успешно установлен.") + + +def install_postgresql(dialog): + """Установка PostgreSQL и Harlequin""" + if not is_installed("postgresql-16"): + install_package(dialog, "postgresql-16") + else: + dialog.msgbox("PostgreSQL уже установлен.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + if not is_installed("python3-harlequin-postgres"): + install_package(dialog, "python3-harlequin-postgres") + else: + dialog.msgbox("python3-harlequin-postgres уже установлен.") + + +def install_mariadb(dialog): + """Установка MariaDB""" + if not is_installed("mariadb-server"): + install_package(dialog, "mariadb-server") + else: + dialog.msgbox("MariaDB уже установлена.") + + if not is_installed("python3-harlequin"): + install_package(dialog, "python3-harlequin") + else: + dialog.msgbox("python3-harlequin уже установлен.") + + +def install_webmin(dialog): + """Установка Webmin и дополнительных пакетов""" + if not is_installed("webmin"): + install_package(dialog, "webmin") + + # Установка cpanminus + install_package(dialog, "postgresql-server-dev-16") + install_package(dialog, "cpanminus") + + run_command_with_log( + dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + dialog.msgbox("Порт 10000/tcp разрешен в ufw.") + + # Установка модулей DBI и DBD::Pg через cpanminus + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI" + ): + dialog.msgbox("Модуль DBI успешно установлен.") + if run_command_with_log( + dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg" + ): + dialog.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + dialog.msgbox("Webmin успешно перезагружен.") + else: + dialog.msgbox("Webmin уже установлен.") + + +if __name__ == "__main__": + main() diff --git a/postinstall/option/docker.py b/postinstall/option/docker.py new file mode 100644 index 0000000..4f373b3 --- /dev/null +++ b/postinstall/option/docker.py @@ -0,0 +1,533 @@ +import subprocess +import os +import re +import shlex +from abc import ABC, abstractmethod +from typing import Optional, List, Callable + +from dialog import Dialog +from postinstall.logger import logger + +import socket + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "anon_user" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class CommandExecutor: + @staticmethod + def run_command( + command: str, + shell: bool = False, + input_data: Optional[str] = None, + ignore_errors: List[int] = [], + ) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command if shell else shlex.split(command), + shell=shell, + check=True, + input=input_data, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as e: + if e.returncode not in ignore_errors: + logger.error(f"Ошибка выполнения команды: {e}") + raise + else: + logger.warning( + f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}" + ) + return e + + +class SystemChecker: + @staticmethod + def is_command_installed(command: str) -> bool: + try: + return CommandExecutor.run_command(f"which {command}").returncode == 0 + except subprocess.CalledProcessError: + return False + + +class ProgressGauge: + def __init__(self, dialog: Dialog, title: str, total_steps: int): + self.dialog = dialog + self.title = title + self.total_steps = total_steps + self.current_step = 0 + + def update(self, step: int, message: str): + self.current_step = step + percent = int((step / self.total_steps) * 100) + self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True) + + def __enter__(self): + self.dialog.gauge_start(self.title, percent=0) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.dialog.gauge_stop() + + +class DockerManager: + @staticmethod + def install(progress_callback: Callable[[int, str], None]): + logger.info("Установка Docker...") + progress_callback(1, "Обновление пакетов") + CommandExecutor.run_command("apt-get update") + progress_callback(2, "Установка Docker") + CommandExecutor.run_command("apt-get install -y docker.io") + progress_callback(3, "Установка Docker Compose") + CommandExecutor.run_command("apt-get install -y docker-compose") + progress_callback(4, "Выполнение входа в Docker registry") + DockerManager.login_to_registry() + progress_callback(5, "Docker успешно установлен") + logger.info("Docker успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Docker...") + progress_callback(1, "Удаление Docker и Docker Compose") + CommandExecutor.run_command("apt-get remove -y docker.io docker-compose") + progress_callback(2, "Очистка зависимостей") + CommandExecutor.run_command("apt-get autoremove -y") + progress_callback(3, "Удаление файлов Docker") + CommandExecutor.run_command("rm -rf /var/run/docker") + CommandExecutor.run_command("rm -rf /var/lib/docker") + progress_callback(4, "Docker успешно удален") + logger.info("Docker успешно удален") + + @staticmethod + def login_to_registry(): + logger.info("Выполняется вход в Docker registry...") + try: + result = CommandExecutor.run_command( + f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}", + shell=True, + ) + logger.info("Успешный вход в Docker registry") + logger.debug(f"Docker login output: {result.stdout}") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка входа в Docker registry: {e}") + logger.error(f"Stderr: {e.stderr}") + raise + + +class UFWManager: + @staticmethod + def add_rule(port: str): + rule_name = "portainer" + logger.info( + f"Добавление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено" + ) + + @staticmethod + def remove_rule(port: str): + rule_name = "portainer" + logger.info( + f"Удаление именованного правила UFW '{rule_name}' для порта {port}..." + ) + CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp") + CommandExecutor.run_command(f"sudo ufw app update {rule_name}") + logger.info( + f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено" + ) + + +class PortainerManager: + @staticmethod + def install( + port: str, password: str, ip: str, progress_callback: Callable[[int, str], None] + ): + logger.info("Установка Portainer...") + progress_callback(1, "Вход в Docker registry") + DockerManager.login_to_registry() + progress_callback(2, "Остановка предыдущей версии Portainer") + try: + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(3, "Удаление старых данных Portainer") + try: + CommandExecutor.run_command( + "docker volume ls -q -f name=portainer* | xargs -r docker volume rm", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + + progress_callback(4, "Установка apache2-utils для htpasswd") + if not SystemChecker.is_command_installed("htpasswd"): + CommandExecutor.run_command("apt-get install -y apache2-utils") + + progress_callback(5, "Создание конфигурации Portainer") + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + password_hash = PortainerManager._generate_password_hash(password) + PortainerManager._create_docker_compose_file(port, password_hash, ip) + + progress_callback(6, "Запуск Portainer") + CommandExecutor.run_command( + f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d" + ) + + # if ip != "127.0.0.1": + if True: + progress_callback(7, "Добавление правила UFW") + UFWManager.add_rule(port) + + progress_callback(8, "Portainer успешно установлен") + logger.info("Portainer успешно установлен") + + @staticmethod + def uninstall(progress_callback: Callable[[int, str], None]): + logger.info("Удаление Portainer...") + progress_callback(1, "Остановка Portainer") + try: + CommandExecutor.run_command( + f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes", + ignore_errors=[1], + ) + except subprocess.CalledProcessError: + pass + progress_callback(2, "Удаление файлов конфигурации") + CommandExecutor.run_command( + f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml" + ) + # try: + # with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f: + # config = f.read() + # port_ip_match = re.search(r'"(.+):(\d+):9000"', config) + # if port_ip_match: + # ip, port = port_ip_match.groups() + # # if True: + # # progress_callback(3, "Удаление правила UFW") + # # UFWManager.remove_rule(port) + # except FileNotFoundError: + # logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.") + + progress_callback(4, "Portainer успешно удален") + logger.info("Portainer успешно удален") + + @staticmethod + def _generate_password_hash(password: str) -> str: + return CommandExecutor.run_command( + f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'", + shell=True, + ).stdout.strip() + + @staticmethod + def _create_docker_compose_file(port: str, password_hash: str, ip: str): + docker_compose_content = f""" +version: '2' + +services: + portainer: + image: {REGISTRY_URL}/base_images/portainer:latest + ports: + - "{ip}:{port}:9000" + command: --admin-password '{password_hash}' + networks: + - local + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer-data:/data + environment: + - PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1 + deploy: + mode: global + +networks: + local: + driver: bridge + +volumes: + portainer-data: + """ + with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + +class InputValidator: + @staticmethod + def validate(value: str, pattern: str) -> bool: + return bool(re.match(pattern, value)) + + +class UIElement(ABC): + def __init__(self, dialog: Dialog): + self.dialog = dialog + + @abstractmethod + def show(self): + pass + + +class Menu(UIElement): + def show(self): + choices = [] + if not SystemChecker.is_command_installed("docker"): + choices.append(("1", "Установить Docker")) + else: + choices.append(("1", "Удалить Docker")) + + if SystemChecker.is_command_installed("docker"): + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + choices.append(("2", "Установить Portainer")) + else: + choices.append(("2", "Удалить Portainer")) + + code, choice = self.dialog.menu("Выберите действие:", choices=choices) + return code, choice + + +class YesNoUI(UIElement): + def show(self, text: str): + return self.dialog.yesno(text) == self.dialog.OK + + +class PasswordInput(UIElement): + def show(self): + while True: + code, password = self.dialog.passwordbox( + "Введите пароль для Portainer:", insecure=True + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(password, r"^.{12,}$"): + return password + self.dialog.msgbox( + "Пароль должен содержать минимум 12 символов.", width=50, height=10 + ) + + +class PortInput(UIElement): + def show(self): + while True: + code, port = self.dialog.inputbox( + "Введите порт для Portainer:", init="9000" + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"): + return port + self.dialog.msgbox( + "Некорректный порт. Введите число от 1 до 65535.", width=50, height=10 + ) + + +class IPInput(UIElement): + def get_default_interface_ip(self): + try: + # Выполнение команды для получения IP-адреса основного интерфейса + command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1" + result = subprocess.check_output(command, shell=True, text=True) + return result.strip() # Возвращаем IP-адрес без лишних символов + except subprocess.CalledProcessError: + return "127.0.0.1" # В случае ошибки вернём стандартный IP + + def show(self): + # Получаем IP-адрес основного сетевого интерфейса + default_ip = self.get_default_interface_ip() + + while True: + # Используем полученный IP-адрес как начальное значение + code, ip = self.dialog.inputbox( + "Введите IP адрес для Portainer:", init=default_ip + ) + if code == self.dialog.CANCEL: + return None + if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"): + return ip + self.dialog.msgbox( + "Некорректный IP адрес. Введите адрес в формате X.X.X.X", + width=50, + height=10, + ) + + +class NetworkInterfaceInput(UIElement): + def get_network_interfaces(self): + try: + # Get all network interfaces + interfaces = subprocess.check_output( + "ip -o link show", shell=True, text=True + ) + # Extract interface names and their IP addresses + interface_list = [] + for line in interfaces.split("\n"): + if line: + interface = line.split(":")[1].strip() + ip = self.get_ip_address(interface) + if ip: + interface_list.append((interface, ip)) + return interface_list + except subprocess.CalledProcessError: + return [] + + def get_ip_address(self, interface): + try: + ip = subprocess.check_output( + f"ip -o -4 addr list {interface}", shell=True, text=True + ) + return ip.split()[3].split("/")[0] + except subprocess.CalledProcessError: + return None + + def show(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.dialog.msgbox( + "Не удалось получить список сетевых интерфейсов.", width=50, height=10 + ) + return None + + choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces] + code, choice = self.dialog.menu( + "Выберите сетевой интерфейс для Docker Swarm:", choices=choices + ) + + if code == self.dialog.OK: + return choice + return None + + +class DockerConfigUI: + def __init__(self): + self.dialog = Dialog(dialog="dialog") + self.menu = Menu(self.dialog) + self.password_input = PasswordInput(self.dialog) + self.port_input = PortInput(self.dialog) + self.ip_input = IPInput(self.dialog) + self.network_interface_input = NetworkInterfaceInput(self.dialog) + + def run(self): + try: + while True: + code, choice = self.menu.show() + if code == self.dialog.OK: + if choice == "1": + self._handle_docker() + elif choice == "2": + self.handle_portainer() + elif choice == "3": + break + else: + break + except Exception as e: + logger.error(f"Произошла ошибка: {e}") + self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10) + + def _handle_docker(self): + if not SystemChecker.is_command_installed("docker"): + with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge: + DockerManager.install(gauge.update) + self.dialog.msgbox("Docker успешно установлен.", width=50, height=10) + logger.info("Docker успешно установлен.") + yes_no_ui = YesNoUI(self.dialog) + if yes_no_ui.show( + text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?" + ): + interface = self.network_interface_input.show() + if interface: + ip = interface.split("(")[1].split(")")[0] + if ip: + try: + CommandExecutor.run_command( + f"docker swarm init --advertise-addr {ip}" + ) + self.dialog.msgbox( + f"Docker Swarm успешно инициализирован на интерфейсе {interface}", + width=50, + height=10, + ) + except subprocess.CalledProcessError as e: + self.dialog.msgbox( + f"Ошибка при инициализации Docker Swarm: {str(e)}", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + f"Не удалось получить IP-адрес для интерфейса {interface}.", + width=50, + height=10, + ) + else: + self.dialog.msgbox( + "Инициализация Docker Swarm отменена.", width=50, height=10 + ) + + if yes_no_ui.show( + text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?" + ): + self.handle_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge: + PortainerManager.uninstall(gauge.update) + DockerManager.uninstall(gauge.update) + self.dialog.msgbox("Docker успешно удален.", width=50, height=10) + logger.info("Docker успешно удален.") + + def handle_portainer(self): + if not SystemChecker.is_command_installed("docker"): + self.dialog.msgbox( + "Docker не установлен. Пожалуйста, сначала установите Docker.", + width=50, + height=10, + ) + return + + if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"): + self._install_portainer() + else: + with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge: + PortainerManager.uninstall(gauge.update) + self.dialog.msgbox("Portainer успешно удален.", width=50, height=10) + logger.info("Portainer успешно удален.") + + def _install_portainer(self): + password = self.password_input.show() + if not password: + return + + port = self.port_input.show() + if not port: + return + + ip = self.ip_input.show() + if not ip: + return + + try: + with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge: + PortainerManager.install(port, password, ip, gauge.update) + self.dialog.msgbox( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль", + width=50, + height=10, + ) + logger.info( + f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль" + ) + except Exception as e: + self.dialog.msgbox( + f"Ошибка при установке Portainer: {str(e)}", width=50, height=10 + ) + + +def run(): + ui = DockerConfigUI() + ui.run() diff --git a/postinstall/option/hostname.py b/postinstall/option/hostname.py new file mode 100644 index 0000000..ef11e59 --- /dev/null +++ b/postinstall/option/hostname.py @@ -0,0 +1,76 @@ +import subprocess +from dialog import Dialog +import os + +HOSTNAME_FILE = "/etc/hostname" +HOSTS_FILE = "/etc/hosts" + + +def get_current_hostname(): + try: + with open(HOSTNAME_FILE, 'r') as f: + return f.read().strip() + except IOError as e: + print(f"Ошибка при чтении hostname: {e}") + return "localhost" + + +def change_hostname(): + d = Dialog(dialog="dialog") + current_hostname = get_current_hostname() + + code, new_hostname = d.inputbox( + f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:", + init=current_hostname + ) + + if code != d.OK: + return None + + if new_hostname == current_hostname: + d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.") + return None + + # Проверка валидности hostname + if not is_valid_hostname(new_hostname): + d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.") + return None + + # Подтверждение изменения + code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?") + if code != d.OK: + return None + + return new_hostname + + +def is_valid_hostname(hostname): + if len(hostname) > 63: + return False + if hostname[-1] == "-": + return False + allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") + return all(char in allowed for char in hostname) + + +def apply_hostname_change(new_hostname): + try: + subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True) + if os.path.exists(HOSTS_FILE): + subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True) + return True + except subprocess.CalledProcessError as e: + Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}") + return False + + +def run_change_hostname(): + return change_hostname() + + +if __name__ == "__main__": + new_hostname = run_change_hostname() + if new_hostname: + print(f"Новое доменное имя: {new_hostname}") + else: + print("Изменение доменного имени отменено.") \ No newline at end of file diff --git a/postinstall/option/install_packages.py b/postinstall/option/install_packages.py new file mode 100644 index 0000000..f66eccd --- /dev/null +++ b/postinstall/option/install_packages.py @@ -0,0 +1,127 @@ +import subprocess +from dialog import Dialog +import os +from postinstall.logger import logger + +logging = logger + +AVAILABLE_PACKAGES = [ + "neofetch", + "webmin", + "usermin", + "cups", + "wireguard", + "wg-cmd", +] + + +def check_sudo(): + return os.geteuid() == 0 + + +def run_command_with_log(dialog, command, message): + """Run a command and log output, showing a message box on success.""" + try: + subprocess.run(command, check=True, capture_output=True, text=True) + logging.info(f"{message}: успешно выполнено.") + return True + except subprocess.CalledProcessError as e: + logging.error(f"{message}: ошибка - {e}") + dialog.msgbox(f"{message}: ошибка.\n{e}") + return False + + +def install_packages(): + d = Dialog(dialog="dialog") + + if not check_sudo(): + d.msgbox("Для установки пакетов необходимы права суперпользователя.") + return + + # Создаем список пакетов для выбора + choices = [(package, "", False) for package in AVAILABLE_PACKAGES] + + # Показываем checklist для выбора пакетов + code, tags = d.checklist( + "Выберите пакеты для установки:", choices=choices, title="Установка программ" + ) + + if code != d.OK: + return + + if not tags: + d.msgbox("Вы не выбрали ни одного пакета для установки.") + return + + # Подтверждение установки + code = d.yesno( + f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?" + ) + if code != d.OK: + return + + # Создаем gauge для отображения прогресса + d.gauge_start("Установка пакетов...", percent=0) + + try: + # Обновление списка пакетов + d.gauge_update(10, "Обновление списка пакетов...") + subprocess.run(["apt", "update"], check=True, capture_output=True) + + logging.info(f"Установка пакетов: {', '.join(tags)}") + # Установка выбранных пакетов + d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...") + process = subprocess.Popen( + ["apt", "install", "-y"] + tags, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Чтение вывода процесса и обновление gauge + for i, line in enumerate(process.stdout): + d.gauge_update(20 + (i % 70), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, process.args, error) + + # Проверяем, если вебмин был выбран для установки + if "webmin" in tags: + d.gauge_update(90, "Настройка Webmin...") + # Установка cpanminus + run_command_with_log( + d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus" + ) + + # Разрешение порта 10000/tcp в ufw + if run_command_with_log( + d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall" + ): + d.msgbox("Порт 10000/tcp разрешен в ufw.") + + # # Установка модулей DBI и DBD::Pg через cpanminus + # if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"): + # d.msgbox("Модуль DBI успешно установлен.") + # if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"): + # d.msgbox("Модуль DBD::Pg успешно установлен.") + + # Перезагрузка Webmin + if run_command_with_log( + d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin" + ): + d.msgbox("Webmin успешно перезагружен.") + + d.gauge_update(100, "Установка завершена успешно!") + d.gauge_stop() + + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке пакетов: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при установке пакетов: {e}") + + +def run_install_packages(): + install_packages() diff --git a/postinstall/option/network.py b/postinstall/option/network.py new file mode 100644 index 0000000..332cbc0 --- /dev/null +++ b/postinstall/option/network.py @@ -0,0 +1,243 @@ +import subprocess +import ipaddress +from dialog import Dialog + + +class NetworkConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.temp_network_settings = {} + + def select_interface(self): + interfaces = self.get_network_interfaces() + if not interfaces: + self.d.msgbox("Нет доступных сетевых интерфейсов.") + return None + + code, tag = self.d.menu( + "Выберите интерфейс:", choices=[(iface, "") for iface in interfaces] + ) + + if code == self.d.OK: + self.d.msgbox(f"Выбран интерфейс: {tag}") + self.show_current_settings(tag) # Добавлено окно с текущими настройками + return tag + else: + self.d.msgbox("Интерфейс не выбран.") + return None + + @staticmethod + def get_network_interfaces(): + try: + result = subprocess.run( + ["ls", "/sys/class/net/"], capture_output=True, text=True, check=True + ) + interfaces = result.stdout.strip().split("\n") + return interfaces + except subprocess.CalledProcessError: + return [] + + def show_current_settings(self, interface): + try: + result = subprocess.run( + ["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True + ) + current_settings = result.stdout.strip() + + ip_address = None + if "inet " in current_settings: + ip_address = current_settings.split("inet ")[1].split("/")[0] + + if "dhcp" in current_settings: + config_type = "DHCP" + elif ip_address: + config_type = "Static" + else: + config_type = "Неизвестно" + + message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен" + + self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50) + except subprocess.CalledProcessError as e: + self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}") + + def configure_dhcp(self, interface): + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + if code == self.d.OK: + self.temp_network_settings[interface] = { + "type": "dhcp", + "dns": dns, + "interface": interface, + } + self.d.msgbox("DHCP настройка сохранена.") + else: + self.d.msgbox("DHCP настройка отменена.") + + def configure_static(self, interface): + code, ip = self.d.inputbox("Введите IP адрес:") + if code != self.d.OK: + return + code, mask = self.d.inputbox("Введите маску подсети:") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if ip and mask and gateway: + self.temp_network_settings[interface] = { + "type": "static", + "ip": ip, + "mask": mask, + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("Статические настройки сохранены.") + else: + self.d.msgbox("Некоторые параметры не заданы.") + + def configure_cidr(self, interface): + code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):") + if code != self.d.OK: + return + code, gateway = self.d.inputbox("Введите шлюз:") + if code != self.d.OK: + return + code, dns = self.d.inputbox("Введите DNS сервер (опционально):") + + if cidr: + try: + ip = ipaddress.ip_interface(cidr).ip + network = ipaddress.ip_network(cidr, strict=False) + mask = network.netmask + self.temp_network_settings[interface] = { + "type": "static", + "ip": str(ip), + "mask": str(mask), + "gateway": gateway, + "dns": dns, + "interface": interface, + } + self.d.msgbox("CIDR конфигурация сохранена.") + except ValueError as e: + self.d.msgbox(f"Неверный формат CIDR: {e}") + else: + self.d.msgbox("CIDR не задан.") + + def run(self): + while True: + interface = self.select_interface() + if not interface: + break + + code, tag = self.d.menu( + "Выберите действие для интерфейса:", + choices=[ + ("1", "Настроить DHCP"), + ("2", "Настроить статический IP"), + ("3", "Настроить CIDR"), + ("4", "Выбрать другой интерфейс"), + ("5", "Выход"), + ], + ) + + if code == self.d.OK: + if tag == "1": + self.configure_dhcp(interface) + elif tag == "2": + self.configure_static(interface) + elif tag == "3": + self.configure_cidr(interface) + elif tag == "4": + continue + elif tag == "5": + break + else: + break + + return self.temp_network_settings + + def save_and_apply_network_settings(self): + for interface, settings in self.temp_network_settings.items(): + try: + if settings["type"] == "dhcp": + self.save_dhcp(interface, settings) + elif settings["type"] == "static": + self.save_static(interface, settings) + if settings.get("dns"): + self.update_resolv_conf(settings["dns"]) + except Exception as e: + self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}") + return False + + try: + subprocess.run(["systemctl", "restart", "networking"], check=True) + self.d.msgbox("Сетевые настройки успешно применены.") + return True + except Exception as e: + self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}") + return False + + def save_dhcp(self, interface, settings): + interface_config = f""" +auto {interface} +iface {interface} inet dhcp +""" + self.update_interface_config(interface, interface_config) + + def save_static(self, interface, settings): + ip = settings["ip"] + mask = settings["mask"] + gateway = settings["gateway"] + interface_config = f""" +auto {interface} +iface {interface} inet static +address {ip} +netmask {mask} +""" + if gateway: + interface_config += f"gateway {gateway}\n" + self.update_interface_config(interface, interface_config) + + def update_interface_config(self, interface, interface_config): + try: + with open("/etc/network/interfaces", "r") as f: + lines = f.readlines() + + with open("/etc/network/interfaces", "w") as f: + skip_section = False + for line in lines: + if interface in line: + skip_section = True + continue + if skip_section and line.strip() == "": + skip_section = False + if not skip_section: + f.write(line) + f.write(interface_config.strip() + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}") + + def update_resolv_conf(self, dns): + if not dns: + return + + dns_entry = f"nameserver {dns}" + try: + with open("/etc/resolv.conf", "r") as f: + resolv_lines = f.readlines() + + if any(line.strip() == dns_entry for line in resolv_lines): + return + + with open("/etc/resolv.conf", "a") as f: + f.write(dns_entry + "\n") + except Exception as e: + self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}") + + +if __name__ == "__main__": + configurator = NetworkConfig() + configurator.run() diff --git a/postinstall/option/shell.py b/postinstall/option/shell.py new file mode 100644 index 0000000..69791f5 --- /dev/null +++ b/postinstall/option/shell.py @@ -0,0 +1,106 @@ +import subprocess +from dialog import Dialog +import pwd +import os +from postinstall.logger import logger + +logging = logger + + +def get_users(): + users = [] + try: + for user in pwd.getpwall(): + if user.pw_uid >= 1000 and user.pw_name != "nobody": + users.append(user.pw_name) + if "root" not in users: + users.append("root") + except Exception as e: + print(f"Ошибка при получении списка пользователей: {e}") + return users + + +def install_zsh(): + try: + subprocess.run(["apt", "update"], check=True) + subprocess.run(["apt", "install", "-y", "zsh"], check=True) + except subprocess.CalledProcessError as e: + print(f"Ошибка при установке zsh: {e}") + return False + return True + + +def change_shell(): + d = Dialog(dialog="dialog") + users = get_users() + + # Первый шаг - выбор опции с помощью radiolist + code, choice = d.radiolist( + "Выберите способ изменения оболочки:", + choices=[ + ("Для всех пользователей", "", False), + ("Выбрать конкретных пользователей", "", True) + ] + ) + + if code != d.OK: + return + + selected_users = [] + + if choice == "Для всех пользователей": + # Если выбрано "Для всех пользователей" + selected_users = users + elif choice == "Выбрать конкретных пользователей": + # Если выбран выбор конкретных пользователей, показываем checklist + code, selected_users = d.checklist( + "Выберите пользователей для изменения оболочки:", + choices=[(user, "", False) for user in users], + clear=True + ) + if code != d.OK or not selected_users: + return + + # Выбор оболочки + code, shell = d.menu( + "Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")] + ) + if code != d.OK: + return + + shell_path = "/bin/bash" if shell == "1" else "/bin/zsh" + + if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"): + logging.info("zsh не установлен. Начинаем установку...") + d.msgbox("zsh не установлен. Начинаем установку...") + if not install_zsh(): + d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.") + return + + # Применение изменений + if choice == "Для всех пользователей": + # Применение изменений для всех пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + d.msgbox("Оболочка изменена для всех существующих пользователей") + logging.info("Оболочка изменена для всех существующих пользователей") + else: + # Применение изменений для конкретных пользователей + for user in selected_users: + try: + subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True) + d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}") + logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}") + except subprocess.CalledProcessError as e: + d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}") + + +def run_change_shell(): + change_shell() + + +if __name__ == "__main__": + run_change_shell() diff --git a/postinstall/option/ssh.py b/postinstall/option/ssh.py new file mode 100644 index 0000000..fdd5890 --- /dev/null +++ b/postinstall/option/ssh.py @@ -0,0 +1,236 @@ +import subprocess +from dialog import Dialog +import logging +import os + +SSH_CONFIG_PATH = "/etc/ssh/sshd_config" +SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf" +UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server" + + +class SSHConfig: + def __init__(self): + self.d = Dialog(dialog="dialog") + self.current_port = self.get_current_ssh_port() + self.current_auth_method = self.get_current_auth_method() + self.new_port = None + self.new_auth_method = None + + def get_sshd_status(self): + try: + result = subprocess.run( + ["systemctl", "is-active", "sshd"], capture_output=True, text=True + ) + return result.stdout.strip() + except subprocess.CalledProcessError: + return "unknown" + + def toggle_sshd_service(self, action): + try: + subprocess.run(["sudo", "systemctl", action, "sshd"], check=True) + self.d.msgbox( + f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}." + ) + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при {action} службы sshd: {e}") + self.d.msgbox( + f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd." + ) + + def get_current_ssh_port(self): + 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" + + def change_ssh_port(self): + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + code, new_port = self.d.inputbox( + f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:", + init=port_display, + ) + if code != self.d.OK: + return + try: + new_port = int(new_port) + if new_port < 1 or new_port > 65535: + raise ValueError + except ValueError: + self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.") + return + code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?") + if code != self.d.OK: + return + self.new_port = str(new_port) + + def get_current_auth_method(self): + try: + with open(SSH_AUTH_CONFIG_PATH, "r") as f: + content = f.read() + password_auth = "yes" if "PasswordAuthentication yes" in content else "no" + pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no" + + if password_auth == "yes" and pubkey_auth == "yes": + return "По ключу и паролю" + elif password_auth == "no" and pubkey_auth == "yes": + return "Только по ключу" + elif password_auth == "yes" and pubkey_auth == "no": + return "Только по паролю" + else: + return "По ключу и паролю" # Default value + except FileNotFoundError: + logging.warning("Файл конфигурации аутентификации SSH не найден") + return "По ключу и паролю" # Default value + except Exception as e: + logging.warning(f"Не удалось получить метод аутентификации SSH: {e}") + return "По ключу и паролю" # Default value + + def change_auth_method(self): + auth_method_display = ( + self.new_auth_method if self.new_auth_method else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + auth_choices = [ + ( + "1", + f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}", + ), + ( + "2", + f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}", + ), + ( + "3", + f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}", + ), + ] + code, tag = self.d.menu( + f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:", + choices=auth_choices, + ) + if code != self.d.OK: + return + self.new_auth_method = { + "1": "Только по паролю", + "2": "Только по ключу", + "3": "По ключу и паролю", + }[tag] + + def apply_changes(self): + if self.new_port: + try: + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^#*Port .*/Port {self.new_port}/", + SSH_CONFIG_PATH, + ], + check=True, + ) + subprocess.run( + [ + "sudo", + "sed", + "-i", + f"s/^ports=.*/ports={self.new_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) + self.current_port = self.new_port + self.new_port = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при изменении порта SSH: {e}") + self.d.msgbox(f"Ошибка при изменении порта SSH: {e}") + + if self.new_auth_method: + try: + auth_config = "" + if self.new_auth_method == "Только по паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication no\n" + ) + elif self.new_auth_method == "Только по ключу": + auth_config = ( + "PasswordAuthentication no\nPubkeyAuthentication yes\n" + ) + elif self.new_auth_method == "По ключу и паролю": + auth_config = ( + "PasswordAuthentication yes\nPubkeyAuthentication yes\n" + ) + + os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True) + with open(SSH_AUTH_CONFIG_PATH, "w") as f: + f.write(auth_config) + + self.current_auth_method = self.new_auth_method + self.new_auth_method = None + subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True) + self.d.msgbox("Метод аутентификации SSH успешно изменен.") + except Exception as e: + logging.error(f"Ошибка при изменении метода аутентификации: {e}") + self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}") + + def main_menu(self): + while True: + status = self.get_sshd_status() + port_display = self.new_port if self.new_port else self.current_port + port_status = " (не применено)" if self.new_port else "" + auth_method_display = ( + self.new_auth_method + if self.new_auth_method + else self.current_auth_method + ) + auth_status = " (не применено)" if self.new_auth_method else "" + + code, tag = self.d.menu( + f"Статус sshd: {status}\n" + f"Текущий порт: {port_display}{port_status}\n" + f"Метод аутентификации: {auth_method_display}{auth_status}", + choices=[ + ("1", "Включить sshd" if status != "active" else "Выключить sshd"), + ("2", "Изменить порт SSH"), + ("3", "Изменить метод аутентификации"), + ], + ) + if code != self.d.OK: + break + if tag == "1": + self.toggle_sshd_service("start" if status != "active" else "stop") + elif tag == "2": + self.change_ssh_port() + elif tag == "3": + self.change_auth_method() + + return self.new_port is not None or self.new_auth_method is not None + + +def run_ssh_config(): + ssh_config = SSHConfig() + changes_made = ssh_config.main_menu() + return ssh_config, changes_made + + +if __name__ == "__main__": + ssh_config, changes_made = run_ssh_config() + # Note: Changes will be applied in the main menu when "Save and exit" is selected diff --git a/postinstall/option/update.py b/postinstall/option/update.py new file mode 100644 index 0000000..a61cb69 --- /dev/null +++ b/postinstall/option/update.py @@ -0,0 +1,57 @@ +import subprocess +from dialog import Dialog +from postinstall.logger import logger + +logging = logger + + +def run_update(): + d = Dialog(dialog="dialog") + + code = d.yesno("Вы действительно хотите обновить систему?", width=50) + if code != d.OK: + return + + commands = [["apt", "update"], ["apt", "upgrade", "-y"]] + + d.gauge_start("Обновление системы", percent=0) + + try: + for i, command in enumerate(commands): + d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}") + + process = subprocess.Popen( + ["sudo"] + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + while True: + line = process.stdout.readline() + if not line: + break + d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip()) + + process.wait() + + if process.returncode != 0: + error = process.stderr.read() + raise subprocess.CalledProcessError(process.returncode, command, error) + + d.gauge_update(100, "Обновление системы завершено успешно") + d.gauge_stop() + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Ошибка при обновлении системы: {e}") + except Exception as e: + logging.error(f"Неожиданная ошибка при обновлении системы: {e}") + d.gauge_stop() + d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}") + + d.msgbox("Обновление системы завершено", width=50) + + +if __name__ == "__main__": + run_update() diff --git a/postinstall/root.py b/postinstall/root.py new file mode 100644 index 0000000..89482a6 --- /dev/null +++ b/postinstall/root.py @@ -0,0 +1,11 @@ +import os +import sys + + +def check_root(): + if os.geteuid() != 0: + print("Запустите скрипт с правами суперпользователя") + sys.exit(1) + + +check_root() diff --git a/pygments-2.18.0-py3-none-any.whl b/pygments-2.18.0-py3-none-any.whl new file mode 100644 index 0000000..e67ab03 Binary files /dev/null and b/pygments-2.18.0-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6dc49c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "postinstall" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +pythondialog = "^3.5.3" + + + +[tool.poetry.scripts] +postinstall = "postinstall:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5f8cf23 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +deps/linkify_it_py-2.0.3-py3-none-any.whl +deps/markdown_it_py-3.0.0-py3-none-any.whl +deps/mdit_py_plugins-0.4.1-py3-none-any.whl +deps/mdurl-0.1.2-py3-none-any.whl +deps/pygments-2.18.0-py3-none-any.whl +deps/rich-13.7.1-py3-none-any.whl +deps/textual-0.72.0-py3-none-any.whl +deps/typing_extensions-4.12.2-py3-none-any.whl +deps/uc_micro_py-1.0.3-py3-none-any.whl + +setuptools~=66.1.1 +textual~=0.72.0 + +postinstall~=0.1.0 \ No newline at end of file diff --git a/rich-13.7.1-py3-none-any.whl b/rich-13.7.1-py3-none-any.whl new file mode 100644 index 0000000..0ed0599 Binary files /dev/null and b/rich-13.7.1-py3-none-any.whl differ diff --git a/run_with_poetry_root.sh b/run_with_poetry_root.sh new file mode 100644 index 0000000..b7218f1 --- /dev/null +++ b/run_with_poetry_root.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Перейти в директорию проекта +cd "${WORKSPACE_FOLDER}" + +# Запустить Poetry shell +sudo poetry shell <=3.5.3,<4.0.0"], + entry_points={ + "console_scripts": [ + "postinstall=postinstall:main", + ], + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent :: Linux", + ], + python_requires=">=3.11", + data_files=[ + ("/usr/share/postinstall", []), + ("/etc/systemd/system", ["deb/postinstall.service"]), + ("/etc/profile.d", ["deb/0002-postinstall.sh"]), + ], +) diff --git a/stdeb.cfg b/stdeb.cfg new file mode 100644 index 0000000..9dceb15 --- /dev/null +++ b/stdeb.cfg @@ -0,0 +1,7 @@ +[DEFAULT] +Suite: stable +Maintainer: LLC Technologies and Communication +Depends: python3-textual +Provides: postinstall +Section: python +Postinst: deb/postinst diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..be9121a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,9 @@ +import subprocess + + +def get_network_interfaces(): + interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n") + return interfaces[:-1] + + +get_network_interfaces() diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a65c476 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-311.pyc differ diff --git a/tests/__pycache__/docker_config.cpython-311.pyc b/tests/__pycache__/docker_config.cpython-311.pyc new file mode 100644 index 0000000..4253dd7 Binary files /dev/null and b/tests/__pycache__/docker_config.cpython-311.pyc differ diff --git a/tests/__pycache__/test_docker.cpython-311.pyc b/tests/__pycache__/test_docker.cpython-311.pyc new file mode 100644 index 0000000..783d1f7 Binary files /dev/null and b/tests/__pycache__/test_docker.cpython-311.pyc differ diff --git a/tests/docker_config.py b/tests/docker_config.py new file mode 100644 index 0000000..62ddf0a --- /dev/null +++ b/tests/docker_config.py @@ -0,0 +1,132 @@ +import logging +import subprocess +import os +import re + +REGISTRY_URL = "dr.gosrv.ru" +REGISTRY_LOGIN = "dr_admin" +REGISTRY_PASSWORD = "Qweasdzxc123C@" +DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/" + + +class DockerConfig: + def check_installed(self, command: str) -> bool: + try: + return ( + subprocess.run( + ["which", command], capture_output=True, text=True + ).returncode + == 0 + ) + except subprocess.SubprocessError as e: + logging.error(f"Error checking if {command} is installed: {e}") + return False + + def install_docker(self): + try: + if not self.check_installed("docker"): + logging.info("Установка Docker...") + subprocess.run(["apt-get", "update"], check=True) + subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True) + logging.info("Docker успешно установился") + + if not self.check_installed("docker-compose"): + logging.info("Установка Docker Compose...") + subprocess.run( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + logging.info("Docker Compose успешно установился") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Docker: {e}") + raise + + def check_docker_login(self, registry_url): + try: + result = subprocess.run( + ["docker", "login", registry_url, "--password-stdin"], + input=b"\n", + capture_output=True, + text=True, + check=True, + ) + return "login succeeded" in result.stdout.lower() + except subprocess.CalledProcessError as e: + logging.error(f"Error checking Docker login: {e}") + return False + + def login_to_registry(self): + try: + if not self.check_docker_login(REGISTRY_URL): + logging.info("Вход в Docker registry...") + subprocess.run( + ["docker", "login", REGISTRY_URL], + input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(), + check=True, + ) + logging.info("Успешный вход в Docker registry") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при входе в Docker registry: {e}") + raise + + def install_portainer(self, port, login, password, ip): + try: + if not self.check_docker_login(REGISTRY_URL): + self.login_to_registry() + + os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True) + docker_compose_content = f""" +version: '3' + +services: + portainer: + image: dr.gosrv.ru/portainer + container_name: portainer + restart: unless-stopped + security_opt: + - no-new-privileges:true + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer_data:/data + - ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password + ports: + - "{ip}:{port}:9000" + environment: + - ADMIN_USER={login} + - ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password + +volumes: + portainer_data: + """ + + with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f: + f.write(docker_compose_content) + + subprocess.run( + ["htpasswd", "-nb", login, password], + stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"), + check=True, + ) + + subprocess.run( + [ + "docker-compose", + "-f", + DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", + "up", + "-d", + ], + check=True, + ) + logging.info("Portainer успешно установлен") + except subprocess.CalledProcessError as e: + logging.error(f"Ошибка при установке Portainer: {e}") + raise + except IOError as e: + logging.error(f"Ошибка при создании файла: {e}") + raise + + def validate_input(self, value, pattern, error_message): + if not re.match(pattern, value): + return False + return True diff --git a/tests/test_docker.py b/tests/test_docker.py new file mode 100644 index 0000000..b5caf04 --- /dev/null +++ b/tests/test_docker.py @@ -0,0 +1,67 @@ +import unittest +from unittest.mock import patch, MagicMock +from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH + +class TestDockerConfig(unittest.TestCase): + def setUp(self): + self.docker_config = DockerConfig() + + @patch("subprocess.run") + def test_check_installed_true(self, mock_run): + mock_run.return_value.returncode = 0 + self.assertTrue(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_check_installed_false(self, mock_run): + mock_run.return_value.returncode = 1 + self.assertFalse(self.docker_config.check_installed("docker")) + + @patch("subprocess.run") + def test_install_docker(self, mock_run): + self.docker_config.install_docker() + mock_run.assert_any_call(["apt-get", "update"], check=True) + mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True) + mock_run.assert_any_call( + ["apt-get", "install", "-y", "docker-compose"], check=True + ) + + @patch("subprocess.run") + def test_check_docker_login_success(self, mock_run): + mock_run.return_value.stdout = "Login Succeeded" + self.assertTrue(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + def test_check_docker_login_failure(self, mock_run): + mock_run.return_value.stdout = "Login Failed" + self.assertFalse(self.docker_config.check_docker_login("registry_url")) + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + def test_login_to_registry(self, mock_check_login, mock_run): + mock_check_login.return_value = False + self.docker_config.login_to_registry() + mock_run.assert_called_once() + + @patch("subprocess.run") + @patch("docker_config.DockerConfig.check_docker_login") + @patch("docker_config.DockerConfig.login_to_registry") + @patch("builtins.open", new_callable=unittest.mock.mock_open) + def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run): + mock_check_login.return_value = True + self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1") + mock_open.assert_called() + mock_run.assert_called() + + def test_validate_input_success(self): + self.assertTrue( + self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + def test_validate_input_failure(self): + self.assertFalse( + self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error") + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/textualconf-0.1.tar.gz b/textualconf-0.1.tar.gz new file mode 100644 index 0000000..decede6 Binary files /dev/null and b/textualconf-0.1.tar.gz differ diff --git a/textualconf.egg-info/PKG-INFO b/textualconf.egg-info/PKG-INFO new file mode 100644 index 0000000..da44f1d --- /dev/null +++ b/textualconf.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.1 +Name: postinstall +Version: 0.1 +Summary: A Textual TUI application for network configuration +Author: LLC Technologies and Communication +Author-email: devteam@texnico.ru +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown diff --git a/textualconf.egg-info/SOURCES.txt b/textualconf.egg-info/SOURCES.txt new file mode 100644 index 0000000..0b2bd4b --- /dev/null +++ b/textualconf.egg-info/SOURCES.txt @@ -0,0 +1,30 @@ +MANIFEST.in +README.md +pyproject.toml +setup.py +deb/postinst +deb/python3-textual_0.1.13-1_all.deb +deb/postinstall-autostart.desktop +deb/postinstall.desktop +deb/postinstall.service +deb/postinstall_start.sh +deps/linkify_it_py-2.0.3-py3-none-any.whl +deps/markdown_it_py-3.0.0-py3-none-any.whl +deps/mdit_py_plugins-0.4.1-py3-none-any.whl +deps/mdurl-0.1.2-py3-none-any.whl +deps/pygments-2.18.0-py3-none-any.whl +deps/rich-13.7.1-py3-none-any.whl +deps/textual-0.72.0-py3-none-any.whl +deps/typing_extensions-4.12.2-py3-none-any.whl +deps/uc_micro_py-1.0.3-py3-none-any.whl +tests/__init__.py +postinstall/__init__.py +postinstall/__main__.py +postinstall/main.py +postinstall/styles.tcss +postinstall.egg-info/PKG-INFO +postinstall.egg-info/SOURCES.txt +postinstall.egg-info/dependency_links.txt +postinstall.egg-info/entry_points.txt +postinstall.egg-info/requires.txt +postinstall.egg-info/top_level.txt \ No newline at end of file diff --git a/textualconf.egg-info/dependency_links.txt b/textualconf.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/textualconf.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/textualconf.egg-info/entry_points.txt b/textualconf.egg-info/entry_points.txt new file mode 100644 index 0000000..09c196f --- /dev/null +++ b/textualconf.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +postinstall = postinstall:main diff --git a/textualconf.egg-info/requires.txt b/textualconf.egg-info/requires.txt new file mode 100644 index 0000000..ca1b03d --- /dev/null +++ b/textualconf.egg-info/requires.txt @@ -0,0 +1 @@ +textual>=0.72.0 diff --git a/textualconf.egg-info/top_level.txt b/textualconf.egg-info/top_level.txt new file mode 100644 index 0000000..d3fb3ed --- /dev/null +++ b/textualconf.egg-info/top_level.txt @@ -0,0 +1,2 @@ +tests +postinstall diff --git a/typing_extensions-4.12.2-py3-none-any.whl b/typing_extensions-4.12.2-py3-none-any.whl new file mode 100644 index 0000000..f6cc799 Binary files /dev/null and b/typing_extensions-4.12.2-py3-none-any.whl differ diff --git a/uc_micro_py-1.0.3-py3-none-any.whl b/uc_micro_py-1.0.3-py3-none-any.whl new file mode 100644 index 0000000..b417bea Binary files /dev/null and b/uc_micro_py-1.0.3-py3-none-any.whl differ