Старый postinstall

This commit is contained in:
commit 4337a3f94e
349 changed files with 21945 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.idea
.vscode
.venv
code.md
prompt.txt

3
MANIFEST.in Normal file
View file

@ -0,0 +1,3 @@
include README.md
include LICENSE
include postinstall/styles.tcss

230
README.md Normal file
View file

@ -0,0 +1,230 @@
## Документация Postinstall
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
### **1. Сетевые настройки**
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
#### **1.1 Выбор сетевого интерфейса**
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
![Номер лицензии](screenshots/network_interface.png)
#### **1.2 Настройка DHCP**
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
#### **1.3 Настройка статического IP**
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
#### **1.4 Настройка CIDR**
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
![Номер лицензии](screenshots/choose_settings.png)
#### **1.5 Сохранение и отмена изменений**
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
---
### **2. Обновление системы**
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
#### **2.1 Обновление списков пакетов**
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
#### **2.2 Установка обновлений**
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
![Номер лицензии](screenshots/update.png)
---
### **3. Добавить пользователя**
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
#### **3.1 Создание нового пользователя**
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
![Номер лицензии](screenshots/add_user.png)
#### **3.2 Установка пароля**
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
![Номер лицензии](screenshots/user_password.png)
#### **3.3 Назначение прав администратора**
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
![Номер лицензии](screenshots/user_admin.png)
---
### **4. Установка программного обеспечения**
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
#### **4.1 Выбор и установка пакетов**
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
![Номер лицензии](screenshots/install_package.png)
---
### **5. Настройки SSH**
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
#### **5.1 Включение/отключение SSH**
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
#### **5.2 Управление ключами SSH**
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
#### **5.3 Изменение порта SSH**
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
![Номер лицензии](screenshots/ssh.png)
---
### **6. Изменить доменное имя ПК**
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
#### **6.1 Изменение доменного имени**
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
![Номер лицензии](screenshots/domain.png)
---
### **7. Выбор оболочки**
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
#### **7.1 Установка оболочки по умолчанию**
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
#### **7.2 Выбор оболочки**
Вы можете выбрать оболочку из списка доступных в системе.
![Номер лицензии](screenshots/user_terminal.png)
![Номер лицензии](screenshots/choose_terminal.png)
---
### **8. Автозапуск при следующем запуске**
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
#### **8.1 Управление автозапуском Postinstall**
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
---
### **9. Настройка базы данных**
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
#### **9.1 Установка и настройка PostgreSQL**
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
#### **9.2 Логирование выполняемых действий**
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
![Номер лицензии](screenshots/db_installing.png)
![Номер лицензии](screenshots/log_db.png)
![Номер лицензии](screenshots/log_install_db.png)
#### **9.2 Установка дополнительных модулей**
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
![Номер лицензии](screenshots/webmin.png)
---
### **10. Настройка Docker**
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
![Номер лицензии](screenshots/install_docker.png)
![Номер лицензии](screenshots/progressbar_docker.png)
#### **10.1 Установка дополнительных модулей Docker**
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
![Номер лицензии](screenshots/docker_swarm.png)
![Номер лицензии](screenshots/install_portainer.png)
#### **10.2 Автоматическая настройка модулей**
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
![Номер лицензии](screenshots/password_portainer.png)
![Номер лицензии](screenshots/port_portainer.png)
![Номер лицензии](screenshots/ip_portainer.png)
---
### **11. Откат последних изменений**
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
---
### **12. Сохранить и выйти**
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.

View 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()

View 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()

View 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
View 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
View 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
View 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

View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
#!/bin/bash
exec /usr/bin/postinstall

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
debian/patches

View file

@ -0,0 +1 @@
series

View file

@ -0,0 +1 @@
2

View file

@ -0,0 +1,3 @@
include README.md
include LICENSE
include postinstall/styles.tcss

View 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 Выбор сетевого интерфейса**
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
![Номер лицензии](screenshots/network_interface.png)
#### **1.2 Настройка DHCP**
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
#### **1.3 Настройка статического IP**
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
#### **1.4 Настройка CIDR**
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
![Номер лицензии](screenshots/choose_settings.png)
#### **1.5 Сохранение и отмена изменений**
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
---
### **2. Обновление системы**
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
#### **2.1 Обновление списков пакетов**
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
#### **2.2 Установка обновлений**
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
![Номер лицензии](screenshots/update.png)
---
### **3. Добавить пользователя**
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
#### **3.1 Создание нового пользователя**
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
![Номер лицензии](screenshots/add_user.png)
#### **3.2 Установка пароля**
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
![Номер лицензии](screenshots/user_password.png)
#### **3.3 Назначение прав администратора**
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
![Номер лицензии](screenshots/user_admin.png)
---
### **4. Установка программного обеспечения**
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
#### **4.1 Выбор и установка пакетов**
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
![Номер лицензии](screenshots/install_package.png)
---
### **5. Настройки SSH**
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
#### **5.1 Включение/отключение SSH**
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
#### **5.2 Управление ключами SSH**
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
#### **5.3 Изменение порта SSH**
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
![Номер лицензии](screenshots/ssh.png)
---
### **6. Изменить доменное имя ПК**
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
#### **6.1 Изменение доменного имени**
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
![Номер лицензии](screenshots/domain.png)
---
### **7. Выбор оболочки**
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
#### **7.1 Установка оболочки по умолчанию**
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
#### **7.2 Выбор оболочки**
Вы можете выбрать оболочку из списка доступных в системе.
![Номер лицензии](screenshots/user_terminal.png)
![Номер лицензии](screenshots/choose_terminal.png)
---
### **8. Автозапуск при следующем запуске**
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
#### **8.1 Управление автозапуском Postinstall**
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
---
### **9. Настройка базы данных**
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
#### **9.1 Установка и настройка PostgreSQL**
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
#### **9.2 Логирование выполняемых действий**
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
![Номер лицензии](screenshots/db_installing.png)
![Номер лицензии](screenshots/log_db.png)
![Номер лицензии](screenshots/log_install_db.png)
#### **9.2 Установка дополнительных модулей**
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
![Номер лицензии](screenshots/webmin.png)
---
### **10. Настройка Docker**
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
![Номер лицензии](screenshots/install_docker.png)
![Номер лицензии](screenshots/progressbar_docker.png)
#### **10.1 Установка дополнительных модулей Docker**
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
![Номер лицензии](screenshots/docker_swarm.png)
![Номер лицензии](screenshots/install_portainer.png)
#### **10.2 Автоматическая настройка модулей**
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
![Номер лицензии](screenshots/password_portainer.png)
![Номер лицензии](screenshots/port_portainer.png)
![Номер лицензии](screenshots/ip_portainer.png)
---
### **11. Откат последних изменений**
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
---
### **12. Сохранить и выйти**
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.

View file

@ -0,0 +1,230 @@
## Документация Postinstall
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
### **1. Сетевые настройки**
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
#### **1.1 Выбор сетевого интерфейса**
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
![Номер лицензии](screenshots/network_interface.png)
#### **1.2 Настройка DHCP**
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
#### **1.3 Настройка статического IP**
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
#### **1.4 Настройка CIDR**
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
![Номер лицензии](screenshots/choose_settings.png)
#### **1.5 Сохранение и отмена изменений**
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
---
### **2. Обновление системы**
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
#### **2.1 Обновление списков пакетов**
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
#### **2.2 Установка обновлений**
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
![Номер лицензии](screenshots/update.png)
---
### **3. Добавить пользователя**
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
#### **3.1 Создание нового пользователя**
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
![Номер лицензии](screenshots/add_user.png)
#### **3.2 Установка пароля**
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
![Номер лицензии](screenshots/user_password.png)
#### **3.3 Назначение прав администратора**
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
![Номер лицензии](screenshots/user_admin.png)
---
### **4. Установка программного обеспечения**
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
#### **4.1 Выбор и установка пакетов**
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
![Номер лицензии](screenshots/install_package.png)
---
### **5. Настройки SSH**
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
#### **5.1 Включение/отключение SSH**
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
#### **5.2 Управление ключами SSH**
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
#### **5.3 Изменение порта SSH**
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
![Номер лицензии](screenshots/ssh.png)
---
### **6. Изменить доменное имя ПК**
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
#### **6.1 Изменение доменного имени**
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
![Номер лицензии](screenshots/domain.png)
---
### **7. Выбор оболочки**
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
#### **7.1 Установка оболочки по умолчанию**
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
#### **7.2 Выбор оболочки**
Вы можете выбрать оболочку из списка доступных в системе.
![Номер лицензии](screenshots/user_terminal.png)
![Номер лицензии](screenshots/choose_terminal.png)
---
### **8. Автозапуск при следующем запуске**
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
#### **8.1 Управление автозапуском Postinstall**
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
---
### **9. Настройка базы данных**
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
#### **9.1 Установка и настройка PostgreSQL**
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
#### **9.2 Логирование выполняемых действий**
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
![Номер лицензии](screenshots/db_installing.png)
![Номер лицензии](screenshots/log_db.png)
![Номер лицензии](screenshots/log_install_db.png)
#### **9.2 Установка дополнительных модулей**
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
![Номер лицензии](screenshots/webmin.png)
---
### **10. Настройка Docker**
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
![Номер лицензии](screenshots/install_docker.png)
![Номер лицензии](screenshots/progressbar_docker.png)
#### **10.1 Установка дополнительных модулей Docker**
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
![Номер лицензии](screenshots/docker_swarm.png)
![Номер лицензии](screenshots/install_portainer.png)
#### **10.2 Автоматическая настройка модулей**
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
![Номер лицензии](screenshots/password_portainer.png)
![Номер лицензии](screenshots/port_portainer.png)
![Номер лицензии](screenshots/ip_portainer.png)
---
### **11. Откат последних изменений**
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
---
### **12. Сохранить и выйти**
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.

View 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()

View file

@ -0,0 +1,3 @@
import postinstall
postinstall.main()

View 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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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("Изменение доменного имени отменено.")

View file

@ -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()

View file

@ -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()

View 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()

View 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

View file

@ -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()

View file

@ -0,0 +1,11 @@
import os
import sys
def check_root():
if os.geteuid() != 0:
print("Запустите скрипт с правами суперпользователя")
sys.exit(1)
check_root()

View 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()

View 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

View 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()

View file

@ -0,0 +1,3 @@
if [ -n "$PS1" ]; then
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
fi

View 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

View 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

View 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;

View 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

View file

@ -0,0 +1,2 @@
#!/bin/bash
exec /usr/bin/postinstall

View 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

View 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

View file

@ -0,0 +1 @@
9

View 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 Выбор сетевого интерфейса**
.
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
.
![Номер лицензии](screenshots/network_interface.png)
.
.
#### **1.2 Настройка DHCP**
.
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
.
#### **1.3 Настройка статического IP**
.

View file

@ -0,0 +1,2 @@
postinstall_1.0.1-1_amd64.buildinfo python optional
python3-postinstall_1.0.1-1_all.deb python optional

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
python3:Depends=python3-dialog, python3:any
misc:Depends=
misc:Pre-Depends=

View file

@ -0,0 +1,2 @@
/etc/profile.d/0002-postinstall.sh
/etc/systemd/system/postinstall.service

View file

@ -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 Выбор сетевого интерфейса**
.
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
.
![Номер лицензии](screenshots/network_interface.png)
.
.
#### **1.2 Настройка DHCP**
.
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
.
#### **1.3 Настройка статического IP**

View file

@ -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

View 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

View 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

View file

@ -0,0 +1,3 @@
if [ -n "$PS1" ]; then
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
fi

View 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

View 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')())

View 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 Выбор сетевого интерфейса**
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
![Номер лицензии](screenshots/network_interface.png)
#### **1.2 Настройка DHCP**
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
#### **1.3 Настройка статического IP**
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
#### **1.4 Настройка CIDR**
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
![Номер лицензии](screenshots/choose_settings.png)
#### **1.5 Сохранение и отмена изменений**
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
---
### **2. Обновление системы**
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
#### **2.1 Обновление списков пакетов**
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
#### **2.2 Установка обновлений**
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
![Номер лицензии](screenshots/update.png)
---
### **3. Добавить пользователя**
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
#### **3.1 Создание нового пользователя**
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
![Номер лицензии](screenshots/add_user.png)
#### **3.2 Установка пароля**
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
![Номер лицензии](screenshots/user_password.png)
#### **3.3 Назначение прав администратора**
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
![Номер лицензии](screenshots/user_admin.png)
---
### **4. Установка программного обеспечения**
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
#### **4.1 Выбор и установка пакетов**
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
![Номер лицензии](screenshots/install_package.png)
---
### **5. Настройки SSH**
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
#### **5.1 Включение/отключение SSH**
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
#### **5.2 Управление ключами SSH**
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
#### **5.3 Изменение порта SSH**
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
![Номер лицензии](screenshots/ssh.png)
---
### **6. Изменить доменное имя ПК**
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
#### **6.1 Изменение доменного имени**
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
![Номер лицензии](screenshots/domain.png)
---
### **7. Выбор оболочки**
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
#### **7.1 Установка оболочки по умолчанию**
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
#### **7.2 Выбор оболочки**
Вы можете выбрать оболочку из списка доступных в системе.
![Номер лицензии](screenshots/user_terminal.png)
![Номер лицензии](screenshots/choose_terminal.png)
---
### **8. Автозапуск при следующем запуске**
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
#### **8.1 Управление автозапуском Postinstall**
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
---
### **9. Настройка базы данных**
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
#### **9.1 Установка и настройка PostgreSQL**
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
#### **9.2 Логирование выполняемых действий**
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
![Номер лицензии](screenshots/db_installing.png)
![Номер лицензии](screenshots/log_db.png)
![Номер лицензии](screenshots/log_install_db.png)
#### **9.2 Установка дополнительных модулей**
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
![Номер лицензии](screenshots/webmin.png)
---
### **10. Настройка Docker**
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
![Номер лицензии](screenshots/install_docker.png)
![Номер лицензии](screenshots/progressbar_docker.png)
#### **10.1 Установка дополнительных модулей Docker**
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
![Номер лицензии](screenshots/docker_swarm.png)
![Номер лицензии](screenshots/install_portainer.png)
#### **10.2 Автоматическая настройка модулей**
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
![Номер лицензии](screenshots/password_portainer.png)
![Номер лицензии](screenshots/port_portainer.png)
![Номер лицензии](screenshots/ip_portainer.png)
---
### **11. Откат последних изменений**
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
---
### **12. Сохранить и выйти**
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.

View file

@ -0,0 +1,2 @@
[console_scripts]
postinstall = postinstall:main

View 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()

View file

@ -0,0 +1,3 @@
import postinstall
postinstall.main()

View 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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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("Изменение доменного имени отменено.")

View file

@ -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()

View file

@ -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()

View 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()

View 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

View file

@ -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()

View file

@ -0,0 +1,11 @@
import os
import sys
def check_root():
if os.geteuid() != 0:
print("Запустите скрипт с правами суперпользователя")
sys.exit(1)
check_root()

View 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

View file

@ -0,0 +1 @@
3.0 (quilt)

View file

@ -0,0 +1 @@
extend-diff-ignore="\.egg-info$"

View 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 Выбор сетевого интерфейса**
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
![Номер лицензии](screenshots/network_interface.png)
#### **1.2 Настройка DHCP**
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
#### **1.3 Настройка статического IP**
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
#### **1.4 Настройка CIDR**
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
![Номер лицензии](screenshots/choose_settings.png)
#### **1.5 Сохранение и отмена изменений**
Важной особенностью панели является возможность предварительного просмотра изменений. Все настройки сохраняются только после выбора опции "Сохранить и выйти" в главном меню. Это предотвращает применение ненужных или ошибочных настроек и позволяет пользователю отменить изменения до их применения, просто выбрав опцию "Выйти без изменений".
---
### **2. Обновление системы**
Раздел "Обновление системы" предоставляет возможность поддерживать систему актуальной, выполняя обновление списков пакетов и установку доступных обновлений. Этот процесс включает в себя две основные задачи: обновление списков пакетов и установку обновлений.
#### **2.1 Обновление списков пакетов**
Первый шаг — обновление списков пакетов. Эта операция выполняется с использованием команды `apt update`, которая обновляет информацию о доступных пакетах и их версиях, загружая актуальные данные из указанных в системе репозиториев.
#### **2.2 Установка обновлений**
После обновления списков пакетов можно приступить к установке доступных обновлений с помощью команды `apt upgrade`. Эта команда загружает и устанавливает последние версии всех установленных пакетов, что обеспечивает безопасность и стабильность системы. Весь процесс можно контролировать через интерфейс панели, который предоставляет удобное отображение статуса обновлений.
![Номер лицензии](screenshots/update.png)
---
### **3. Добавить пользователя**
В этом разделе реализована функция управления учетными записями пользователей, позволяющая создавать новых пользователей и задавать им права доступа.
#### **3.1 Создание нового пользователя**
Для добавления нового пользователя введите его имя и другие необходимые параметры. Эта операция позволяет администратору создавать учетные записи для сотрудников.
![Номер лицензии](screenshots/add_user.png)
#### **3.2 Установка пароля**
После создания учетной записи необходимо задать пароль для нового пользователя. Это важный шаг, поскольку от него зависит безопасность учетной записи. После необходимо повторить пароль для избежания ввода некорректных данных.
![Номер лицензии](screenshots/user_password.png)
#### **3.3 Назначение прав администратора**
При создании нового пользователя вы можете задать, будет ли у него доступ к административным привилегиям. Для этого выберите соответствующую опцию, которая добавит нового пользователя в группу `sudo`, предоставляя ему право выполнять административные задачи.
![Номер лицензии](screenshots/user_admin.png)
---
### **4. Установка программного обеспечения**
Раздел "Установка программ" предлагает пользователю простой интерфейс для выбора и установки необходимых пакетов программного обеспечения.
#### **4.1 Выбор и установка пакетов**
В этом разделе пользователь может выбрать необходимые программы для установки. Список программ включает в себя часто используемые инструменты. Список пакетов будет расширяться по мере разработки панели.
![Номер лицензии](screenshots/install_package.png)
---
### **5. Настройки SSH**
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
#### **5.1 Включение/отключение SSH**
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
#### **5.2 Управление ключами SSH**
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
#### **5.3 Изменение порта SSH**
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
![Номер лицензии](screenshots/ssh.png)
---
### **6. Изменить доменное имя ПК**
В этом разделе можно изменить доменное имя системы, которое используется для идентификации устройства в сети.
#### **6.1 Изменение доменного имени**
В панели можно легко изменить доменное имя, просто введя новое значение. После подтверждения изменения будут немедленно применены и сохранены в файлах `/etc/hostname` и `/etc/hosts`.
![Номер лицензии](screenshots/domain.png)
---
### **7. Выбор оболочки**
Выбор оболочки (или командной строки) влияет на повседневное взаимодействие с системой. В этом разделе вы можете выбрать предпочтительную оболочку для пользователей системы.
#### **7.1 Установка оболочки по умолчанию**
Оболочку можно установить по умолчанию для всех пользователей или для конкретного пользователя. Панель поддерживает несколько популярных оболочек, таких как Bash и Zsh.
#### **7.2 Выбор оболочки**
Вы можете выбрать оболочку из списка доступных в системе.
![Номер лицензии](screenshots/user_terminal.png)
![Номер лицензии](screenshots/choose_terminal.png)
---
### **8. Автозапуск при следующем запуске**
Этот раздел позволяет управлять автозапуском сервиса Postinstall и других приложений, которые должны запускаться при старте системы.
#### **8.1 Управление автозапуском Postinstall**
Вы можете включить или отключить автозапуск панели Postinstall, что позволит вам контролировать, будет ли панель автоматически запускаться при каждом перезагрузке системы.
---
### **9. Настройка базы данных**
Этот раздел предоставляет возможности для установки и настройки баз данных, в том числе PostgreSQL, с интеграцией с Harlequin.
#### **9.1 Установка и настройка PostgreSQL**
Панель позволяет установить PostgreSQL и интегрировать его с Harlequin, специализированным модулем для работы с базой данных (В последующем раздел будет расширяться для добавления новых баз данных). После установки система также автоматически настраивает необходимые порты в UFW для обеспечения доступа к базе данных.
#### **9.2 Логирование выполняемых действий**
Установка и настройка необходимых инструментов и базы данных логируется и выводится вам для информации.
![Номер лицензии](screenshots/db_installing.png)
![Номер лицензии](screenshots/log_db.png)
![Номер лицензии](screenshots/log_install_db.png)
#### **9.2 Установка дополнительных модулей**
В дополнение к базовой установке, в панели предусмотрена возможность установки дополнительных модулей, таких как Webmin, который может служить удобным веб-интерфейсом для управления базами данных.
![Номер лицензии](screenshots/webmin.png)
---
### **10. Настройка Docker**
Данный раздел пока находится в стадии разработки и будет включать функции для управления Docker-контейнерами и их автоматической конфигурации.
![Номер лицензии](screenshots/install_docker.png)
![Номер лицензии](screenshots/progressbar_docker.png)
#### **10.1 Установка дополнительных модулей Docker**
После установки Docker, предлагается вам установить такие модули как Docker Swarm и Portainer с автоматической настройкой для использования, если необходимо.
![Номер лицензии](screenshots/docker_swarm.png)
![Номер лицензии](screenshots/install_portainer.png)
#### **10.2 Автоматическая настройка модулей**
При установке Portainer вам сразу же устанавливается начальная конфигурация для пользования. С настройкой его пароля, порта и IP адреса.
![Номер лицензии](screenshots/password_portainer.png)
![Номер лицензии](screenshots/port_portainer.png)
![Номер лицензии](screenshots/ip_portainer.png)
---
### **11. Откат последних изменений**
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
---
### **12. Сохранить и выйти**
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.

View 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

View file

@ -0,0 +1,2 @@
[console_scripts]
postinstall = postinstall:main

View file

@ -0,0 +1 @@
pythondialog<4.0.0,>=3.5.3

View file

@ -0,0 +1,2 @@
postinstall
tests

View 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()

View file

@ -0,0 +1,3 @@
import postinstall
postinstall.main()

View 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