Старый postinstall
This commit is contained in:
commit
4337a3f94e
349 changed files with 21945 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.idea
|
||||
.vscode
|
||||
.venv
|
||||
code.md
|
||||
prompt.txt
|
||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
include README.md
|
||||
include LICENSE
|
||||
include postinstall/styles.tcss
|
||||
230
README.md
Normal file
230
README.md
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
## Документация Postinstall
|
||||
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **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-адресов будет использоваться в сети.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.5 Сохранение и отмена изменений**
|
||||
|
||||
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
|
||||
|
||||
---
|
||||
|
||||
### **2. Обновление системы**
|
||||
|
||||
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
|
||||
|
||||
#### **2.1 Обновление списков пакетов**
|
||||
|
||||
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
|
||||
|
||||
#### **2.2 Установка обновлений**
|
||||
|
||||
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Добавить пользователя**
|
||||
|
||||
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
|
||||
|
||||
#### **3.1 Создание нового пользователя**
|
||||
|
||||
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.2 Установка пароля**
|
||||
|
||||
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.3 Назначение прав администратора**
|
||||
|
||||
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Установка программного обеспечения**
|
||||
|
||||
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
|
||||
|
||||
#### **4.1 Выбор и установка пакетов**
|
||||
|
||||
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **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`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **6. Изменить доменное имя ПК**
|
||||
|
||||
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
|
||||
|
||||
#### **6.1 Изменение доменного имени**
|
||||
|
||||
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **7. Выбор оболочки**
|
||||
|
||||
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
|
||||
|
||||
#### **7.1 Установка оболочки по умолчанию**
|
||||
|
||||
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
|
||||
|
||||
#### **7.2 Выбор оболочки**
|
||||
|
||||
Вы можете выбрать оболочку из списка доступных в системе.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **8. Автозапуск при следующем запуске**
|
||||
|
||||
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
|
||||
|
||||
#### **8.1 Управление автозапуском Postinstall**
|
||||
|
||||
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
|
||||
|
||||
---
|
||||
|
||||
### **9. Настройка базы данных**
|
||||
|
||||
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
|
||||
|
||||
#### **9.1 Установка и настройка PostgreSQL**
|
||||
|
||||
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
|
||||
|
||||
#### **9.2 Логирование выполняемых действий**
|
||||
|
||||
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
#### **9.2 Установка дополнительных модулей**
|
||||
|
||||
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **10. Настройка Docker**
|
||||
|
||||
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.1 Установка дополнительных модулей Docker**
|
||||
|
||||
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.2 Автоматическая настройка модулей**
|
||||
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
9
build/lib/tests/__init__.py
Normal file
9
build/lib/tests/__init__.py
Normal file
|
|
@ -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()
|
||||
507
build/lib/textualconf/__init__.py
Normal file
507
build/lib/textualconf/__init__.py
Normal file
|
|
@ -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()
|
||||
66
build/lib/textualconf/styles.tcss
Executable file
66
build/lib/textualconf/styles.tcss
Executable file
|
|
@ -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%;
|
||||
}
|
||||
4
build_deb.sh
Executable file
4
build_deb.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#/bin/bash
|
||||
rm -rf deb_dist
|
||||
rm -rf dist
|
||||
python3 setup.py --command-packages=stdeb.command bdist_deb
|
||||
3
deb/0002-postinstall.sh
Normal file
3
deb/0002-postinstall.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
if [ -n "$PS1" ]; then
|
||||
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
|
||||
fi
|
||||
12
deb/postinst
Executable file
12
deb/postinst
Executable file
|
|
@ -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
|
||||
|
||||
10
deb/postinstall-autostart.desktop
Normal file
10
deb/postinstall-autostart.desktop
Normal file
|
|
@ -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
|
||||
9
deb/postinstall.desktop
Normal file
9
deb/postinstall.desktop
Normal file
|
|
@ -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;
|
||||
15
deb/postinstall.service
Normal file
15
deb/postinstall.service
Normal file
|
|
@ -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
|
||||
2
deb/postinstall_start.sh
Executable file
2
deb/postinstall_start.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
exec /usr/bin/postinstall
|
||||
BIN
deb/python3-textual_0.1.13-1_all.deb
Normal file
BIN
deb/python3-textual_0.1.13-1_all.deb
Normal file
Binary file not shown.
BIN
deb/textual-0.72.0-py3-none-any.whl
Normal file
BIN
deb/textual-0.72.0-py3-none-any.whl
Normal file
Binary file not shown.
1
deb_dist/postinstall-1.0.1/.pc/.quilt_patches
Normal file
1
deb_dist/postinstall-1.0.1/.pc/.quilt_patches
Normal file
|
|
@ -0,0 +1 @@
|
|||
debian/patches
|
||||
1
deb_dist/postinstall-1.0.1/.pc/.quilt_series
Normal file
1
deb_dist/postinstall-1.0.1/.pc/.quilt_series
Normal file
|
|
@ -0,0 +1 @@
|
|||
series
|
||||
1
deb_dist/postinstall-1.0.1/.pc/.version
Normal file
1
deb_dist/postinstall-1.0.1/.pc/.version
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
0
deb_dist/postinstall-1.0.1/.pc/applied-patches
Normal file
0
deb_dist/postinstall-1.0.1/.pc/applied-patches
Normal file
3
deb_dist/postinstall-1.0.1/MANIFEST.in
Normal file
3
deb_dist/postinstall-1.0.1/MANIFEST.in
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
include README.md
|
||||
include LICENSE
|
||||
include postinstall/styles.tcss
|
||||
243
deb_dist/postinstall-1.0.1/PKG-INFO
Normal file
243
deb_dist/postinstall-1.0.1/PKG-INFO
Normal file
|
|
@ -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 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **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-адресов будет использоваться в сети.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.5 Сохранение и отмена изменений**
|
||||
|
||||
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
|
||||
|
||||
---
|
||||
|
||||
### **2. Обновление системы**
|
||||
|
||||
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
|
||||
|
||||
#### **2.1 Обновление списков пакетов**
|
||||
|
||||
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
|
||||
|
||||
#### **2.2 Установка обновлений**
|
||||
|
||||
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Добавить пользователя**
|
||||
|
||||
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
|
||||
|
||||
#### **3.1 Создание нового пользователя**
|
||||
|
||||
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.2 Установка пароля**
|
||||
|
||||
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.3 Назначение прав администратора**
|
||||
|
||||
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Установка программного обеспечения**
|
||||
|
||||
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
|
||||
|
||||
#### **4.1 Выбор и установка пакетов**
|
||||
|
||||
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **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`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **6. Изменить доменное имя ПК**
|
||||
|
||||
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
|
||||
|
||||
#### **6.1 Изменение доменного имени**
|
||||
|
||||
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **7. Выбор оболочки**
|
||||
|
||||
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
|
||||
|
||||
#### **7.1 Установка оболочки по умолчанию**
|
||||
|
||||
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
|
||||
|
||||
#### **7.2 Выбор оболочки**
|
||||
|
||||
Вы можете выбрать оболочку из списка доступных в системе.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **8. Автозапуск при следующем запуске**
|
||||
|
||||
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
|
||||
|
||||
#### **8.1 Управление автозапуском Postinstall**
|
||||
|
||||
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
|
||||
|
||||
---
|
||||
|
||||
### **9. Настройка базы данных**
|
||||
|
||||
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
|
||||
|
||||
#### **9.1 Установка и настройка PostgreSQL**
|
||||
|
||||
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
|
||||
|
||||
#### **9.2 Логирование выполняемых действий**
|
||||
|
||||
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
#### **9.2 Установка дополнительных модулей**
|
||||
|
||||
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **10. Настройка Docker**
|
||||
|
||||
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.1 Установка дополнительных модулей Docker**
|
||||
|
||||
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.2 Автоматическая настройка модулей**
|
||||
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
230
deb_dist/postinstall-1.0.1/README.md
Normal file
230
deb_dist/postinstall-1.0.1/README.md
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
## Документация Postinstall
|
||||
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **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-адресов будет использоваться в сети.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.5 Сохранение и отмена изменений**
|
||||
|
||||
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
|
||||
|
||||
---
|
||||
|
||||
### **2. Обновление системы**
|
||||
|
||||
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
|
||||
|
||||
#### **2.1 Обновление списков пакетов**
|
||||
|
||||
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
|
||||
|
||||
#### **2.2 Установка обновлений**
|
||||
|
||||
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Добавить пользователя**
|
||||
|
||||
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
|
||||
|
||||
#### **3.1 Создание нового пользователя**
|
||||
|
||||
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.2 Установка пароля**
|
||||
|
||||
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.3 Назначение прав администратора**
|
||||
|
||||
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Установка программного обеспечения**
|
||||
|
||||
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
|
||||
|
||||
#### **4.1 Выбор и установка пакетов**
|
||||
|
||||
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **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`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **6. Изменить доменное имя ПК**
|
||||
|
||||
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
|
||||
|
||||
#### **6.1 Изменение доменного имени**
|
||||
|
||||
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **7. Выбор оболочки**
|
||||
|
||||
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
|
||||
|
||||
#### **7.1 Установка оболочки по умолчанию**
|
||||
|
||||
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
|
||||
|
||||
#### **7.2 Выбор оболочки**
|
||||
|
||||
Вы можете выбрать оболочку из списка доступных в системе.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **8. Автозапуск при следующем запуске**
|
||||
|
||||
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
|
||||
|
||||
#### **8.1 Управление автозапуском Postinstall**
|
||||
|
||||
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
|
||||
|
||||
---
|
||||
|
||||
### **9. Настройка базы данных**
|
||||
|
||||
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
|
||||
|
||||
#### **9.1 Установка и настройка PostgreSQL**
|
||||
|
||||
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
|
||||
|
||||
#### **9.2 Логирование выполняемых действий**
|
||||
|
||||
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
#### **9.2 Установка дополнительных модулей**
|
||||
|
||||
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **10. Настройка Docker**
|
||||
|
||||
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.1 Установка дополнительных модулей Docker**
|
||||
|
||||
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.2 Автоматическая настройка модулей**
|
||||
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
395
deb_dist/postinstall-1.0.1/build/lib/postinstall/__init__.py
Normal file
395
deb_dist/postinstall-1.0.1/build/lib/postinstall/__init__.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
25
deb_dist/postinstall-1.0.1/build/lib/postinstall/logger.py
Normal file
25
deb_dist/postinstall-1.0.1/build/lib/postinstall/logger.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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("Изменение доменного имени отменено.")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
106
deb_dist/postinstall-1.0.1/build/lib/postinstall/option/shell.py
Normal file
106
deb_dist/postinstall-1.0.1/build/lib/postinstall/option/shell.py
Normal file
|
|
@ -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()
|
||||
236
deb_dist/postinstall-1.0.1/build/lib/postinstall/option/ssh.py
Normal file
236
deb_dist/postinstall-1.0.1/build/lib/postinstall/option/ssh.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
11
deb_dist/postinstall-1.0.1/build/lib/postinstall/root.py
Normal file
11
deb_dist/postinstall-1.0.1/build/lib/postinstall/root.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def check_root():
|
||||
if os.geteuid() != 0:
|
||||
print("Запустите скрипт с правами суперпользователя")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
check_root()
|
||||
9
deb_dist/postinstall-1.0.1/build/lib/tests/__init__.py
Normal file
9
deb_dist/postinstall-1.0.1/build/lib/tests/__init__.py
Normal file
|
|
@ -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()
|
||||
132
deb_dist/postinstall-1.0.1/build/lib/tests/docker_config.py
Normal file
132
deb_dist/postinstall-1.0.1/build/lib/tests/docker_config.py
Normal file
|
|
@ -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
|
||||
67
deb_dist/postinstall-1.0.1/build/lib/tests/test_docker.py
Normal file
67
deb_dist/postinstall-1.0.1/build/lib/tests/test_docker.py
Normal file
|
|
@ -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()
|
||||
3
deb_dist/postinstall-1.0.1/deb/0002-postinstall.sh
Normal file
3
deb_dist/postinstall-1.0.1/deb/0002-postinstall.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
if [ -n "$PS1" ]; then
|
||||
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
|
||||
fi
|
||||
12
deb_dist/postinstall-1.0.1/deb/postinst
Executable file
12
deb_dist/postinstall-1.0.1/deb/postinst
Executable file
|
|
@ -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
|
||||
|
||||
10
deb_dist/postinstall-1.0.1/deb/postinstall-autostart.desktop
Normal file
10
deb_dist/postinstall-1.0.1/deb/postinstall-autostart.desktop
Normal file
|
|
@ -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
|
||||
9
deb_dist/postinstall-1.0.1/deb/postinstall.desktop
Normal file
9
deb_dist/postinstall-1.0.1/deb/postinstall.desktop
Normal file
|
|
@ -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;
|
||||
15
deb_dist/postinstall-1.0.1/deb/postinstall.service
Normal file
15
deb_dist/postinstall-1.0.1/deb/postinstall.service
Normal file
|
|
@ -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
|
||||
2
deb_dist/postinstall-1.0.1/deb/postinstall_start.sh
Executable file
2
deb_dist/postinstall-1.0.1/deb/postinstall_start.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
exec /usr/bin/postinstall
|
||||
BIN
deb_dist/postinstall-1.0.1/deb/python3-textual_0.1.13-1_all.deb
Normal file
BIN
deb_dist/postinstall-1.0.1/deb/python3-textual_0.1.13-1_all.deb
Normal file
Binary file not shown.
BIN
deb_dist/postinstall-1.0.1/deb/textual-0.72.0-py3-none-any.whl
Normal file
BIN
deb_dist/postinstall-1.0.1/deb/textual-0.72.0-py3-none-any.whl
Normal file
Binary file not shown.
|
|
@ -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 <devteam@texnico.ru> Fri, 20 Sep 2024 01:42:42 +0500
|
||||
5
deb_dist/postinstall-1.0.1/debian/changelog
Normal file
5
deb_dist/postinstall-1.0.1/debian/changelog
Normal file
|
|
@ -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 <devteam@texnico.ru> Fri, 20 Sep 2024 01:42:42 +0500
|
||||
1
deb_dist/postinstall-1.0.1/debian/compat
Normal file
1
deb_dist/postinstall-1.0.1/debian/compat
Normal file
|
|
@ -0,0 +1 @@
|
|||
9
|
||||
33
deb_dist/postinstall-1.0.1/debian/control
Normal file
33
deb_dist/postinstall-1.0.1/debian/control
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
Source: postinstall
|
||||
Maintainer: LLC Technologies and Communication <devteam@texnico.ru>
|
||||
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 Выбор сетевого интерфейса**
|
||||
.
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
.
|
||||

|
||||
.
|
||||
.
|
||||
#### **1.2 Настройка DHCP**
|
||||
.
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
.
|
||||
#### **1.3 Настройка статического IP**
|
||||
.
|
||||
|
||||
2
deb_dist/postinstall-1.0.1/debian/files
Normal file
2
deb_dist/postinstall-1.0.1/debian/files
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
postinstall_1.0.1-1_amd64.buildinfo python optional
|
||||
python3-postinstall_1.0.1-1_all.deb python optional
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
python3:Depends=python3-dialog, python3:any
|
||||
misc:Depends=
|
||||
misc:Pre-Depends=
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/etc/profile.d/0002-postinstall.sh
|
||||
/etc/systemd/system/postinstall.service
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
Package: python3-postinstall
|
||||
Source: postinstall
|
||||
Version: 1.0.1-1
|
||||
Architecture: all
|
||||
Maintainer: LLC Technologies and Communication <devteam@texnico.ru>
|
||||
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 Выбор сетевого интерфейса**
|
||||
.
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
.
|
||||

|
||||
.
|
||||
.
|
||||
#### **1.2 Настройка DHCP**
|
||||
.
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
.
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
|
@ -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
|
||||
12
deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/postinst
Executable file
12
deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/postinst
Executable file
|
|
@ -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
|
||||
12
deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/prerm
Executable file
12
deb_dist/postinstall-1.0.1/debian/python3-postinstall/DEBIAN/prerm
Executable file
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
if [ -n "$PS1" ]; then
|
||||
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
|
||||
fi
|
||||
|
|
@ -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
|
||||
33
deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/bin/postinstall
Executable file
33
deb_dist/postinstall-1.0.1/debian/python3-postinstall/usr/bin/postinstall
Executable file
|
|
@ -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')())
|
||||
|
|
@ -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 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **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-адресов будет использоваться в сети.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.5 Сохранение и отмена изменений**
|
||||
|
||||
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
|
||||
|
||||
---
|
||||
|
||||
### **2. Обновление системы**
|
||||
|
||||
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
|
||||
|
||||
#### **2.1 Обновление списков пакетов**
|
||||
|
||||
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
|
||||
|
||||
#### **2.2 Установка обновлений**
|
||||
|
||||
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Добавить пользователя**
|
||||
|
||||
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
|
||||
|
||||
#### **3.1 Создание нового пользователя**
|
||||
|
||||
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.2 Установка пароля**
|
||||
|
||||
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.3 Назначение прав администратора**
|
||||
|
||||
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Установка программного обеспечения**
|
||||
|
||||
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
|
||||
|
||||
#### **4.1 Выбор и установка пакетов**
|
||||
|
||||
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **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`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **6. Изменить доменное имя ПК**
|
||||
|
||||
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
|
||||
|
||||
#### **6.1 Изменение доменного имени**
|
||||
|
||||
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **7. Выбор оболочки**
|
||||
|
||||
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
|
||||
|
||||
#### **7.1 Установка оболочки по умолчанию**
|
||||
|
||||
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
|
||||
|
||||
#### **7.2 Выбор оболочки**
|
||||
|
||||
Вы можете выбрать оболочку из списка доступных в системе.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **8. Автозапуск при следующем запуске**
|
||||
|
||||
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
|
||||
|
||||
#### **8.1 Управление автозапуском Postinstall**
|
||||
|
||||
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
|
||||
|
||||
---
|
||||
|
||||
### **9. Настройка базы данных**
|
||||
|
||||
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
|
||||
|
||||
#### **9.1 Установка и настройка PostgreSQL**
|
||||
|
||||
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
|
||||
|
||||
#### **9.2 Логирование выполняемых действий**
|
||||
|
||||
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
#### **9.2 Установка дополнительных модулей**
|
||||
|
||||
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **10. Настройка Docker**
|
||||
|
||||
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.1 Установка дополнительных модулей Docker**
|
||||
|
||||
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.2 Автоматическая настройка модулей**
|
||||
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[console_scripts]
|
||||
postinstall = postinstall:main
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
postinstall
|
||||
tests
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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("Изменение доменного имени отменено.")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def check_root():
|
||||
if os.geteuid() != 0:
|
||||
print("Запустите скрипт с правами суперпользователя")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
check_root()
|
||||
Binary file not shown.
21
deb_dist/postinstall-1.0.1/debian/rules
Executable file
21
deb_dist/postinstall-1.0.1/debian/rules
Executable file
|
|
@ -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
|
||||
|
||||
1
deb_dist/postinstall-1.0.1/debian/source/format
Normal file
1
deb_dist/postinstall-1.0.1/debian/source/format
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
||||
1
deb_dist/postinstall-1.0.1/debian/source/options
Normal file
1
deb_dist/postinstall-1.0.1/debian/source/options
Normal file
|
|
@ -0,0 +1 @@
|
|||
extend-diff-ignore="\.egg-info$"
|
||||
243
deb_dist/postinstall-1.0.1/postinstall.egg-info/PKG-INFO
Normal file
243
deb_dist/postinstall-1.0.1/postinstall.egg-info/PKG-INFO
Normal file
|
|
@ -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 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **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-адресов будет использоваться в сети.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.5 Сохранение и отмена изменений**
|
||||
|
||||
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
|
||||
|
||||
---
|
||||
|
||||
### **2. Обновление системы**
|
||||
|
||||
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
|
||||
|
||||
#### **2.1 Обновление списков пакетов**
|
||||
|
||||
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
|
||||
|
||||
#### **2.2 Установка обновлений**
|
||||
|
||||
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **3. Добавить пользователя**
|
||||
|
||||
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
|
||||
|
||||
#### **3.1 Создание нового пользователя**
|
||||
|
||||
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.2 Установка пароля**
|
||||
|
||||
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **3.3 Назначение прав администратора**
|
||||
|
||||
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **4. Установка программного обеспечения**
|
||||
|
||||
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
|
||||
|
||||
#### **4.1 Выбор и установка пакетов**
|
||||
|
||||
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **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`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **6. Изменить доменное имя ПК**
|
||||
|
||||
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
|
||||
|
||||
#### **6.1 Изменение доменного имени**
|
||||
|
||||
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **7. Выбор оболочки**
|
||||
|
||||
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
|
||||
|
||||
#### **7.1 Установка оболочки по умолчанию**
|
||||
|
||||
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
|
||||
|
||||
#### **7.2 Выбор оболочки**
|
||||
|
||||
Вы можете выбрать оболочку из списка доступных в системе.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **8. Автозапуск при следующем запуске**
|
||||
|
||||
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
|
||||
|
||||
#### **8.1 Управление автозапуском Postinstall**
|
||||
|
||||
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
|
||||
|
||||
---
|
||||
|
||||
### **9. Настройка базы данных**
|
||||
|
||||
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
|
||||
|
||||
#### **9.1 Установка и настройка PostgreSQL**
|
||||
|
||||
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
|
||||
|
||||
#### **9.2 Логирование выполняемых действий**
|
||||
|
||||
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
#### **9.2 Установка дополнительных модулей**
|
||||
|
||||
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### **10. Настройка Docker**
|
||||
|
||||
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.1 Установка дополнительных модулей Docker**
|
||||
|
||||
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
#### **10.2 Автоматическая настройка модулей**
|
||||
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
37
deb_dist/postinstall-1.0.1/postinstall.egg-info/SOURCES.txt
Normal file
37
deb_dist/postinstall-1.0.1/postinstall.egg-info/SOURCES.txt
Normal file
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[console_scripts]
|
||||
postinstall = postinstall:main
|
||||
|
|
@ -0,0 +1 @@
|
|||
pythondialog<4.0.0,>=3.5.3
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
postinstall
|
||||
tests
|
||||
395
deb_dist/postinstall-1.0.1/postinstall/__init__.py
Normal file
395
deb_dist/postinstall-1.0.1/postinstall/__init__.py
Normal file
|
|
@ -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()
|
||||
3
deb_dist/postinstall-1.0.1/postinstall/__main__.py
Normal file
3
deb_dist/postinstall-1.0.1/postinstall/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
25
deb_dist/postinstall-1.0.1/postinstall/logger.py
Normal file
25
deb_dist/postinstall-1.0.1/postinstall/logger.py
Normal file
|
|
@ -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()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue