Старый postinstall
This commit is contained in:
commit
4337a3f94e
349 changed files with 21945 additions and 0 deletions
1
deb_dist/postinstall-1.5.0/.pc/.quilt_patches
Normal file
1
deb_dist/postinstall-1.5.0/.pc/.quilt_patches
Normal file
|
|
@ -0,0 +1 @@
|
|||
debian/patches
|
||||
1
deb_dist/postinstall-1.5.0/.pc/.quilt_series
Normal file
1
deb_dist/postinstall-1.5.0/.pc/.quilt_series
Normal file
|
|
@ -0,0 +1 @@
|
|||
series
|
||||
1
deb_dist/postinstall-1.5.0/.pc/.version
Normal file
1
deb_dist/postinstall-1.5.0/.pc/.version
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
0
deb_dist/postinstall-1.5.0/.pc/applied-patches
Normal file
0
deb_dist/postinstall-1.5.0/.pc/applied-patches
Normal file
3
deb_dist/postinstall-1.5.0/MANIFEST.in
Normal file
3
deb_dist/postinstall-1.5.0/MANIFEST.in
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
include README.md
|
||||
include LICENSE
|
||||
include postinstall/styles.tcss
|
||||
243
deb_dist/postinstall-1.5.0/PKG-INFO
Normal file
243
deb_dist/postinstall-1.5.0/PKG-INFO
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: postinstall
|
||||
Version: 1.5.0
|
||||
Summary: A Dialog TUI application for postinstall system configuration
|
||||
Author: LLC Technologies and Communication
|
||||
Author-email: devteam@texnico.ru
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent :: Linux
|
||||
Requires-Python: >=3.11
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: pythondialog<4.0.0,>=3.5.3
|
||||
|
||||
## Документация Postinstall
|
||||
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.2 Настройка DHCP**
|
||||
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
||||
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
|
||||
|
||||
#### **1.4 Настройка CIDR**
|
||||
|
||||
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **5. Настройки SSH**
|
||||
|
||||
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
|
||||
|
||||
#### **5.1 Включение/отключение SSH**
|
||||
|
||||
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
|
||||
|
||||
#### **5.2 Управление ключами SSH**
|
||||
|
||||
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
|
||||
|
||||
#### **5.3 Изменение порта SSH**
|
||||
|
||||
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
|
||||
#### **1.2 Настройка DHCP**
|
||||
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
||||
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
|
||||
|
||||
#### **1.4 Настройка CIDR**
|
||||
|
||||
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **5. Настройки SSH**
|
||||
|
||||
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
|
||||
|
||||
#### **5.1 Включение/отключение SSH**
|
||||
|
||||
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
|
||||
|
||||
#### **5.2 Управление ключами SSH**
|
||||
|
||||
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
|
||||
|
||||
#### **5.3 Изменение порта SSH**
|
||||
|
||||
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
395
deb_dist/postinstall-1.5.0/build/lib/postinstall/__init__.py
Normal file
395
deb_dist/postinstall-1.5.0/build/lib/postinstall/__init__.py
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
from dialog import Dialog
|
||||
from postinstall.root import check_root
|
||||
from postinstall.option import (
|
||||
network,
|
||||
update,
|
||||
add_user,
|
||||
install_packages,
|
||||
ssh,
|
||||
hostname,
|
||||
shell,
|
||||
database,
|
||||
docker,
|
||||
)
|
||||
import json
|
||||
|
||||
check_root()
|
||||
|
||||
from postinstall.logger import logger
|
||||
|
||||
# import debugpy
|
||||
|
||||
# debugpy.listen(("0.0.0.0", 5678))
|
||||
# debugpy.wait_for_client()
|
||||
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
HOSTNAME_FILE = "/etc/hostname"
|
||||
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
|
||||
BACKUPS_FILE = "/usr/share/postinstall/backups.json"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_hostname() -> str:
|
||||
try:
|
||||
with open(HOSTNAME_FILE, "r") as f:
|
||||
return f.read().strip()
|
||||
except IOError as e:
|
||||
logging.error(f"Ошибка при чтении hostname: {e}")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def get_ssh_port() -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Не удалось получить порт SSH, используется порт по умолчанию")
|
||||
return "22"
|
||||
|
||||
|
||||
class MenuScreen:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.ssh_port = get_ssh_port()
|
||||
self.hostname = get_hostname()
|
||||
self.ssh_changes = None
|
||||
self.new_hostname = None
|
||||
self.network = None
|
||||
self.autostart_enabled = self.check_auto_start()
|
||||
self.new_autostart_enabled = self.autostart_enabled
|
||||
|
||||
self.options = [
|
||||
{
|
||||
"name": "Сетевые настройки",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "network",
|
||||
},
|
||||
{
|
||||
"name": "Обновление системы",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "update",
|
||||
},
|
||||
{
|
||||
"name": "Добавить пользователя",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "add_user",
|
||||
},
|
||||
{
|
||||
"name": "Установка программ",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "install_packages",
|
||||
},
|
||||
{
|
||||
"name": "Настройки ssh",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "ssh",
|
||||
},
|
||||
{
|
||||
"name": "Изменить доменное имя ПК",
|
||||
"value": self.hostname,
|
||||
"tabs": "\t \t",
|
||||
"option": "hostname",
|
||||
},
|
||||
{"name": "Выбор оболочки", "value": "", "tabs": "\t \t", "option": "shell"},
|
||||
{
|
||||
"name": "Автозапуск при следующем запуске",
|
||||
"value": "Включен" if self.new_autostart_enabled else "Отключен",
|
||||
"tabs": "\t",
|
||||
"option": "auto_start",
|
||||
},
|
||||
{
|
||||
"name": "Настройка базы данных",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "database",
|
||||
},
|
||||
{
|
||||
"name": "Настройка docker",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "docker",
|
||||
},
|
||||
{
|
||||
"name": "Откат последних изменений",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "rollback",
|
||||
},
|
||||
{
|
||||
"name": "Сохранить и выйти",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "save_and_exit",
|
||||
},
|
||||
]
|
||||
|
||||
os.makedirs(os.path.dirname(AUTOSTART_FILE), exist_ok=True)
|
||||
|
||||
def show_menu(self):
|
||||
choices = []
|
||||
for i, option in enumerate(self.options, 1):
|
||||
value = option["value"]
|
||||
if option["option"] == "hostname":
|
||||
value = (
|
||||
self.new_hostname
|
||||
if self.new_hostname is not None
|
||||
else self.hostname
|
||||
)
|
||||
if self.new_hostname is not None:
|
||||
value = f"{value} (не применено)"
|
||||
elif option["option"] == "network":
|
||||
value = "Изменено (не применено)" if self.network else ""
|
||||
|
||||
choice_text = f"{option['name']}{option['tabs']}{value}"
|
||||
choices.append((str(i), choice_text))
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите опцию:",
|
||||
choices=choices,
|
||||
title="Главное меню",
|
||||
cancel_label="Выйти без сохранения",
|
||||
width=70,
|
||||
height=15,
|
||||
)
|
||||
|
||||
if code == self.d.OK and tag:
|
||||
return self.options[int(tag) - 1]["option"]
|
||||
return None
|
||||
|
||||
def set_temp_network(self, settings):
|
||||
self.temp_network = settings
|
||||
|
||||
def check_auto_start(self) -> bool:
|
||||
return not os.path.exists(AUTOSTART_FILE)
|
||||
|
||||
def set_auto_start(self, enable: bool) -> None:
|
||||
try:
|
||||
if enable:
|
||||
if os.path.exists(AUTOSTART_FILE):
|
||||
os.remove(AUTOSTART_FILE)
|
||||
logging.info("Автозапуск включен")
|
||||
else:
|
||||
with open(AUTOSTART_FILE, "w") as f:
|
||||
f.write("# Это файл создан для отключения автозапуска\n")
|
||||
logging.info("Автозапуск отключен")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении автозапуска: {e}")
|
||||
|
||||
def update_ssh_port(self, new_port):
|
||||
self.new_ssh_port = new_port
|
||||
|
||||
def update_hostname(self, new_hostname):
|
||||
self.new_hostname = new_hostname
|
||||
|
||||
def update_autostart(self, new_state):
|
||||
self.new_autostart_enabled = new_state
|
||||
|
||||
|
||||
def choose(option, menu):
|
||||
if menu.new_autostart_enabled != menu.autostart_enabled:
|
||||
menu.set_auto_start(menu.new_autostart_enabled)
|
||||
if option == "network":
|
||||
net_config = network.NetworkConfig()
|
||||
if net_config.run():
|
||||
menu.network = net_config
|
||||
elif option == "update":
|
||||
logging.info("Начало обновления системы.")
|
||||
update.run_update()
|
||||
logging.info("Завершено обновление системы.")
|
||||
elif option == "add_user":
|
||||
logging.info("Началось добавления пользователя.")
|
||||
add_user.run_add_user()
|
||||
logging.info("Завершено добавление пользователя.")
|
||||
elif option == "install_packages":
|
||||
logging.info("Начало установки пакетов.")
|
||||
install_packages.run_install_packages()
|
||||
logging.info("Завершена установка пакетов.")
|
||||
elif option == "ssh":
|
||||
ssh_config, changes_made = ssh.run_ssh_config()
|
||||
if changes_made:
|
||||
menu.ssh_changes = ssh_config
|
||||
elif option == "hostname":
|
||||
new_hostname = hostname.run_change_hostname()
|
||||
if new_hostname:
|
||||
menu.new_hostname = new_hostname
|
||||
menu.options[5]["value"] = f"{new_hostname} (не применено)"
|
||||
elif option == "shell":
|
||||
logging.info("Началось изменение shell.")
|
||||
shell.run_change_shell()
|
||||
logging.info("Завершено изменение shell.")
|
||||
elif option == "auto_start":
|
||||
menu.new_autostart_enabled = not menu.new_autostart_enabled
|
||||
menu.options[7]["value"] = (
|
||||
"Включен" if menu.new_autostart_enabled else "Отключен"
|
||||
)
|
||||
logging.info(
|
||||
f"Автозапуск {'включен' if menu.new_autostart_enabled else 'отключен'}"
|
||||
)
|
||||
elif option == "database":
|
||||
logging.info("Запуск настройки базы данных.")
|
||||
|
||||
# Открытие диалогового окна для выбора базы данных
|
||||
code, choice = menu.d.menu("Выберите базу данных для установки:",
|
||||
choices=[("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB")])
|
||||
|
||||
if code == menu.d.OK:
|
||||
if choice == "PostgreSQL":
|
||||
if not database.is_installed('postgresql-16'):
|
||||
database.install_postgresql(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("PostgreSQL и Harlequin уже установлены.")
|
||||
|
||||
elif choice == "MariaDB":
|
||||
if not database.is_installed('mariadb-server'):
|
||||
database.install_mariadb(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("MariaDB уже установлена.")
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if not database.is_installed('webmin'):
|
||||
code = menu.d.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
if code == menu.d.OK:
|
||||
database.install_webmin(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("Webmin уже установлен.")
|
||||
|
||||
logging.info("Настройка базы данных завершена.")
|
||||
else:
|
||||
menu.d.msgbox("Выбор базы данных отменён")
|
||||
elif option == "docker":
|
||||
docker.run()
|
||||
|
||||
elif option == "rollback":
|
||||
try:
|
||||
with open(BACKUPS_FILE, "r") as f:
|
||||
backups = json.load(f)
|
||||
f.close()
|
||||
for i in backups:
|
||||
with open(i["file"], "w") as f:
|
||||
f.write(i["content"])
|
||||
f.close()
|
||||
subprocess.run(["systemctl", "restart", "networking"], check=True)
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
ssh_port = get_ssh_port()
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={ssh_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
|
||||
menu.d.msgbox("Резервные копии успешно восстановлены")
|
||||
logging.info("Резервные копии успешно восстановлены")
|
||||
except Exception as e:
|
||||
menu.d.msgbox(f"Вы еще не применяли изменения.{e}")
|
||||
return False
|
||||
|
||||
elif option == "save_and_exit":
|
||||
with open("/etc/network/interfaces", "r") as f:
|
||||
interfaces = f.read()
|
||||
f.close()
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
dns_settings = f.read()
|
||||
f.close()
|
||||
with open("/etc/ssh/sshd_config", "r") as f:
|
||||
ssh_settings = f.read()
|
||||
f.close()
|
||||
try:
|
||||
with open("/etc/ssh/sshd_config.d/auth.conf", "r") as f:
|
||||
ssh_auth = f.read()
|
||||
except Exception:
|
||||
ssh_auth = ""
|
||||
backup = [
|
||||
{
|
||||
"file": "/etc/network/interfaces",
|
||||
"content": interfaces,
|
||||
},
|
||||
{
|
||||
"file": "/etc/resolv.conf",
|
||||
"content": dns_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config",
|
||||
"content": ssh_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config.d/auth.conf",
|
||||
"content": ssh_auth,
|
||||
},
|
||||
]
|
||||
|
||||
with open(BACKUPS_FILE, "w") as f:
|
||||
f.write(json.dumps(backup))
|
||||
f.close()
|
||||
|
||||
if menu.ssh_changes:
|
||||
logging.info(
|
||||
f"Применены новые настройки SSH. {'Новый порт SSH: ' + str(menu.ssh_changes.new_port) if menu.ssh_changes.new_port is not None else ''} {'Новый метод авторизации: ' + menu.ssh_changes.new_auth_method if menu.ssh_changes.new_auth_method is not None else ''}"
|
||||
)
|
||||
menu.ssh_changes.apply_changes()
|
||||
if menu.new_hostname:
|
||||
logging.info(f"Применено новое имя хоста: {menu.new_hostname}")
|
||||
if hostname.apply_hostname_change(menu.new_hostname):
|
||||
menu.hostname = menu.new_hostname
|
||||
if menu.network:
|
||||
logging.info(
|
||||
f"Применены новые настройки сети: {menu.network.temp_network_settings}"
|
||||
)
|
||||
if not menu.network.save_and_apply_network_settings():
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
logging.info("Приложение запущено")
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
menu = MenuScreen()
|
||||
while True:
|
||||
option = menu.show_menu()
|
||||
if option is None:
|
||||
break
|
||||
if not choose(option, menu):
|
||||
break
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
25
deb_dist/postinstall-1.5.0/build/lib/postinstall/logger.py
Normal file
25
deb_dist/postinstall-1.5.0/build/lib/postinstall/logger.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
def setup_logger():
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s")
|
||||
|
||||
file_handler = logging.FileHandler("/var/log/postinstall.log")
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
class UserFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.user = getpass.getuser()
|
||||
return True
|
||||
|
||||
file_handler.addFilter(UserFilter())
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import pwd
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_password_with_toggle(d, title):
|
||||
"""Функция для ввода пароля с возможностью показать/скрыть пароль"""
|
||||
password_hidden = True
|
||||
password = ""
|
||||
|
||||
# Фиксированная ширина окна
|
||||
window_width = 60
|
||||
|
||||
while True:
|
||||
if password_hidden:
|
||||
# Используем insecure=True для показа скрытого пароля снежинками
|
||||
code, entered_password = d.passwordbox(
|
||||
title, insecure=True, init=password, width=window_width,
|
||||
extra_button=True, extra_label="Показать пароль"
|
||||
)
|
||||
else:
|
||||
# Используем insecure=False для показа пароля открытым текстом
|
||||
code, entered_password = d.inputbox(
|
||||
title, init=password, width=window_width,
|
||||
extra_button=True, extra_label="Скрыть пароль"
|
||||
)
|
||||
|
||||
if code == d.EXTRA:
|
||||
password_hidden = not password_hidden
|
||||
password = entered_password # Сохраняем введённый пароль
|
||||
continue # Перезапускаем окно с новым состоянием
|
||||
|
||||
if code == d.CANCEL:
|
||||
return None
|
||||
|
||||
# Обновляем сохранённый пароль
|
||||
password = entered_password
|
||||
return password
|
||||
|
||||
|
||||
def add_user():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
fields = [
|
||||
("Имя пользователя", 1, 1, "", 1, 20, 30, 0),
|
||||
("Полное имя", 2, 1, "", 2, 20, 30, 0),
|
||||
("Мобильный телефон", 3, 1, "", 3, 20, 30, 0),
|
||||
("Городской телефон", 4, 1, "", 4, 20, 30, 0),
|
||||
("Кабинет", 5, 1, "", 5, 20, 30, 0),
|
||||
]
|
||||
|
||||
code, values = d.form("Введите данные нового пользователя:", fields)
|
||||
|
||||
if code != d.OK:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
username, fullname, mobile, city_phone, cabinet = values
|
||||
|
||||
# Проверка существования пользователя
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
d.msgbox(f"Пользователь {username} уже существует.")
|
||||
return
|
||||
except KeyError:
|
||||
pass # Пользователь не существует, продолжаем
|
||||
|
||||
# Запрос пароля с возможностью его показать/скрыть
|
||||
password = get_password_with_toggle(d, "Введите пароль для нового пользователя")
|
||||
if not password:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
# Запрос подтверждения пароля с возможностью его показать/скрыть
|
||||
confirm_password = get_password_with_toggle(d, "Подтвердите пароль")
|
||||
if not confirm_password:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
if password != confirm_password:
|
||||
d.msgbox("Пароли не совпадают. Попробуйте снова.")
|
||||
return
|
||||
|
||||
# Запрос прав администратора
|
||||
code = d.yesno("Сделать пользователя администратором?")
|
||||
is_admin = code == d.OK
|
||||
|
||||
try:
|
||||
# Создание пользователя
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"useradd",
|
||||
"-m",
|
||||
"-c",
|
||||
f"{fullname},{cabinet},{city_phone},{mobile}",
|
||||
"-s",
|
||||
"/bin/bash",
|
||||
username,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Установка пароля
|
||||
process = subprocess.Popen(
|
||||
["sudo", "passwd", username],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
process.communicate(input=f"{password}\n{password}\n".encode())
|
||||
|
||||
# Добавление в группу sudo, если пользователь - администратор
|
||||
if is_admin:
|
||||
subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True)
|
||||
|
||||
logging.info(
|
||||
f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}"
|
||||
)
|
||||
|
||||
d.msgbox(f"Пользователь {username} успешно добавлен.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при добавлении пользователя: {e}")
|
||||
d.msgbox(f"Ошибка при добавлении пользователя: {e}")
|
||||
|
||||
|
||||
def run_add_user():
|
||||
add_user()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_add_user()
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import os
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
|
||||
|
||||
|
||||
class AutoStartScreen:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
|
||||
def check_auto_start(self) -> bool:
|
||||
return not os.path.exists(AUTOSTART_FILE)
|
||||
|
||||
def set_auto_start(self, enable: bool) -> None:
|
||||
try:
|
||||
if enable:
|
||||
try:
|
||||
if os.path.exists(AUTOSTART_FILE):
|
||||
os.remove(AUTOSTART_FILE)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при включении автозапуска: {e}")
|
||||
else:
|
||||
with open(AUTOSTART_FILE, "w") as f:
|
||||
f.write("# Это файл создан для отключения автозапуска\n")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении автозапуска: {e}")
|
||||
|
||||
def show_screen(self):
|
||||
auto_start_enabled = self.check_auto_start()
|
||||
|
||||
code, tags = self.d.checklist(
|
||||
"Выберите действие",
|
||||
choices=[
|
||||
("autostart", "Автозапуск при следующем запуске", auto_start_enabled)
|
||||
],
|
||||
title="Настройки автозапуска",
|
||||
backtitle="Postinstall Configuration",
|
||||
ok_label="Применить",
|
||||
cancel_label="Назад",
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
self.set_auto_start("autostart" in tags)
|
||||
self.d.msgbox("Изменения сохранены.")
|
||||
else:
|
||||
logging.info("Изменения отменены пользователем.")
|
||||
|
||||
|
||||
def main():
|
||||
auto_start_screen = AutoStartScreen()
|
||||
auto_start_screen.show_screen()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
|
||||
|
||||
def main():
|
||||
dialog = Dialog(dialog="dialog")
|
||||
|
||||
# Диалоговое окно с выбором базы данных
|
||||
code, choice = dialog.menu(
|
||||
"Выберите базу данных для установки:",
|
||||
choices=[
|
||||
("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB"),
|
||||
],
|
||||
)
|
||||
|
||||
if code == dialog.OK:
|
||||
dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён")
|
||||
if choice == "PostgreSQL":
|
||||
install_postgresql(dialog)
|
||||
elif choice == "MariaDB":
|
||||
install_mariadb(dialog)
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if (
|
||||
dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
== dialog.OK
|
||||
):
|
||||
install_webmin(dialog)
|
||||
else:
|
||||
dialog.msgbox("Выбор базы данных отменён", title="Отмена")
|
||||
|
||||
|
||||
def is_installed(package_name):
|
||||
"""Проверяет, установлен ли пакет"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["dpkg-query", "-W", "-f=${Status}", package_name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return "install ok installed" in result.stdout
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, title):
|
||||
"""Запуск команды с выводом лога в диалоговое окно"""
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
log_output = []
|
||||
for line in iter(process.stdout.readline, ""):
|
||||
log_output.append(line.strip())
|
||||
|
||||
stderr_output = process.stderr.read().strip()
|
||||
if stderr_output:
|
||||
log_output.append(stderr_output)
|
||||
|
||||
process.stdout.close()
|
||||
process.stderr.close()
|
||||
process.wait()
|
||||
|
||||
dialog.msgbox("\n".join(log_output), title=title, height=20, width=60)
|
||||
return process.returncode == 0
|
||||
|
||||
|
||||
def install_package(dialog, package_name):
|
||||
"""Устанавливает указанный пакет с выводом лога"""
|
||||
dialog.infobox(f"Начало установки {package_name}...")
|
||||
|
||||
if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"):
|
||||
dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.")
|
||||
return
|
||||
|
||||
if not run_command_with_log(
|
||||
dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}"
|
||||
):
|
||||
dialog.msgbox(f"Ошибка при установке {package_name}.")
|
||||
return
|
||||
|
||||
dialog.msgbox(f"{package_name} успешно установлен.")
|
||||
|
||||
|
||||
def install_postgresql(dialog):
|
||||
"""Установка PostgreSQL и Harlequin"""
|
||||
if not is_installed("postgresql-16"):
|
||||
install_package(dialog, "postgresql-16")
|
||||
else:
|
||||
dialog.msgbox("PostgreSQL уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin-postgres"):
|
||||
install_package(dialog, "python3-harlequin-postgres")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin-postgres уже установлен.")
|
||||
|
||||
|
||||
def install_mariadb(dialog):
|
||||
"""Установка MariaDB"""
|
||||
if not is_installed("mariadb-server"):
|
||||
install_package(dialog, "mariadb-server")
|
||||
else:
|
||||
dialog.msgbox("MariaDB уже установлена.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
|
||||
def install_webmin(dialog):
|
||||
"""Установка Webmin и дополнительных пакетов"""
|
||||
if not is_installed("webmin"):
|
||||
install_package(dialog, "webmin")
|
||||
|
||||
# Установка cpanminus
|
||||
install_package(dialog, "postgresql-server-dev-16")
|
||||
install_package(dialog, "cpanminus")
|
||||
|
||||
run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
dialog.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# Установка модулей DBI и DBD::Pg через cpanminus
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI"
|
||||
):
|
||||
dialog.msgbox("Модуль DBI успешно установлен.")
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg"
|
||||
):
|
||||
dialog.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
dialog.msgbox("Webmin успешно перезагружен.")
|
||||
else:
|
||||
dialog.msgbox("Webmin уже установлен.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Callable
|
||||
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
import socket
|
||||
|
||||
REGISTRY_URL = "dr.gosrv.ru"
|
||||
REGISTRY_LOGIN = "anon_user"
|
||||
REGISTRY_PASSWORD = "Qweasdzxc123C@"
|
||||
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
|
||||
|
||||
|
||||
class CommandExecutor:
|
||||
@staticmethod
|
||||
def run_command(
|
||||
command: str,
|
||||
shell: bool = False,
|
||||
input_data: Optional[str] = None,
|
||||
ignore_errors: List[int] = [],
|
||||
) -> subprocess.CompletedProcess:
|
||||
try:
|
||||
return subprocess.run(
|
||||
command if shell else shlex.split(command),
|
||||
shell=shell,
|
||||
check=True,
|
||||
input=input_data,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode not in ignore_errors:
|
||||
logger.error(f"Ошибка выполнения команды: {e}")
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}"
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
class SystemChecker:
|
||||
@staticmethod
|
||||
def is_command_installed(command: str) -> bool:
|
||||
try:
|
||||
return CommandExecutor.run_command(f"which {command}").returncode == 0
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
class ProgressGauge:
|
||||
def __init__(self, dialog: Dialog, title: str, total_steps: int):
|
||||
self.dialog = dialog
|
||||
self.title = title
|
||||
self.total_steps = total_steps
|
||||
self.current_step = 0
|
||||
|
||||
def update(self, step: int, message: str):
|
||||
self.current_step = step
|
||||
percent = int((step / self.total_steps) * 100)
|
||||
self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True)
|
||||
|
||||
def __enter__(self):
|
||||
self.dialog.gauge_start(self.title, percent=0)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.dialog.gauge_stop()
|
||||
|
||||
|
||||
class DockerManager:
|
||||
@staticmethod
|
||||
def install(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Установка Docker...")
|
||||
progress_callback(1, "Обновление пакетов")
|
||||
CommandExecutor.run_command("apt-get update")
|
||||
progress_callback(2, "Установка Docker")
|
||||
CommandExecutor.run_command("apt-get install -y docker.io")
|
||||
progress_callback(3, "Установка Docker Compose")
|
||||
CommandExecutor.run_command("apt-get install -y docker-compose")
|
||||
progress_callback(4, "Выполнение входа в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(5, "Docker успешно установлен")
|
||||
logger.info("Docker успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Docker...")
|
||||
progress_callback(1, "Удаление Docker и Docker Compose")
|
||||
CommandExecutor.run_command("apt-get remove -y docker.io docker-compose")
|
||||
progress_callback(2, "Очистка зависимостей")
|
||||
CommandExecutor.run_command("apt-get autoremove -y")
|
||||
progress_callback(3, "Удаление файлов Docker")
|
||||
CommandExecutor.run_command("rm -rf /var/run/docker")
|
||||
CommandExecutor.run_command("rm -rf /var/lib/docker")
|
||||
progress_callback(4, "Docker успешно удален")
|
||||
logger.info("Docker успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def login_to_registry():
|
||||
logger.info("Выполняется вход в Docker registry...")
|
||||
try:
|
||||
result = CommandExecutor.run_command(
|
||||
f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}",
|
||||
shell=True,
|
||||
)
|
||||
logger.info("Успешный вход в Docker registry")
|
||||
logger.debug(f"Docker login output: {result.stdout}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка входа в Docker registry: {e}")
|
||||
logger.error(f"Stderr: {e.stderr}")
|
||||
raise
|
||||
|
||||
|
||||
class UFWManager:
|
||||
@staticmethod
|
||||
def add_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Добавление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Удаление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено"
|
||||
)
|
||||
|
||||
|
||||
class PortainerManager:
|
||||
@staticmethod
|
||||
def install(
|
||||
port: str, password: str, ip: str, progress_callback: Callable[[int, str], None]
|
||||
):
|
||||
logger.info("Установка Portainer...")
|
||||
progress_callback(1, "Вход в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(2, "Остановка предыдущей версии Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(3, "Удаление старых данных Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
"docker volume ls -q -f name=portainer* | xargs -r docker volume rm",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(4, "Установка apache2-utils для htpasswd")
|
||||
if not SystemChecker.is_command_installed("htpasswd"):
|
||||
CommandExecutor.run_command("apt-get install -y apache2-utils")
|
||||
|
||||
progress_callback(5, "Создание конфигурации Portainer")
|
||||
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
|
||||
password_hash = PortainerManager._generate_password_hash(password)
|
||||
PortainerManager._create_docker_compose_file(port, password_hash, ip)
|
||||
|
||||
progress_callback(6, "Запуск Portainer")
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d"
|
||||
)
|
||||
|
||||
# if ip != "127.0.0.1":
|
||||
if True:
|
||||
progress_callback(7, "Добавление правила UFW")
|
||||
UFWManager.add_rule(port)
|
||||
|
||||
progress_callback(8, "Portainer успешно установлен")
|
||||
logger.info("Portainer успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Portainer...")
|
||||
progress_callback(1, "Остановка Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
progress_callback(2, "Удаление файлов конфигурации")
|
||||
CommandExecutor.run_command(
|
||||
f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"
|
||||
)
|
||||
# try:
|
||||
# with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f:
|
||||
# config = f.read()
|
||||
# port_ip_match = re.search(r'"(.+):(\d+):9000"', config)
|
||||
# if port_ip_match:
|
||||
# ip, port = port_ip_match.groups()
|
||||
# # if True:
|
||||
# # progress_callback(3, "Удаление правила UFW")
|
||||
# # UFWManager.remove_rule(port)
|
||||
# except FileNotFoundError:
|
||||
# logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.")
|
||||
|
||||
progress_callback(4, "Portainer успешно удален")
|
||||
logger.info("Portainer успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def _generate_password_hash(password: str) -> str:
|
||||
return CommandExecutor.run_command(
|
||||
f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'",
|
||||
shell=True,
|
||||
).stdout.strip()
|
||||
|
||||
@staticmethod
|
||||
def _create_docker_compose_file(port: str, password_hash: str, ip: str):
|
||||
docker_compose_content = f"""
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: {REGISTRY_URL}/base_images/portainer:latest
|
||||
ports:
|
||||
- "{ip}:{port}:9000"
|
||||
command: --admin-password '{password_hash}'
|
||||
networks:
|
||||
- local
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- portainer-data:/data
|
||||
environment:
|
||||
- PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1
|
||||
deploy:
|
||||
mode: global
|
||||
|
||||
networks:
|
||||
local:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
portainer-data:
|
||||
"""
|
||||
with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f:
|
||||
f.write(docker_compose_content)
|
||||
|
||||
|
||||
class InputValidator:
|
||||
@staticmethod
|
||||
def validate(value: str, pattern: str) -> bool:
|
||||
return bool(re.match(pattern, value))
|
||||
|
||||
|
||||
class UIElement(ABC):
|
||||
def __init__(self, dialog: Dialog):
|
||||
self.dialog = dialog
|
||||
|
||||
@abstractmethod
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
|
||||
class Menu(UIElement):
|
||||
def show(self):
|
||||
choices = []
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
choices.append(("1", "Установить Docker"))
|
||||
else:
|
||||
choices.append(("1", "Удалить Docker"))
|
||||
|
||||
if SystemChecker.is_command_installed("docker"):
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
choices.append(("2", "Установить Portainer"))
|
||||
else:
|
||||
choices.append(("2", "Удалить Portainer"))
|
||||
|
||||
code, choice = self.dialog.menu("Выберите действие:", choices=choices)
|
||||
return code, choice
|
||||
|
||||
|
||||
class YesNoUI(UIElement):
|
||||
def show(self, text: str):
|
||||
return self.dialog.yesno(text) == self.dialog.OK
|
||||
|
||||
|
||||
class PasswordInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, password = self.dialog.passwordbox(
|
||||
"Введите пароль для Portainer:", insecure=True
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(password, r"^.{12,}$"):
|
||||
return password
|
||||
self.dialog.msgbox(
|
||||
"Пароль должен содержать минимум 12 символов.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class PortInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, port = self.dialog.inputbox(
|
||||
"Введите порт для Portainer:", init="9000"
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"):
|
||||
return port
|
||||
self.dialog.msgbox(
|
||||
"Некорректный порт. Введите число от 1 до 65535.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class IPInput(UIElement):
|
||||
def get_default_interface_ip(self):
|
||||
try:
|
||||
# Выполнение команды для получения IP-адреса основного интерфейса
|
||||
command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1"
|
||||
result = subprocess.check_output(command, shell=True, text=True)
|
||||
return result.strip() # Возвращаем IP-адрес без лишних символов
|
||||
except subprocess.CalledProcessError:
|
||||
return "127.0.0.1" # В случае ошибки вернём стандартный IP
|
||||
|
||||
def show(self):
|
||||
# Получаем IP-адрес основного сетевого интерфейса
|
||||
default_ip = self.get_default_interface_ip()
|
||||
|
||||
while True:
|
||||
# Используем полученный IP-адрес как начальное значение
|
||||
code, ip = self.dialog.inputbox(
|
||||
"Введите IP адрес для Portainer:", init=default_ip
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"):
|
||||
return ip
|
||||
self.dialog.msgbox(
|
||||
"Некорректный IP адрес. Введите адрес в формате X.X.X.X",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
|
||||
|
||||
class NetworkInterfaceInput(UIElement):
|
||||
def get_network_interfaces(self):
|
||||
try:
|
||||
# Get all network interfaces
|
||||
interfaces = subprocess.check_output(
|
||||
"ip -o link show", shell=True, text=True
|
||||
)
|
||||
# Extract interface names and their IP addresses
|
||||
interface_list = []
|
||||
for line in interfaces.split("\n"):
|
||||
if line:
|
||||
interface = line.split(":")[1].strip()
|
||||
ip = self.get_ip_address(interface)
|
||||
if ip:
|
||||
interface_list.append((interface, ip))
|
||||
return interface_list
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def get_ip_address(self, interface):
|
||||
try:
|
||||
ip = subprocess.check_output(
|
||||
f"ip -o -4 addr list {interface}", shell=True, text=True
|
||||
)
|
||||
return ip.split()[3].split("/")[0]
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def show(self):
|
||||
interfaces = self.get_network_interfaces()
|
||||
if not interfaces:
|
||||
self.dialog.msgbox(
|
||||
"Не удалось получить список сетевых интерфейсов.", width=50, height=10
|
||||
)
|
||||
return None
|
||||
|
||||
choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces]
|
||||
code, choice = self.dialog.menu(
|
||||
"Выберите сетевой интерфейс для Docker Swarm:", choices=choices
|
||||
)
|
||||
|
||||
if code == self.dialog.OK:
|
||||
return choice
|
||||
return None
|
||||
|
||||
|
||||
class DockerConfigUI:
|
||||
def __init__(self):
|
||||
self.dialog = Dialog(dialog="dialog")
|
||||
self.menu = Menu(self.dialog)
|
||||
self.password_input = PasswordInput(self.dialog)
|
||||
self.port_input = PortInput(self.dialog)
|
||||
self.ip_input = IPInput(self.dialog)
|
||||
self.network_interface_input = NetworkInterfaceInput(self.dialog)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
code, choice = self.menu.show()
|
||||
if code == self.dialog.OK:
|
||||
if choice == "1":
|
||||
self._handle_docker()
|
||||
elif choice == "2":
|
||||
self.handle_portainer()
|
||||
elif choice == "3":
|
||||
break
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Произошла ошибка: {e}")
|
||||
self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10)
|
||||
|
||||
def _handle_docker(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge:
|
||||
DockerManager.install(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно установлен.", width=50, height=10)
|
||||
logger.info("Docker успешно установлен.")
|
||||
yes_no_ui = YesNoUI(self.dialog)
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"
|
||||
):
|
||||
interface = self.network_interface_input.show()
|
||||
if interface:
|
||||
ip = interface.split("(")[1].split(")")[0]
|
||||
if ip:
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker swarm init --advertise-addr {ip}"
|
||||
)
|
||||
self.dialog.msgbox(
|
||||
f"Docker Swarm успешно инициализирован на интерфейсе {interface}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при инициализации Docker Swarm: {str(e)}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
f"Не удалось получить IP-адрес для интерфейса {interface}.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
"Инициализация Docker Swarm отменена.", width=50, height=10
|
||||
)
|
||||
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?"
|
||||
):
|
||||
self.handle_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
DockerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно удален.", width=50, height=10)
|
||||
logger.info("Docker успешно удален.")
|
||||
|
||||
def handle_portainer(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
self.dialog.msgbox(
|
||||
"Docker не установлен. Пожалуйста, сначала установите Docker.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
return
|
||||
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
self._install_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Portainer успешно удален.", width=50, height=10)
|
||||
logger.info("Portainer успешно удален.")
|
||||
|
||||
def _install_portainer(self):
|
||||
password = self.password_input.show()
|
||||
if not password:
|
||||
return
|
||||
|
||||
port = self.port_input.show()
|
||||
if not port:
|
||||
return
|
||||
|
||||
ip = self.ip_input.show()
|
||||
if not ip:
|
||||
return
|
||||
|
||||
try:
|
||||
with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge:
|
||||
PortainerManager.install(port, password, ip, gauge.update)
|
||||
self.dialog.msgbox(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
logger.info(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль"
|
||||
)
|
||||
except Exception as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при установке Portainer: {str(e)}", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
ui = DockerConfigUI()
|
||||
ui.run()
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import os
|
||||
|
||||
HOSTNAME_FILE = "/etc/hostname"
|
||||
HOSTS_FILE = "/etc/hosts"
|
||||
|
||||
|
||||
def get_current_hostname():
|
||||
try:
|
||||
with open(HOSTNAME_FILE, 'r') as f:
|
||||
return f.read().strip()
|
||||
except IOError as e:
|
||||
print(f"Ошибка при чтении hostname: {e}")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def change_hostname():
|
||||
d = Dialog(dialog="dialog")
|
||||
current_hostname = get_current_hostname()
|
||||
|
||||
code, new_hostname = d.inputbox(
|
||||
f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:",
|
||||
init=current_hostname
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return None
|
||||
|
||||
if new_hostname == current_hostname:
|
||||
d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.")
|
||||
return None
|
||||
|
||||
# Проверка валидности hostname
|
||||
if not is_valid_hostname(new_hostname):
|
||||
d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.")
|
||||
return None
|
||||
|
||||
# Подтверждение изменения
|
||||
code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?")
|
||||
if code != d.OK:
|
||||
return None
|
||||
|
||||
return new_hostname
|
||||
|
||||
|
||||
def is_valid_hostname(hostname):
|
||||
if len(hostname) > 63:
|
||||
return False
|
||||
if hostname[-1] == "-":
|
||||
return False
|
||||
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-")
|
||||
return all(char in allowed for char in hostname)
|
||||
|
||||
|
||||
def apply_hostname_change(new_hostname):
|
||||
try:
|
||||
subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True)
|
||||
if os.path.exists(HOSTS_FILE):
|
||||
subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_change_hostname():
|
||||
return change_hostname()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
new_hostname = run_change_hostname()
|
||||
if new_hostname:
|
||||
print(f"Новое доменное имя: {new_hostname}")
|
||||
else:
|
||||
print("Изменение доменного имени отменено.")
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
AVAILABLE_PACKAGES = [
|
||||
"neofetch",
|
||||
"webmin",
|
||||
"usermin",
|
||||
"cups",
|
||||
"wireguard",
|
||||
"wg-cmd",
|
||||
]
|
||||
|
||||
|
||||
def check_sudo():
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, message):
|
||||
"""Run a command and log output, showing a message box on success."""
|
||||
try:
|
||||
subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
logging.info(f"{message}: успешно выполнено.")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"{message}: ошибка - {e}")
|
||||
dialog.msgbox(f"{message}: ошибка.\n{e}")
|
||||
return False
|
||||
|
||||
|
||||
def install_packages():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
if not check_sudo():
|
||||
d.msgbox("Для установки пакетов необходимы права суперпользователя.")
|
||||
return
|
||||
|
||||
# Создаем список пакетов для выбора
|
||||
choices = [(package, "", False) for package in AVAILABLE_PACKAGES]
|
||||
|
||||
# Показываем checklist для выбора пакетов
|
||||
code, tags = d.checklist(
|
||||
"Выберите пакеты для установки:", choices=choices, title="Установка программ"
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
if not tags:
|
||||
d.msgbox("Вы не выбрали ни одного пакета для установки.")
|
||||
return
|
||||
|
||||
# Подтверждение установки
|
||||
code = d.yesno(
|
||||
f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?"
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
# Создаем gauge для отображения прогресса
|
||||
d.gauge_start("Установка пакетов...", percent=0)
|
||||
|
||||
try:
|
||||
# Обновление списка пакетов
|
||||
d.gauge_update(10, "Обновление списка пакетов...")
|
||||
subprocess.run(["apt", "update"], check=True, capture_output=True)
|
||||
|
||||
logging.info(f"Установка пакетов: {', '.join(tags)}")
|
||||
# Установка выбранных пакетов
|
||||
d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...")
|
||||
process = subprocess.Popen(
|
||||
["apt", "install", "-y"] + tags,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
# Чтение вывода процесса и обновление gauge
|
||||
for i, line in enumerate(process.stdout):
|
||||
d.gauge_update(20 + (i % 70), line.strip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
error = process.stderr.read()
|
||||
raise subprocess.CalledProcessError(process.returncode, process.args, error)
|
||||
|
||||
# Проверяем, если вебмин был выбран для установки
|
||||
if "webmin" in tags:
|
||||
d.gauge_update(90, "Настройка Webmin...")
|
||||
# Установка cpanminus
|
||||
run_command_with_log(
|
||||
d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
d.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# # Установка модулей DBI и DBD::Pg через cpanminus
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"):
|
||||
# d.msgbox("Модуль DBI успешно установлен.")
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"):
|
||||
# d.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
d.msgbox("Webmin успешно перезагружен.")
|
||||
|
||||
d.gauge_update(100, "Установка завершена успешно!")
|
||||
d.gauge_stop()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при установке пакетов: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Ошибка при установке пакетов: {e}")
|
||||
|
||||
|
||||
def run_install_packages():
|
||||
install_packages()
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import subprocess
|
||||
import ipaddress
|
||||
from dialog import Dialog
|
||||
|
||||
|
||||
class NetworkConfig:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.temp_network_settings = {}
|
||||
|
||||
def select_interface(self):
|
||||
interfaces = self.get_network_interfaces()
|
||||
if not interfaces:
|
||||
self.d.msgbox("Нет доступных сетевых интерфейсов.")
|
||||
return None
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите интерфейс:", choices=[(iface, "") for iface in interfaces]
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
self.d.msgbox(f"Выбран интерфейс: {tag}")
|
||||
self.show_current_settings(tag) # Добавлено окно с текущими настройками
|
||||
return tag
|
||||
else:
|
||||
self.d.msgbox("Интерфейс не выбран.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_network_interfaces():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ls", "/sys/class/net/"], capture_output=True, text=True, check=True
|
||||
)
|
||||
interfaces = result.stdout.strip().split("\n")
|
||||
return interfaces
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def show_current_settings(self, interface):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True
|
||||
)
|
||||
current_settings = result.stdout.strip()
|
||||
|
||||
ip_address = None
|
||||
if "inet " in current_settings:
|
||||
ip_address = current_settings.split("inet ")[1].split("/")[0]
|
||||
|
||||
if "dhcp" in current_settings:
|
||||
config_type = "DHCP"
|
||||
elif ip_address:
|
||||
config_type = "Static"
|
||||
else:
|
||||
config_type = "Неизвестно"
|
||||
|
||||
message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен"
|
||||
|
||||
self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}")
|
||||
|
||||
def configure_dhcp(self, interface):
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
if code == self.d.OK:
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "dhcp",
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("DHCP настройка сохранена.")
|
||||
else:
|
||||
self.d.msgbox("DHCP настройка отменена.")
|
||||
|
||||
def configure_static(self, interface):
|
||||
code, ip = self.d.inputbox("Введите IP адрес:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, mask = self.d.inputbox("Введите маску подсети:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, gateway = self.d.inputbox("Введите шлюз:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
|
||||
if ip and mask and gateway:
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "static",
|
||||
"ip": ip,
|
||||
"mask": mask,
|
||||
"gateway": gateway,
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("Статические настройки сохранены.")
|
||||
else:
|
||||
self.d.msgbox("Некоторые параметры не заданы.")
|
||||
|
||||
def configure_cidr(self, interface):
|
||||
code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, gateway = self.d.inputbox("Введите шлюз:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
|
||||
if cidr:
|
||||
try:
|
||||
ip = ipaddress.ip_interface(cidr).ip
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
mask = network.netmask
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "static",
|
||||
"ip": str(ip),
|
||||
"mask": str(mask),
|
||||
"gateway": gateway,
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("CIDR конфигурация сохранена.")
|
||||
except ValueError as e:
|
||||
self.d.msgbox(f"Неверный формат CIDR: {e}")
|
||||
else:
|
||||
self.d.msgbox("CIDR не задан.")
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
interface = self.select_interface()
|
||||
if not interface:
|
||||
break
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите действие для интерфейса:",
|
||||
choices=[
|
||||
("1", "Настроить DHCP"),
|
||||
("2", "Настроить статический IP"),
|
||||
("3", "Настроить CIDR"),
|
||||
("4", "Выбрать другой интерфейс"),
|
||||
("5", "Выход"),
|
||||
],
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
if tag == "1":
|
||||
self.configure_dhcp(interface)
|
||||
elif tag == "2":
|
||||
self.configure_static(interface)
|
||||
elif tag == "3":
|
||||
self.configure_cidr(interface)
|
||||
elif tag == "4":
|
||||
continue
|
||||
elif tag == "5":
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return self.temp_network_settings
|
||||
|
||||
def save_and_apply_network_settings(self):
|
||||
for interface, settings in self.temp_network_settings.items():
|
||||
try:
|
||||
if settings["type"] == "dhcp":
|
||||
self.save_dhcp(interface, settings)
|
||||
elif settings["type"] == "static":
|
||||
self.save_static(interface, settings)
|
||||
if settings.get("dns"):
|
||||
self.update_resolv_conf(settings["dns"])
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "networking"], check=True)
|
||||
self.d.msgbox("Сетевые настройки успешно применены.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}")
|
||||
return False
|
||||
|
||||
def save_dhcp(self, interface, settings):
|
||||
interface_config = f"""
|
||||
auto {interface}
|
||||
iface {interface} inet dhcp
|
||||
"""
|
||||
self.update_interface_config(interface, interface_config)
|
||||
|
||||
def save_static(self, interface, settings):
|
||||
ip = settings["ip"]
|
||||
mask = settings["mask"]
|
||||
gateway = settings["gateway"]
|
||||
interface_config = f"""
|
||||
auto {interface}
|
||||
iface {interface} inet static
|
||||
address {ip}
|
||||
netmask {mask}
|
||||
"""
|
||||
if gateway:
|
||||
interface_config += f"gateway {gateway}\n"
|
||||
self.update_interface_config(interface, interface_config)
|
||||
|
||||
def update_interface_config(self, interface, interface_config):
|
||||
try:
|
||||
with open("/etc/network/interfaces", "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
with open("/etc/network/interfaces", "w") as f:
|
||||
skip_section = False
|
||||
for line in lines:
|
||||
if interface in line:
|
||||
skip_section = True
|
||||
continue
|
||||
if skip_section and line.strip() == "":
|
||||
skip_section = False
|
||||
if not skip_section:
|
||||
f.write(line)
|
||||
f.write(interface_config.strip() + "\n")
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}")
|
||||
|
||||
def update_resolv_conf(self, dns):
|
||||
if not dns:
|
||||
return
|
||||
|
||||
dns_entry = f"nameserver {dns}"
|
||||
try:
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
resolv_lines = f.readlines()
|
||||
|
||||
if any(line.strip() == dns_entry for line in resolv_lines):
|
||||
return
|
||||
|
||||
with open("/etc/resolv.conf", "a") as f:
|
||||
f.write(dns_entry + "\n")
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configurator = NetworkConfig()
|
||||
configurator.run()
|
||||
106
deb_dist/postinstall-1.5.0/build/lib/postinstall/option/shell.py
Normal file
106
deb_dist/postinstall-1.5.0/build/lib/postinstall/option/shell.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import pwd
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_users():
|
||||
users = []
|
||||
try:
|
||||
for user in pwd.getpwall():
|
||||
if user.pw_uid >= 1000 and user.pw_name != "nobody":
|
||||
users.append(user.pw_name)
|
||||
if "root" not in users:
|
||||
users.append("root")
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении списка пользователей: {e}")
|
||||
return users
|
||||
|
||||
|
||||
def install_zsh():
|
||||
try:
|
||||
subprocess.run(["apt", "update"], check=True)
|
||||
subprocess.run(["apt", "install", "-y", "zsh"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Ошибка при установке zsh: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def change_shell():
|
||||
d = Dialog(dialog="dialog")
|
||||
users = get_users()
|
||||
|
||||
# Первый шаг - выбор опции с помощью radiolist
|
||||
code, choice = d.radiolist(
|
||||
"Выберите способ изменения оболочки:",
|
||||
choices=[
|
||||
("Для всех пользователей", "", False),
|
||||
("Выбрать конкретных пользователей", "", True)
|
||||
]
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
selected_users = []
|
||||
|
||||
if choice == "Для всех пользователей":
|
||||
# Если выбрано "Для всех пользователей"
|
||||
selected_users = users
|
||||
elif choice == "Выбрать конкретных пользователей":
|
||||
# Если выбран выбор конкретных пользователей, показываем checklist
|
||||
code, selected_users = d.checklist(
|
||||
"Выберите пользователей для изменения оболочки:",
|
||||
choices=[(user, "", False) for user in users],
|
||||
clear=True
|
||||
)
|
||||
if code != d.OK or not selected_users:
|
||||
return
|
||||
|
||||
# Выбор оболочки
|
||||
code, shell = d.menu(
|
||||
"Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")]
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
shell_path = "/bin/bash" if shell == "1" else "/bin/zsh"
|
||||
|
||||
if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"):
|
||||
logging.info("zsh не установлен. Начинаем установку...")
|
||||
d.msgbox("zsh не установлен. Начинаем установку...")
|
||||
if not install_zsh():
|
||||
d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.")
|
||||
return
|
||||
|
||||
# Применение изменений
|
||||
if choice == "Для всех пользователей":
|
||||
# Применение изменений для всех пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
d.msgbox("Оболочка изменена для всех существующих пользователей")
|
||||
logging.info("Оболочка изменена для всех существующих пользователей")
|
||||
else:
|
||||
# Применение изменений для конкретных пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
|
||||
|
||||
def run_change_shell():
|
||||
change_shell()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_change_shell()
|
||||
236
deb_dist/postinstall-1.5.0/build/lib/postinstall/option/ssh.py
Normal file
236
deb_dist/postinstall-1.5.0/build/lib/postinstall/option/ssh.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import logging
|
||||
import os
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
class SSHConfig:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.current_port = self.get_current_ssh_port()
|
||||
self.current_auth_method = self.get_current_auth_method()
|
||||
self.new_port = None
|
||||
self.new_auth_method = None
|
||||
|
||||
def get_sshd_status(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", "sshd"], capture_output=True, text=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return "unknown"
|
||||
|
||||
def toggle_sshd_service(self, action):
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", action, "sshd"], check=True)
|
||||
self.d.msgbox(
|
||||
f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}."
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при {action} службы sshd: {e}")
|
||||
self.d.msgbox(
|
||||
f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd."
|
||||
)
|
||||
|
||||
def get_current_ssh_port(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning(
|
||||
"Не удалось получить порт SSH, используется порт по умолчанию"
|
||||
)
|
||||
return "22"
|
||||
|
||||
def change_ssh_port(self):
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
code, new_port = self.d.inputbox(
|
||||
f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:",
|
||||
init=port_display,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
try:
|
||||
new_port = int(new_port)
|
||||
if new_port < 1 or new_port > 65535:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.")
|
||||
return
|
||||
code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_port = str(new_port)
|
||||
|
||||
def get_current_auth_method(self):
|
||||
try:
|
||||
with open(SSH_AUTH_CONFIG_PATH, "r") as f:
|
||||
content = f.read()
|
||||
password_auth = "yes" if "PasswordAuthentication yes" in content else "no"
|
||||
pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no"
|
||||
|
||||
if password_auth == "yes" and pubkey_auth == "yes":
|
||||
return "По ключу и паролю"
|
||||
elif password_auth == "no" and pubkey_auth == "yes":
|
||||
return "Только по ключу"
|
||||
elif password_auth == "yes" and pubkey_auth == "no":
|
||||
return "Только по паролю"
|
||||
else:
|
||||
return "По ключу и паролю" # Default value
|
||||
except FileNotFoundError:
|
||||
logging.warning("Файл конфигурации аутентификации SSH не найден")
|
||||
return "По ключу и паролю" # Default value
|
||||
except Exception as e:
|
||||
logging.warning(f"Не удалось получить метод аутентификации SSH: {e}")
|
||||
return "По ключу и паролю" # Default value
|
||||
|
||||
def change_auth_method(self):
|
||||
auth_method_display = (
|
||||
self.new_auth_method if self.new_auth_method else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
auth_choices = [
|
||||
(
|
||||
"1",
|
||||
f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}",
|
||||
),
|
||||
(
|
||||
"2",
|
||||
f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}",
|
||||
),
|
||||
(
|
||||
"3",
|
||||
f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}",
|
||||
),
|
||||
]
|
||||
code, tag = self.d.menu(
|
||||
f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:",
|
||||
choices=auth_choices,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_auth_method = {
|
||||
"1": "Только по паролю",
|
||||
"2": "Только по ключу",
|
||||
"3": "По ключу и паролю",
|
||||
}[tag]
|
||||
|
||||
def apply_changes(self):
|
||||
if self.new_port:
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^#*Port .*/Port {self.new_port}/",
|
||||
SSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={self.new_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(
|
||||
["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
self.current_port = self.new_port
|
||||
self.new_port = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении порта SSH: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении порта SSH: {e}")
|
||||
|
||||
if self.new_auth_method:
|
||||
try:
|
||||
auth_config = ""
|
||||
if self.new_auth_method == "Только по паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication no\n"
|
||||
)
|
||||
elif self.new_auth_method == "Только по ключу":
|
||||
auth_config = (
|
||||
"PasswordAuthentication no\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
elif self.new_auth_method == "По ключу и паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True)
|
||||
with open(SSH_AUTH_CONFIG_PATH, "w") as f:
|
||||
f.write(auth_config)
|
||||
|
||||
self.current_auth_method = self.new_auth_method
|
||||
self.new_auth_method = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox("Метод аутентификации SSH успешно изменен.")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
|
||||
def main_menu(self):
|
||||
while True:
|
||||
status = self.get_sshd_status()
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
auth_method_display = (
|
||||
self.new_auth_method
|
||||
if self.new_auth_method
|
||||
else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
|
||||
code, tag = self.d.menu(
|
||||
f"Статус sshd: {status}\n"
|
||||
f"Текущий порт: {port_display}{port_status}\n"
|
||||
f"Метод аутентификации: {auth_method_display}{auth_status}",
|
||||
choices=[
|
||||
("1", "Включить sshd" if status != "active" else "Выключить sshd"),
|
||||
("2", "Изменить порт SSH"),
|
||||
("3", "Изменить метод аутентификации"),
|
||||
],
|
||||
)
|
||||
if code != self.d.OK:
|
||||
break
|
||||
if tag == "1":
|
||||
self.toggle_sshd_service("start" if status != "active" else "stop")
|
||||
elif tag == "2":
|
||||
self.change_ssh_port()
|
||||
elif tag == "3":
|
||||
self.change_auth_method()
|
||||
|
||||
return self.new_port is not None or self.new_auth_method is not None
|
||||
|
||||
|
||||
def run_ssh_config():
|
||||
ssh_config = SSHConfig()
|
||||
changes_made = ssh_config.main_menu()
|
||||
return ssh_config, changes_made
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ssh_config, changes_made = run_ssh_config()
|
||||
# Note: Changes will be applied in the main menu when "Save and exit" is selected
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def run_update():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
code = d.yesno("Вы действительно хотите обновить систему?", width=50)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
commands = [["apt", "update"], ["apt", "upgrade", "-y"]]
|
||||
|
||||
d.gauge_start("Обновление системы", percent=0)
|
||||
|
||||
try:
|
||||
for i, command in enumerate(commands):
|
||||
d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
["sudo"] + command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
error = process.stderr.read()
|
||||
raise subprocess.CalledProcessError(process.returncode, command, error)
|
||||
|
||||
d.gauge_update(100, "Обновление системы завершено успешно")
|
||||
d.gauge_stop()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при обновлении системы: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Ошибка при обновлении системы: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Неожиданная ошибка при обновлении системы: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}")
|
||||
|
||||
d.msgbox("Обновление системы завершено", width=50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_update()
|
||||
11
deb_dist/postinstall-1.5.0/build/lib/postinstall/root.py
Normal file
11
deb_dist/postinstall-1.5.0/build/lib/postinstall/root.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def check_root():
|
||||
if os.geteuid() != 0:
|
||||
print("Запустите скрипт с правами суперпользователя")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
check_root()
|
||||
9
deb_dist/postinstall-1.5.0/build/lib/tests/__init__.py
Normal file
9
deb_dist/postinstall-1.5.0/build/lib/tests/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import subprocess
|
||||
|
||||
|
||||
def get_network_interfaces():
|
||||
interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n")
|
||||
return interfaces[:-1]
|
||||
|
||||
|
||||
get_network_interfaces()
|
||||
132
deb_dist/postinstall-1.5.0/build/lib/tests/docker_config.py
Normal file
132
deb_dist/postinstall-1.5.0/build/lib/tests/docker_config.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
|
||||
REGISTRY_URL = "dr.gosrv.ru"
|
||||
REGISTRY_LOGIN = "dr_admin"
|
||||
REGISTRY_PASSWORD = "Qweasdzxc123C@"
|
||||
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
|
||||
|
||||
|
||||
class DockerConfig:
|
||||
def check_installed(self, command: str) -> bool:
|
||||
try:
|
||||
return (
|
||||
subprocess.run(
|
||||
["which", command], capture_output=True, text=True
|
||||
).returncode
|
||||
== 0
|
||||
)
|
||||
except subprocess.SubprocessError as e:
|
||||
logging.error(f"Error checking if {command} is installed: {e}")
|
||||
return False
|
||||
|
||||
def install_docker(self):
|
||||
try:
|
||||
if not self.check_installed("docker"):
|
||||
logging.info("Установка Docker...")
|
||||
subprocess.run(["apt-get", "update"], check=True)
|
||||
subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True)
|
||||
logging.info("Docker успешно установился")
|
||||
|
||||
if not self.check_installed("docker-compose"):
|
||||
logging.info("Установка Docker Compose...")
|
||||
subprocess.run(
|
||||
["apt-get", "install", "-y", "docker-compose"], check=True
|
||||
)
|
||||
logging.info("Docker Compose успешно установился")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при установке Docker: {e}")
|
||||
raise
|
||||
|
||||
def check_docker_login(self, registry_url):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "login", registry_url, "--password-stdin"],
|
||||
input=b"\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return "login succeeded" in result.stdout.lower()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Error checking Docker login: {e}")
|
||||
return False
|
||||
|
||||
def login_to_registry(self):
|
||||
try:
|
||||
if not self.check_docker_login(REGISTRY_URL):
|
||||
logging.info("Вход в Docker registry...")
|
||||
subprocess.run(
|
||||
["docker", "login", REGISTRY_URL],
|
||||
input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(),
|
||||
check=True,
|
||||
)
|
||||
logging.info("Успешный вход в Docker registry")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при входе в Docker registry: {e}")
|
||||
raise
|
||||
|
||||
def install_portainer(self, port, login, password, ip):
|
||||
try:
|
||||
if not self.check_docker_login(REGISTRY_URL):
|
||||
self.login_to_registry()
|
||||
|
||||
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
|
||||
docker_compose_content = f"""
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: dr.gosrv.ru/portainer
|
||||
container_name: portainer
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- portainer_data:/data
|
||||
- ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password
|
||||
ports:
|
||||
- "{ip}:{port}:9000"
|
||||
environment:
|
||||
- ADMIN_USER={login}
|
||||
- ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password
|
||||
|
||||
volumes:
|
||||
portainer_data:
|
||||
"""
|
||||
|
||||
with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f:
|
||||
f.write(docker_compose_content)
|
||||
|
||||
subprocess.run(
|
||||
["htpasswd", "-nb", login, password],
|
||||
stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"),
|
||||
check=True,
|
||||
)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"docker-compose",
|
||||
"-f",
|
||||
DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml",
|
||||
"up",
|
||||
"-d",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
logging.info("Portainer успешно установлен")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при установке Portainer: {e}")
|
||||
raise
|
||||
except IOError as e:
|
||||
logging.error(f"Ошибка при создании файла: {e}")
|
||||
raise
|
||||
|
||||
def validate_input(self, value, pattern, error_message):
|
||||
if not re.match(pattern, value):
|
||||
return False
|
||||
return True
|
||||
67
deb_dist/postinstall-1.5.0/build/lib/tests/test_docker.py
Normal file
67
deb_dist/postinstall-1.5.0/build/lib/tests/test_docker.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH
|
||||
|
||||
class TestDockerConfig(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.docker_config = DockerConfig()
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_installed_true(self, mock_run):
|
||||
mock_run.return_value.returncode = 0
|
||||
self.assertTrue(self.docker_config.check_installed("docker"))
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_installed_false(self, mock_run):
|
||||
mock_run.return_value.returncode = 1
|
||||
self.assertFalse(self.docker_config.check_installed("docker"))
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_install_docker(self, mock_run):
|
||||
self.docker_config.install_docker()
|
||||
mock_run.assert_any_call(["apt-get", "update"], check=True)
|
||||
mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True)
|
||||
mock_run.assert_any_call(
|
||||
["apt-get", "install", "-y", "docker-compose"], check=True
|
||||
)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_docker_login_success(self, mock_run):
|
||||
mock_run.return_value.stdout = "Login Succeeded"
|
||||
self.assertTrue(self.docker_config.check_docker_login("registry_url"))
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_check_docker_login_failure(self, mock_run):
|
||||
mock_run.return_value.stdout = "Login Failed"
|
||||
self.assertFalse(self.docker_config.check_docker_login("registry_url"))
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("docker_config.DockerConfig.check_docker_login")
|
||||
def test_login_to_registry(self, mock_check_login, mock_run):
|
||||
mock_check_login.return_value = False
|
||||
self.docker_config.login_to_registry()
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch("subprocess.run")
|
||||
@patch("docker_config.DockerConfig.check_docker_login")
|
||||
@patch("docker_config.DockerConfig.login_to_registry")
|
||||
@patch("builtins.open", new_callable=unittest.mock.mock_open)
|
||||
def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run):
|
||||
mock_check_login.return_value = True
|
||||
self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1")
|
||||
mock_open.assert_called()
|
||||
mock_run.assert_called()
|
||||
|
||||
def test_validate_input_success(self):
|
||||
self.assertTrue(
|
||||
self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error")
|
||||
)
|
||||
|
||||
def test_validate_input_failure(self):
|
||||
self.assertFalse(
|
||||
self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
3
deb_dist/postinstall-1.5.0/deb/0002-postinstall.sh
Normal file
3
deb_dist/postinstall-1.5.0/deb/0002-postinstall.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
if [ -n "$PS1" ]; then
|
||||
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
|
||||
fi
|
||||
12
deb_dist/postinstall-1.5.0/deb/postinst
Executable file
12
deb_dist/postinstall-1.5.0/deb/postinst
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
mkdir -p /var/lib/postinstall
|
||||
|
||||
if [ ! -f /var/lib/postinstall/first_run_done ]; then
|
||||
chmod +x /usr/share/postinstall/postinstall_start.sh
|
||||
cp /usr/share/postinstall/postinstall.service /etc/systemd/system/
|
||||
systemctl enable postinstall.service
|
||||
pip install --upgrade textual --break-system-packages
|
||||
fi
|
||||
|
||||
10
deb_dist/postinstall-1.5.0/deb/postinstall-autostart.desktop
Normal file
10
deb_dist/postinstall-1.5.0/deb/postinstall-autostart.desktop
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=PostInstall
|
||||
Comment=Textual TUI Application for Network Configuration
|
||||
Exec=pkexec /usr/bin/postinstall
|
||||
Icon=postinstall
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;Network;
|
||||
X-GNOME-Autostart-enabled=true
|
||||
9
deb_dist/postinstall-1.5.0/deb/postinstall.desktop
Normal file
9
deb_dist/postinstall-1.5.0/deb/postinstall.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=PostInstall
|
||||
Comment=Textual TUI Application for Network Configuration
|
||||
Exec=pkexec /usr/bin/postinstall
|
||||
Icon=postinstall
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;Network;
|
||||
15
deb_dist/postinstall-1.5.0/deb/postinstall.service
Normal file
15
deb_dist/postinstall-1.5.0/deb/postinstall.service
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=Postinstall TUI Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/postinstall
|
||||
Restart=always
|
||||
User=root
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
StandardInput=tty
|
||||
StandardOutput=tty
|
||||
TTYPath=/dev/tty
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
2
deb_dist/postinstall-1.5.0/deb/postinstall_start.sh
Executable file
2
deb_dist/postinstall-1.5.0/deb/postinstall_start.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
exec /usr/bin/postinstall
|
||||
BIN
deb_dist/postinstall-1.5.0/deb/python3-textual_0.1.13-1_all.deb
Normal file
BIN
deb_dist/postinstall-1.5.0/deb/python3-textual_0.1.13-1_all.deb
Normal file
Binary file not shown.
BIN
deb_dist/postinstall-1.5.0/deb/textual-0.72.0-py3-none-any.whl
Normal file
BIN
deb_dist/postinstall-1.5.0/deb/textual-0.72.0-py3-none-any.whl
Normal file
Binary file not shown.
|
|
@ -0,0 +1,5 @@
|
|||
postinstall (1.5.0-1) stable; urgency=low
|
||||
|
||||
* source package automatically created by stdeb 0.10.0
|
||||
|
||||
-- LLC Technologies and Communication <devteam@texnico.ru> Wed, 27 Nov 2024 15:29:20 +0500
|
||||
5
deb_dist/postinstall-1.5.0/debian/changelog
Normal file
5
deb_dist/postinstall-1.5.0/debian/changelog
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
postinstall (1.5.0-1) stable; urgency=low
|
||||
|
||||
* source package automatically created by stdeb 0.10.0
|
||||
|
||||
-- LLC Technologies and Communication <devteam@texnico.ru> Wed, 27 Nov 2024 15:29:20 +0500
|
||||
1
deb_dist/postinstall-1.5.0/debian/compat
Normal file
1
deb_dist/postinstall-1.5.0/debian/compat
Normal file
|
|
@ -0,0 +1 @@
|
|||
9
|
||||
33
deb_dist/postinstall-1.5.0/debian/control
Normal file
33
deb_dist/postinstall-1.5.0/debian/control
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
Source: postinstall
|
||||
Maintainer: LLC Technologies and Communication <devteam@texnico.ru>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: python3-setuptools, python3-all, debhelper (>= 9)
|
||||
Standards-Version: 3.9.1
|
||||
Homepage: None
|
||||
|
||||
Package: python3-postinstall
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python3:Depends}
|
||||
Description: A Dialog TUI application for postinstall system configuratio
|
||||
## Документация Postinstall
|
||||
.
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
.
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
.
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
.
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
.
|
||||

|
||||
.
|
||||
.
|
||||
#### **1.2 Настройка DHCP**
|
||||
.
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
.
|
||||
#### **1.3 Настройка статического IP**
|
||||
.
|
||||
|
||||
2
deb_dist/postinstall-1.5.0/debian/files
Normal file
2
deb_dist/postinstall-1.5.0/debian/files
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
postinstall_1.5.0-1_amd64.buildinfo python optional
|
||||
python3-postinstall_1.5.0-1_all.deb python optional
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
dh_update_autotools_config
|
||||
dh_auto_configure
|
||||
dh_auto_build
|
||||
dh_auto_test
|
||||
dh_prep
|
||||
dh_auto_install
|
||||
dh_installdocs
|
||||
dh_installchangelogs
|
||||
dh_installinit
|
||||
dh_perl
|
||||
dh_link
|
||||
dh_strip_nondeterminism
|
||||
dh_compress
|
||||
dh_fixperms
|
||||
dh_missing
|
||||
dh_installdeb
|
||||
dh_gencontrol
|
||||
dh_md5sums
|
||||
dh_builddeb
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
# Automatically added by dh_python3
|
||||
if command -v py3compile >/dev/null 2>&1; then
|
||||
py3compile -p python3-postinstall
|
||||
fi
|
||||
if command -v pypy3compile >/dev/null 2>&1; then
|
||||
pypy3compile -p python3-postinstall || true
|
||||
fi
|
||||
|
||||
# End automatically added section
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
# Automatically added by dh_python3
|
||||
if command -v py3clean >/dev/null 2>&1; then
|
||||
py3clean -p python3-postinstall
|
||||
else
|
||||
dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e'
|
||||
find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
|
||||
fi
|
||||
|
||||
# End automatically added section
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
python3:Depends=python3-dialog, python3:any
|
||||
misc:Depends=
|
||||
misc:Pre-Depends=
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/etc/profile.d/0002-postinstall.sh
|
||||
/etc/systemd/system/postinstall.service
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
Package: python3-postinstall
|
||||
Source: postinstall
|
||||
Version: 1.5.0-1
|
||||
Architecture: all
|
||||
Maintainer: LLC Technologies and Communication <devteam@texnico.ru>
|
||||
Installed-Size: 132
|
||||
Depends: python3-dialog, python3:any
|
||||
Section: python
|
||||
Priority: optional
|
||||
Homepage: None
|
||||
Description: A Dialog TUI application for postinstall system configuratio
|
||||
## Документация Postinstall
|
||||
.
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
.
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
.
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
.
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
.
|
||||

|
||||
.
|
||||
.
|
||||
#### **1.2 Настройка DHCP**
|
||||
.
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
.
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
04a70127767c205f3dc92b73f521a3ee usr/bin/postinstall
|
||||
44a9b40eff604b04d07a03b1a9dbb4c6 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/PKG-INFO
|
||||
68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/dependency_links.txt
|
||||
bcaee1a445a35da75a848bd8cee176a7 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/entry_points.txt
|
||||
d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/requires.txt
|
||||
147b741cc4a5c60607a1dc9f4fa92ca1 usr/lib/python3/dist-packages/postinstall-1.5.0-py3.11.egg-info/top_level.txt
|
||||
2a4a742cf1d86768584347780bcd16f6 usr/lib/python3/dist-packages/postinstall/__init__.py
|
||||
666d344c47c8050391790adb6a9d69bb usr/lib/python3/dist-packages/postinstall/__main__.py
|
||||
9e3443cf16cce74b2dcf317b122aee4f usr/lib/python3/dist-packages/postinstall/logger.py
|
||||
d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/postinstall/option/__init__.py
|
||||
ed35c883b020ece0a9933a55383a08a5 usr/lib/python3/dist-packages/postinstall/option/add_user.py
|
||||
13e13a3b080d38a57fc984bcc09d6291 usr/lib/python3/dist-packages/postinstall/option/autostart.py
|
||||
f391e7e0329deeafc4c1bc3798a81e57 usr/lib/python3/dist-packages/postinstall/option/database.py
|
||||
d1b06abeee3abbcb3047e873c3270465 usr/lib/python3/dist-packages/postinstall/option/docker.py
|
||||
55583cfd043bf938df8ca95b368b68bc usr/lib/python3/dist-packages/postinstall/option/hostname.py
|
||||
a6485960a6c9f2c3fed14925d9a5c7fa usr/lib/python3/dist-packages/postinstall/option/install_packages.py
|
||||
4ad632661661fd0449f5cc1127471ab1 usr/lib/python3/dist-packages/postinstall/option/network.py
|
||||
39961879cf3f2b241f946c61830ea018 usr/lib/python3/dist-packages/postinstall/option/shell.py
|
||||
f47b245cb5673e70988e801ee3776d0a usr/lib/python3/dist-packages/postinstall/option/ssh.py
|
||||
abb826ae4319de07a7b312a38652e0d2 usr/lib/python3/dist-packages/postinstall/option/update.py
|
||||
633499d822e582c720519dc6b4f80f80 usr/lib/python3/dist-packages/postinstall/root.py
|
||||
fd2df9873601eb2b957cd774dd5f11a2 usr/share/doc/python3-postinstall/changelog.Debian.gz
|
||||
12
deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/postinst
Executable file
12
deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/postinst
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Automatically added by dh_python3
|
||||
if command -v py3compile >/dev/null 2>&1; then
|
||||
py3compile -p python3-postinstall
|
||||
fi
|
||||
if command -v pypy3compile >/dev/null 2>&1; then
|
||||
pypy3compile -p python3-postinstall || true
|
||||
fi
|
||||
|
||||
# End automatically added section
|
||||
12
deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/prerm
Executable file
12
deb_dist/postinstall-1.5.0/debian/python3-postinstall/DEBIAN/prerm
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Automatically added by dh_python3
|
||||
if command -v py3clean >/dev/null 2>&1; then
|
||||
py3clean -p python3-postinstall
|
||||
else
|
||||
dpkg -L python3-postinstall | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e'
|
||||
find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir
|
||||
fi
|
||||
|
||||
# End automatically added section
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
if [ -n "$PS1" ]; then
|
||||
[ -f /usr/share/postinstall/.norestart ] || ( su -c /usr/bin/postinstall && clear )
|
||||
fi
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=Postinstall TUI Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/postinstall
|
||||
Restart=always
|
||||
User=root
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
StandardInput=tty
|
||||
StandardOutput=tty
|
||||
TTYPath=/dev/tty
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
33
deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/bin/postinstall
Executable file
33
deb_dist/postinstall-1.5.0/debian/python3-postinstall/usr/bin/postinstall
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#! /usr/bin/python3
|
||||
# EASY-INSTALL-ENTRY-SCRIPT: 'postinstall==1.5.0','console_scripts','postinstall'
|
||||
import re
|
||||
import sys
|
||||
|
||||
# for compatibility with easy_install; see #2198
|
||||
__requires__ = 'postinstall==1.5.0'
|
||||
|
||||
try:
|
||||
from importlib.metadata import distribution
|
||||
except ImportError:
|
||||
try:
|
||||
from importlib_metadata import distribution
|
||||
except ImportError:
|
||||
from pkg_resources import load_entry_point
|
||||
|
||||
|
||||
def importlib_load_entry_point(spec, group, name):
|
||||
dist_name, _, _ = spec.partition('==')
|
||||
matches = (
|
||||
entry_point
|
||||
for entry_point in distribution(dist_name).entry_points
|
||||
if entry_point.group == group and entry_point.name == name
|
||||
)
|
||||
return next(matches).load()
|
||||
|
||||
|
||||
globals().setdefault('load_entry_point', importlib_load_entry_point)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(load_entry_point('postinstall==1.5.0', 'console_scripts', 'postinstall')())
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: postinstall
|
||||
Version: 1.5.0
|
||||
Summary: A Dialog TUI application for postinstall system configuration
|
||||
Author: LLC Technologies and Communication
|
||||
Author-email: devteam@texnico.ru
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent :: Linux
|
||||
Requires-Python: >=3.11
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: pythondialog<4.0.0,>=3.5.3
|
||||
|
||||
## Документация Postinstall
|
||||
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.2 Настройка DHCP**
|
||||
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
||||
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
|
||||
|
||||
#### **1.4 Настройка CIDR**
|
||||
|
||||
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **5. Настройки SSH**
|
||||
|
||||
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
|
||||
|
||||
#### **5.1 Включение/отключение SSH**
|
||||
|
||||
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
|
||||
|
||||
#### **5.2 Управление ключами SSH**
|
||||
|
||||
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
|
||||
|
||||
#### **5.3 Изменение порта SSH**
|
||||
|
||||
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[console_scripts]
|
||||
postinstall = postinstall:main
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
postinstall
|
||||
tests
|
||||
|
|
@ -0,0 +1,395 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
from dialog import Dialog
|
||||
from postinstall.root import check_root
|
||||
from postinstall.option import (
|
||||
network,
|
||||
update,
|
||||
add_user,
|
||||
install_packages,
|
||||
ssh,
|
||||
hostname,
|
||||
shell,
|
||||
database,
|
||||
docker,
|
||||
)
|
||||
import json
|
||||
|
||||
check_root()
|
||||
|
||||
from postinstall.logger import logger
|
||||
|
||||
# import debugpy
|
||||
|
||||
# debugpy.listen(("0.0.0.0", 5678))
|
||||
# debugpy.wait_for_client()
|
||||
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
HOSTNAME_FILE = "/etc/hostname"
|
||||
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
|
||||
BACKUPS_FILE = "/usr/share/postinstall/backups.json"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_hostname() -> str:
|
||||
try:
|
||||
with open(HOSTNAME_FILE, "r") as f:
|
||||
return f.read().strip()
|
||||
except IOError as e:
|
||||
logging.error(f"Ошибка при чтении hostname: {e}")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def get_ssh_port() -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Не удалось получить порт SSH, используется порт по умолчанию")
|
||||
return "22"
|
||||
|
||||
|
||||
class MenuScreen:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.ssh_port = get_ssh_port()
|
||||
self.hostname = get_hostname()
|
||||
self.ssh_changes = None
|
||||
self.new_hostname = None
|
||||
self.network = None
|
||||
self.autostart_enabled = self.check_auto_start()
|
||||
self.new_autostart_enabled = self.autostart_enabled
|
||||
|
||||
self.options = [
|
||||
{
|
||||
"name": "Сетевые настройки",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "network",
|
||||
},
|
||||
{
|
||||
"name": "Обновление системы",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "update",
|
||||
},
|
||||
{
|
||||
"name": "Добавить пользователя",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "add_user",
|
||||
},
|
||||
{
|
||||
"name": "Установка программ",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "install_packages",
|
||||
},
|
||||
{
|
||||
"name": "Настройки ssh",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "ssh",
|
||||
},
|
||||
{
|
||||
"name": "Изменить доменное имя ПК",
|
||||
"value": self.hostname,
|
||||
"tabs": "\t \t",
|
||||
"option": "hostname",
|
||||
},
|
||||
{"name": "Выбор оболочки", "value": "", "tabs": "\t \t", "option": "shell"},
|
||||
{
|
||||
"name": "Автозапуск при следующем запуске",
|
||||
"value": "Включен" if self.new_autostart_enabled else "Отключен",
|
||||
"tabs": "\t",
|
||||
"option": "auto_start",
|
||||
},
|
||||
{
|
||||
"name": "Настройка базы данных",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "database",
|
||||
},
|
||||
{
|
||||
"name": "Настройка docker",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "docker",
|
||||
},
|
||||
{
|
||||
"name": "Откат последних изменений",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "rollback",
|
||||
},
|
||||
{
|
||||
"name": "Сохранить и выйти",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "save_and_exit",
|
||||
},
|
||||
]
|
||||
|
||||
os.makedirs(os.path.dirname(AUTOSTART_FILE), exist_ok=True)
|
||||
|
||||
def show_menu(self):
|
||||
choices = []
|
||||
for i, option in enumerate(self.options, 1):
|
||||
value = option["value"]
|
||||
if option["option"] == "hostname":
|
||||
value = (
|
||||
self.new_hostname
|
||||
if self.new_hostname is not None
|
||||
else self.hostname
|
||||
)
|
||||
if self.new_hostname is not None:
|
||||
value = f"{value} (не применено)"
|
||||
elif option["option"] == "network":
|
||||
value = "Изменено (не применено)" if self.network else ""
|
||||
|
||||
choice_text = f"{option['name']}{option['tabs']}{value}"
|
||||
choices.append((str(i), choice_text))
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите опцию:",
|
||||
choices=choices,
|
||||
title="Главное меню",
|
||||
cancel_label="Выйти без сохранения",
|
||||
width=70,
|
||||
height=15,
|
||||
)
|
||||
|
||||
if code == self.d.OK and tag:
|
||||
return self.options[int(tag) - 1]["option"]
|
||||
return None
|
||||
|
||||
def set_temp_network(self, settings):
|
||||
self.temp_network = settings
|
||||
|
||||
def check_auto_start(self) -> bool:
|
||||
return not os.path.exists(AUTOSTART_FILE)
|
||||
|
||||
def set_auto_start(self, enable: bool) -> None:
|
||||
try:
|
||||
if enable:
|
||||
if os.path.exists(AUTOSTART_FILE):
|
||||
os.remove(AUTOSTART_FILE)
|
||||
logging.info("Автозапуск включен")
|
||||
else:
|
||||
with open(AUTOSTART_FILE, "w") as f:
|
||||
f.write("# Это файл создан для отключения автозапуска\n")
|
||||
logging.info("Автозапуск отключен")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении автозапуска: {e}")
|
||||
|
||||
def update_ssh_port(self, new_port):
|
||||
self.new_ssh_port = new_port
|
||||
|
||||
def update_hostname(self, new_hostname):
|
||||
self.new_hostname = new_hostname
|
||||
|
||||
def update_autostart(self, new_state):
|
||||
self.new_autostart_enabled = new_state
|
||||
|
||||
|
||||
def choose(option, menu):
|
||||
if menu.new_autostart_enabled != menu.autostart_enabled:
|
||||
menu.set_auto_start(menu.new_autostart_enabled)
|
||||
if option == "network":
|
||||
net_config = network.NetworkConfig()
|
||||
if net_config.run():
|
||||
menu.network = net_config
|
||||
elif option == "update":
|
||||
logging.info("Начало обновления системы.")
|
||||
update.run_update()
|
||||
logging.info("Завершено обновление системы.")
|
||||
elif option == "add_user":
|
||||
logging.info("Началось добавления пользователя.")
|
||||
add_user.run_add_user()
|
||||
logging.info("Завершено добавление пользователя.")
|
||||
elif option == "install_packages":
|
||||
logging.info("Начало установки пакетов.")
|
||||
install_packages.run_install_packages()
|
||||
logging.info("Завершена установка пакетов.")
|
||||
elif option == "ssh":
|
||||
ssh_config, changes_made = ssh.run_ssh_config()
|
||||
if changes_made:
|
||||
menu.ssh_changes = ssh_config
|
||||
elif option == "hostname":
|
||||
new_hostname = hostname.run_change_hostname()
|
||||
if new_hostname:
|
||||
menu.new_hostname = new_hostname
|
||||
menu.options[5]["value"] = f"{new_hostname} (не применено)"
|
||||
elif option == "shell":
|
||||
logging.info("Началось изменение shell.")
|
||||
shell.run_change_shell()
|
||||
logging.info("Завершено изменение shell.")
|
||||
elif option == "auto_start":
|
||||
menu.new_autostart_enabled = not menu.new_autostart_enabled
|
||||
menu.options[7]["value"] = (
|
||||
"Включен" if menu.new_autostart_enabled else "Отключен"
|
||||
)
|
||||
logging.info(
|
||||
f"Автозапуск {'включен' if menu.new_autostart_enabled else 'отключен'}"
|
||||
)
|
||||
elif option == "database":
|
||||
logging.info("Запуск настройки базы данных.")
|
||||
|
||||
# Открытие диалогового окна для выбора базы данных
|
||||
code, choice = menu.d.menu("Выберите базу данных для установки:",
|
||||
choices=[("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB")])
|
||||
|
||||
if code == menu.d.OK:
|
||||
if choice == "PostgreSQL":
|
||||
if not database.is_installed('postgresql-16'):
|
||||
database.install_postgresql(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("PostgreSQL и Harlequin уже установлены.")
|
||||
|
||||
elif choice == "MariaDB":
|
||||
if not database.is_installed('mariadb-server'):
|
||||
database.install_mariadb(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("MariaDB уже установлена.")
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if not database.is_installed('webmin'):
|
||||
code = menu.d.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
if code == menu.d.OK:
|
||||
database.install_webmin(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("Webmin уже установлен.")
|
||||
|
||||
logging.info("Настройка базы данных завершена.")
|
||||
else:
|
||||
menu.d.msgbox("Выбор базы данных отменён")
|
||||
elif option == "docker":
|
||||
docker.run()
|
||||
|
||||
elif option == "rollback":
|
||||
try:
|
||||
with open(BACKUPS_FILE, "r") as f:
|
||||
backups = json.load(f)
|
||||
f.close()
|
||||
for i in backups:
|
||||
with open(i["file"], "w") as f:
|
||||
f.write(i["content"])
|
||||
f.close()
|
||||
subprocess.run(["systemctl", "restart", "networking"], check=True)
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
ssh_port = get_ssh_port()
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={ssh_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
|
||||
menu.d.msgbox("Резервные копии успешно восстановлены")
|
||||
logging.info("Резервные копии успешно восстановлены")
|
||||
except Exception as e:
|
||||
menu.d.msgbox(f"Вы еще не применяли изменения.{e}")
|
||||
return False
|
||||
|
||||
elif option == "save_and_exit":
|
||||
with open("/etc/network/interfaces", "r") as f:
|
||||
interfaces = f.read()
|
||||
f.close()
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
dns_settings = f.read()
|
||||
f.close()
|
||||
with open("/etc/ssh/sshd_config", "r") as f:
|
||||
ssh_settings = f.read()
|
||||
f.close()
|
||||
try:
|
||||
with open("/etc/ssh/sshd_config.d/auth.conf", "r") as f:
|
||||
ssh_auth = f.read()
|
||||
except Exception:
|
||||
ssh_auth = ""
|
||||
backup = [
|
||||
{
|
||||
"file": "/etc/network/interfaces",
|
||||
"content": interfaces,
|
||||
},
|
||||
{
|
||||
"file": "/etc/resolv.conf",
|
||||
"content": dns_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config",
|
||||
"content": ssh_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config.d/auth.conf",
|
||||
"content": ssh_auth,
|
||||
},
|
||||
]
|
||||
|
||||
with open(BACKUPS_FILE, "w") as f:
|
||||
f.write(json.dumps(backup))
|
||||
f.close()
|
||||
|
||||
if menu.ssh_changes:
|
||||
logging.info(
|
||||
f"Применены новые настройки SSH. {'Новый порт SSH: ' + str(menu.ssh_changes.new_port) if menu.ssh_changes.new_port is not None else ''} {'Новый метод авторизации: ' + menu.ssh_changes.new_auth_method if menu.ssh_changes.new_auth_method is not None else ''}"
|
||||
)
|
||||
menu.ssh_changes.apply_changes()
|
||||
if menu.new_hostname:
|
||||
logging.info(f"Применено новое имя хоста: {menu.new_hostname}")
|
||||
if hostname.apply_hostname_change(menu.new_hostname):
|
||||
menu.hostname = menu.new_hostname
|
||||
if menu.network:
|
||||
logging.info(
|
||||
f"Применены новые настройки сети: {menu.network.temp_network_settings}"
|
||||
)
|
||||
if not menu.network.save_and_apply_network_settings():
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
logging.info("Приложение запущено")
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
menu = MenuScreen()
|
||||
while True:
|
||||
option = menu.show_menu()
|
||||
if option is None:
|
||||
break
|
||||
if not choose(option, menu):
|
||||
break
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
def setup_logger():
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s")
|
||||
|
||||
file_handler = logging.FileHandler("/var/log/postinstall.log")
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
class UserFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.user = getpass.getuser()
|
||||
return True
|
||||
|
||||
file_handler.addFilter(UserFilter())
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import pwd
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_password_with_toggle(d, title):
|
||||
"""Функция для ввода пароля с возможностью показать/скрыть пароль"""
|
||||
password_hidden = True
|
||||
password = ""
|
||||
|
||||
# Фиксированная ширина окна
|
||||
window_width = 60
|
||||
|
||||
while True:
|
||||
if password_hidden:
|
||||
# Используем insecure=True для показа скрытого пароля снежинками
|
||||
code, entered_password = d.passwordbox(
|
||||
title, insecure=True, init=password, width=window_width,
|
||||
extra_button=True, extra_label="Показать пароль"
|
||||
)
|
||||
else:
|
||||
# Используем insecure=False для показа пароля открытым текстом
|
||||
code, entered_password = d.inputbox(
|
||||
title, init=password, width=window_width,
|
||||
extra_button=True, extra_label="Скрыть пароль"
|
||||
)
|
||||
|
||||
if code == d.EXTRA:
|
||||
password_hidden = not password_hidden
|
||||
password = entered_password # Сохраняем введённый пароль
|
||||
continue # Перезапускаем окно с новым состоянием
|
||||
|
||||
if code == d.CANCEL:
|
||||
return None
|
||||
|
||||
# Обновляем сохранённый пароль
|
||||
password = entered_password
|
||||
return password
|
||||
|
||||
|
||||
def add_user():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
fields = [
|
||||
("Имя пользователя", 1, 1, "", 1, 20, 30, 0),
|
||||
("Полное имя", 2, 1, "", 2, 20, 30, 0),
|
||||
("Мобильный телефон", 3, 1, "", 3, 20, 30, 0),
|
||||
("Городской телефон", 4, 1, "", 4, 20, 30, 0),
|
||||
("Кабинет", 5, 1, "", 5, 20, 30, 0),
|
||||
]
|
||||
|
||||
code, values = d.form("Введите данные нового пользователя:", fields)
|
||||
|
||||
if code != d.OK:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
username, fullname, mobile, city_phone, cabinet = values
|
||||
|
||||
# Проверка существования пользователя
|
||||
try:
|
||||
pwd.getpwnam(username)
|
||||
d.msgbox(f"Пользователь {username} уже существует.")
|
||||
return
|
||||
except KeyError:
|
||||
pass # Пользователь не существует, продолжаем
|
||||
|
||||
# Запрос пароля с возможностью его показать/скрыть
|
||||
password = get_password_with_toggle(d, "Введите пароль для нового пользователя")
|
||||
if not password:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
# Запрос подтверждения пароля с возможностью его показать/скрыть
|
||||
confirm_password = get_password_with_toggle(d, "Подтвердите пароль")
|
||||
if not confirm_password:
|
||||
return # Пользователь отменил операцию, выходим из функции
|
||||
|
||||
if password != confirm_password:
|
||||
d.msgbox("Пароли не совпадают. Попробуйте снова.")
|
||||
return
|
||||
|
||||
# Запрос прав администратора
|
||||
code = d.yesno("Сделать пользователя администратором?")
|
||||
is_admin = code == d.OK
|
||||
|
||||
try:
|
||||
# Создание пользователя
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"useradd",
|
||||
"-m",
|
||||
"-c",
|
||||
f"{fullname},{cabinet},{city_phone},{mobile}",
|
||||
"-s",
|
||||
"/bin/bash",
|
||||
username,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Установка пароля
|
||||
process = subprocess.Popen(
|
||||
["sudo", "passwd", username],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
process.communicate(input=f"{password}\n{password}\n".encode())
|
||||
|
||||
# Добавление в группу sudo, если пользователь - администратор
|
||||
if is_admin:
|
||||
subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True)
|
||||
|
||||
logging.info(
|
||||
f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}"
|
||||
)
|
||||
|
||||
d.msgbox(f"Пользователь {username} успешно добавлен.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при добавлении пользователя: {e}")
|
||||
d.msgbox(f"Ошибка при добавлении пользователя: {e}")
|
||||
|
||||
|
||||
def run_add_user():
|
||||
add_user()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_add_user()
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import os
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
|
||||
|
||||
|
||||
class AutoStartScreen:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
|
||||
def check_auto_start(self) -> bool:
|
||||
return not os.path.exists(AUTOSTART_FILE)
|
||||
|
||||
def set_auto_start(self, enable: bool) -> None:
|
||||
try:
|
||||
if enable:
|
||||
try:
|
||||
if os.path.exists(AUTOSTART_FILE):
|
||||
os.remove(AUTOSTART_FILE)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при включении автозапуска: {e}")
|
||||
else:
|
||||
with open(AUTOSTART_FILE, "w") as f:
|
||||
f.write("# Это файл создан для отключения автозапуска\n")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении автозапуска: {e}")
|
||||
|
||||
def show_screen(self):
|
||||
auto_start_enabled = self.check_auto_start()
|
||||
|
||||
code, tags = self.d.checklist(
|
||||
"Выберите действие",
|
||||
choices=[
|
||||
("autostart", "Автозапуск при следующем запуске", auto_start_enabled)
|
||||
],
|
||||
title="Настройки автозапуска",
|
||||
backtitle="Postinstall Configuration",
|
||||
ok_label="Применить",
|
||||
cancel_label="Назад",
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
self.set_auto_start("autostart" in tags)
|
||||
self.d.msgbox("Изменения сохранены.")
|
||||
else:
|
||||
logging.info("Изменения отменены пользователем.")
|
||||
|
||||
|
||||
def main():
|
||||
auto_start_screen = AutoStartScreen()
|
||||
auto_start_screen.show_screen()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
|
||||
|
||||
def main():
|
||||
dialog = Dialog(dialog="dialog")
|
||||
|
||||
# Диалоговое окно с выбором базы данных
|
||||
code, choice = dialog.menu(
|
||||
"Выберите базу данных для установки:",
|
||||
choices=[
|
||||
("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB"),
|
||||
],
|
||||
)
|
||||
|
||||
if code == dialog.OK:
|
||||
dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён")
|
||||
if choice == "PostgreSQL":
|
||||
install_postgresql(dialog)
|
||||
elif choice == "MariaDB":
|
||||
install_mariadb(dialog)
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if (
|
||||
dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
== dialog.OK
|
||||
):
|
||||
install_webmin(dialog)
|
||||
else:
|
||||
dialog.msgbox("Выбор базы данных отменён", title="Отмена")
|
||||
|
||||
|
||||
def is_installed(package_name):
|
||||
"""Проверяет, установлен ли пакет"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["dpkg-query", "-W", "-f=${Status}", package_name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return "install ok installed" in result.stdout
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, title):
|
||||
"""Запуск команды с выводом лога в диалоговое окно"""
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
log_output = []
|
||||
for line in iter(process.stdout.readline, ""):
|
||||
log_output.append(line.strip())
|
||||
|
||||
stderr_output = process.stderr.read().strip()
|
||||
if stderr_output:
|
||||
log_output.append(stderr_output)
|
||||
|
||||
process.stdout.close()
|
||||
process.stderr.close()
|
||||
process.wait()
|
||||
|
||||
dialog.msgbox("\n".join(log_output), title=title, height=20, width=60)
|
||||
return process.returncode == 0
|
||||
|
||||
|
||||
def install_package(dialog, package_name):
|
||||
"""Устанавливает указанный пакет с выводом лога"""
|
||||
dialog.infobox(f"Начало установки {package_name}...")
|
||||
|
||||
if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"):
|
||||
dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.")
|
||||
return
|
||||
|
||||
if not run_command_with_log(
|
||||
dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}"
|
||||
):
|
||||
dialog.msgbox(f"Ошибка при установке {package_name}.")
|
||||
return
|
||||
|
||||
dialog.msgbox(f"{package_name} успешно установлен.")
|
||||
|
||||
|
||||
def install_postgresql(dialog):
|
||||
"""Установка PostgreSQL и Harlequin"""
|
||||
if not is_installed("postgresql-16"):
|
||||
install_package(dialog, "postgresql-16")
|
||||
else:
|
||||
dialog.msgbox("PostgreSQL уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin-postgres"):
|
||||
install_package(dialog, "python3-harlequin-postgres")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin-postgres уже установлен.")
|
||||
|
||||
|
||||
def install_mariadb(dialog):
|
||||
"""Установка MariaDB"""
|
||||
if not is_installed("mariadb-server"):
|
||||
install_package(dialog, "mariadb-server")
|
||||
else:
|
||||
dialog.msgbox("MariaDB уже установлена.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
|
||||
def install_webmin(dialog):
|
||||
"""Установка Webmin и дополнительных пакетов"""
|
||||
if not is_installed("webmin"):
|
||||
install_package(dialog, "webmin")
|
||||
|
||||
# Установка cpanminus
|
||||
install_package(dialog, "postgresql-server-dev-16")
|
||||
install_package(dialog, "cpanminus")
|
||||
|
||||
run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
dialog.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# Установка модулей DBI и DBD::Pg через cpanminus
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI"
|
||||
):
|
||||
dialog.msgbox("Модуль DBI успешно установлен.")
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg"
|
||||
):
|
||||
dialog.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
dialog.msgbox("Webmin успешно перезагружен.")
|
||||
else:
|
||||
dialog.msgbox("Webmin уже установлен.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,533 @@
|
|||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Callable
|
||||
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
import socket
|
||||
|
||||
REGISTRY_URL = "dr.gosrv.ru"
|
||||
REGISTRY_LOGIN = "anon_user"
|
||||
REGISTRY_PASSWORD = "Qweasdzxc123C@"
|
||||
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
|
||||
|
||||
|
||||
class CommandExecutor:
|
||||
@staticmethod
|
||||
def run_command(
|
||||
command: str,
|
||||
shell: bool = False,
|
||||
input_data: Optional[str] = None,
|
||||
ignore_errors: List[int] = [],
|
||||
) -> subprocess.CompletedProcess:
|
||||
try:
|
||||
return subprocess.run(
|
||||
command if shell else shlex.split(command),
|
||||
shell=shell,
|
||||
check=True,
|
||||
input=input_data,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode not in ignore_errors:
|
||||
logger.error(f"Ошибка выполнения команды: {e}")
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}"
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
class SystemChecker:
|
||||
@staticmethod
|
||||
def is_command_installed(command: str) -> bool:
|
||||
try:
|
||||
return CommandExecutor.run_command(f"which {command}").returncode == 0
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
class ProgressGauge:
|
||||
def __init__(self, dialog: Dialog, title: str, total_steps: int):
|
||||
self.dialog = dialog
|
||||
self.title = title
|
||||
self.total_steps = total_steps
|
||||
self.current_step = 0
|
||||
|
||||
def update(self, step: int, message: str):
|
||||
self.current_step = step
|
||||
percent = int((step / self.total_steps) * 100)
|
||||
self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True)
|
||||
|
||||
def __enter__(self):
|
||||
self.dialog.gauge_start(self.title, percent=0)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.dialog.gauge_stop()
|
||||
|
||||
|
||||
class DockerManager:
|
||||
@staticmethod
|
||||
def install(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Установка Docker...")
|
||||
progress_callback(1, "Обновление пакетов")
|
||||
CommandExecutor.run_command("apt-get update")
|
||||
progress_callback(2, "Установка Docker")
|
||||
CommandExecutor.run_command("apt-get install -y docker.io")
|
||||
progress_callback(3, "Установка Docker Compose")
|
||||
CommandExecutor.run_command("apt-get install -y docker-compose")
|
||||
progress_callback(4, "Выполнение входа в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(5, "Docker успешно установлен")
|
||||
logger.info("Docker успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Docker...")
|
||||
progress_callback(1, "Удаление Docker и Docker Compose")
|
||||
CommandExecutor.run_command("apt-get remove -y docker.io docker-compose")
|
||||
progress_callback(2, "Очистка зависимостей")
|
||||
CommandExecutor.run_command("apt-get autoremove -y")
|
||||
progress_callback(3, "Удаление файлов Docker")
|
||||
CommandExecutor.run_command("rm -rf /var/run/docker")
|
||||
CommandExecutor.run_command("rm -rf /var/lib/docker")
|
||||
progress_callback(4, "Docker успешно удален")
|
||||
logger.info("Docker успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def login_to_registry():
|
||||
logger.info("Выполняется вход в Docker registry...")
|
||||
try:
|
||||
result = CommandExecutor.run_command(
|
||||
f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}",
|
||||
shell=True,
|
||||
)
|
||||
logger.info("Успешный вход в Docker registry")
|
||||
logger.debug(f"Docker login output: {result.stdout}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка входа в Docker registry: {e}")
|
||||
logger.error(f"Stderr: {e.stderr}")
|
||||
raise
|
||||
|
||||
|
||||
class UFWManager:
|
||||
@staticmethod
|
||||
def add_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Добавление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Удаление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено"
|
||||
)
|
||||
|
||||
|
||||
class PortainerManager:
|
||||
@staticmethod
|
||||
def install(
|
||||
port: str, password: str, ip: str, progress_callback: Callable[[int, str], None]
|
||||
):
|
||||
logger.info("Установка Portainer...")
|
||||
progress_callback(1, "Вход в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(2, "Остановка предыдущей версии Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(3, "Удаление старых данных Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
"docker volume ls -q -f name=portainer* | xargs -r docker volume rm",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(4, "Установка apache2-utils для htpasswd")
|
||||
if not SystemChecker.is_command_installed("htpasswd"):
|
||||
CommandExecutor.run_command("apt-get install -y apache2-utils")
|
||||
|
||||
progress_callback(5, "Создание конфигурации Portainer")
|
||||
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
|
||||
password_hash = PortainerManager._generate_password_hash(password)
|
||||
PortainerManager._create_docker_compose_file(port, password_hash, ip)
|
||||
|
||||
progress_callback(6, "Запуск Portainer")
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d"
|
||||
)
|
||||
|
||||
# if ip != "127.0.0.1":
|
||||
if True:
|
||||
progress_callback(7, "Добавление правила UFW")
|
||||
UFWManager.add_rule(port)
|
||||
|
||||
progress_callback(8, "Portainer успешно установлен")
|
||||
logger.info("Portainer успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Portainer...")
|
||||
progress_callback(1, "Остановка Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
progress_callback(2, "Удаление файлов конфигурации")
|
||||
CommandExecutor.run_command(
|
||||
f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"
|
||||
)
|
||||
# try:
|
||||
# with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f:
|
||||
# config = f.read()
|
||||
# port_ip_match = re.search(r'"(.+):(\d+):9000"', config)
|
||||
# if port_ip_match:
|
||||
# ip, port = port_ip_match.groups()
|
||||
# # if True:
|
||||
# # progress_callback(3, "Удаление правила UFW")
|
||||
# # UFWManager.remove_rule(port)
|
||||
# except FileNotFoundError:
|
||||
# logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.")
|
||||
|
||||
progress_callback(4, "Portainer успешно удален")
|
||||
logger.info("Portainer успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def _generate_password_hash(password: str) -> str:
|
||||
return CommandExecutor.run_command(
|
||||
f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'",
|
||||
shell=True,
|
||||
).stdout.strip()
|
||||
|
||||
@staticmethod
|
||||
def _create_docker_compose_file(port: str, password_hash: str, ip: str):
|
||||
docker_compose_content = f"""
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: {REGISTRY_URL}/base_images/portainer:latest
|
||||
ports:
|
||||
- "{ip}:{port}:9000"
|
||||
command: --admin-password '{password_hash}'
|
||||
networks:
|
||||
- local
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- portainer-data:/data
|
||||
environment:
|
||||
- PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1
|
||||
deploy:
|
||||
mode: global
|
||||
|
||||
networks:
|
||||
local:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
portainer-data:
|
||||
"""
|
||||
with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f:
|
||||
f.write(docker_compose_content)
|
||||
|
||||
|
||||
class InputValidator:
|
||||
@staticmethod
|
||||
def validate(value: str, pattern: str) -> bool:
|
||||
return bool(re.match(pattern, value))
|
||||
|
||||
|
||||
class UIElement(ABC):
|
||||
def __init__(self, dialog: Dialog):
|
||||
self.dialog = dialog
|
||||
|
||||
@abstractmethod
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
|
||||
class Menu(UIElement):
|
||||
def show(self):
|
||||
choices = []
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
choices.append(("1", "Установить Docker"))
|
||||
else:
|
||||
choices.append(("1", "Удалить Docker"))
|
||||
|
||||
if SystemChecker.is_command_installed("docker"):
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
choices.append(("2", "Установить Portainer"))
|
||||
else:
|
||||
choices.append(("2", "Удалить Portainer"))
|
||||
|
||||
code, choice = self.dialog.menu("Выберите действие:", choices=choices)
|
||||
return code, choice
|
||||
|
||||
|
||||
class YesNoUI(UIElement):
|
||||
def show(self, text: str):
|
||||
return self.dialog.yesno(text) == self.dialog.OK
|
||||
|
||||
|
||||
class PasswordInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, password = self.dialog.passwordbox(
|
||||
"Введите пароль для Portainer:", insecure=True
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(password, r"^.{12,}$"):
|
||||
return password
|
||||
self.dialog.msgbox(
|
||||
"Пароль должен содержать минимум 12 символов.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class PortInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, port = self.dialog.inputbox(
|
||||
"Введите порт для Portainer:", init="9000"
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"):
|
||||
return port
|
||||
self.dialog.msgbox(
|
||||
"Некорректный порт. Введите число от 1 до 65535.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class IPInput(UIElement):
|
||||
def get_default_interface_ip(self):
|
||||
try:
|
||||
# Выполнение команды для получения IP-адреса основного интерфейса
|
||||
command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1"
|
||||
result = subprocess.check_output(command, shell=True, text=True)
|
||||
return result.strip() # Возвращаем IP-адрес без лишних символов
|
||||
except subprocess.CalledProcessError:
|
||||
return "127.0.0.1" # В случае ошибки вернём стандартный IP
|
||||
|
||||
def show(self):
|
||||
# Получаем IP-адрес основного сетевого интерфейса
|
||||
default_ip = self.get_default_interface_ip()
|
||||
|
||||
while True:
|
||||
# Используем полученный IP-адрес как начальное значение
|
||||
code, ip = self.dialog.inputbox(
|
||||
"Введите IP адрес для Portainer:", init=default_ip
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"):
|
||||
return ip
|
||||
self.dialog.msgbox(
|
||||
"Некорректный IP адрес. Введите адрес в формате X.X.X.X",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
|
||||
|
||||
class NetworkInterfaceInput(UIElement):
|
||||
def get_network_interfaces(self):
|
||||
try:
|
||||
# Get all network interfaces
|
||||
interfaces = subprocess.check_output(
|
||||
"ip -o link show", shell=True, text=True
|
||||
)
|
||||
# Extract interface names and their IP addresses
|
||||
interface_list = []
|
||||
for line in interfaces.split("\n"):
|
||||
if line:
|
||||
interface = line.split(":")[1].strip()
|
||||
ip = self.get_ip_address(interface)
|
||||
if ip:
|
||||
interface_list.append((interface, ip))
|
||||
return interface_list
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def get_ip_address(self, interface):
|
||||
try:
|
||||
ip = subprocess.check_output(
|
||||
f"ip -o -4 addr list {interface}", shell=True, text=True
|
||||
)
|
||||
return ip.split()[3].split("/")[0]
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def show(self):
|
||||
interfaces = self.get_network_interfaces()
|
||||
if not interfaces:
|
||||
self.dialog.msgbox(
|
||||
"Не удалось получить список сетевых интерфейсов.", width=50, height=10
|
||||
)
|
||||
return None
|
||||
|
||||
choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces]
|
||||
code, choice = self.dialog.menu(
|
||||
"Выберите сетевой интерфейс для Docker Swarm:", choices=choices
|
||||
)
|
||||
|
||||
if code == self.dialog.OK:
|
||||
return choice
|
||||
return None
|
||||
|
||||
|
||||
class DockerConfigUI:
|
||||
def __init__(self):
|
||||
self.dialog = Dialog(dialog="dialog")
|
||||
self.menu = Menu(self.dialog)
|
||||
self.password_input = PasswordInput(self.dialog)
|
||||
self.port_input = PortInput(self.dialog)
|
||||
self.ip_input = IPInput(self.dialog)
|
||||
self.network_interface_input = NetworkInterfaceInput(self.dialog)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
code, choice = self.menu.show()
|
||||
if code == self.dialog.OK:
|
||||
if choice == "1":
|
||||
self._handle_docker()
|
||||
elif choice == "2":
|
||||
self.handle_portainer()
|
||||
elif choice == "3":
|
||||
break
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Произошла ошибка: {e}")
|
||||
self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10)
|
||||
|
||||
def _handle_docker(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge:
|
||||
DockerManager.install(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно установлен.", width=50, height=10)
|
||||
logger.info("Docker успешно установлен.")
|
||||
yes_no_ui = YesNoUI(self.dialog)
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"
|
||||
):
|
||||
interface = self.network_interface_input.show()
|
||||
if interface:
|
||||
ip = interface.split("(")[1].split(")")[0]
|
||||
if ip:
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker swarm init --advertise-addr {ip}"
|
||||
)
|
||||
self.dialog.msgbox(
|
||||
f"Docker Swarm успешно инициализирован на интерфейсе {interface}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при инициализации Docker Swarm: {str(e)}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
f"Не удалось получить IP-адрес для интерфейса {interface}.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
"Инициализация Docker Swarm отменена.", width=50, height=10
|
||||
)
|
||||
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?"
|
||||
):
|
||||
self.handle_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
DockerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно удален.", width=50, height=10)
|
||||
logger.info("Docker успешно удален.")
|
||||
|
||||
def handle_portainer(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
self.dialog.msgbox(
|
||||
"Docker не установлен. Пожалуйста, сначала установите Docker.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
return
|
||||
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
self._install_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Portainer успешно удален.", width=50, height=10)
|
||||
logger.info("Portainer успешно удален.")
|
||||
|
||||
def _install_portainer(self):
|
||||
password = self.password_input.show()
|
||||
if not password:
|
||||
return
|
||||
|
||||
port = self.port_input.show()
|
||||
if not port:
|
||||
return
|
||||
|
||||
ip = self.ip_input.show()
|
||||
if not ip:
|
||||
return
|
||||
|
||||
try:
|
||||
with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge:
|
||||
PortainerManager.install(port, password, ip, gauge.update)
|
||||
self.dialog.msgbox(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
logger.info(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль"
|
||||
)
|
||||
except Exception as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при установке Portainer: {str(e)}", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
ui = DockerConfigUI()
|
||||
ui.run()
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import os
|
||||
|
||||
HOSTNAME_FILE = "/etc/hostname"
|
||||
HOSTS_FILE = "/etc/hosts"
|
||||
|
||||
|
||||
def get_current_hostname():
|
||||
try:
|
||||
with open(HOSTNAME_FILE, 'r') as f:
|
||||
return f.read().strip()
|
||||
except IOError as e:
|
||||
print(f"Ошибка при чтении hostname: {e}")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def change_hostname():
|
||||
d = Dialog(dialog="dialog")
|
||||
current_hostname = get_current_hostname()
|
||||
|
||||
code, new_hostname = d.inputbox(
|
||||
f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:",
|
||||
init=current_hostname
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return None
|
||||
|
||||
if new_hostname == current_hostname:
|
||||
d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.")
|
||||
return None
|
||||
|
||||
# Проверка валидности hostname
|
||||
if not is_valid_hostname(new_hostname):
|
||||
d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.")
|
||||
return None
|
||||
|
||||
# Подтверждение изменения
|
||||
code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?")
|
||||
if code != d.OK:
|
||||
return None
|
||||
|
||||
return new_hostname
|
||||
|
||||
|
||||
def is_valid_hostname(hostname):
|
||||
if len(hostname) > 63:
|
||||
return False
|
||||
if hostname[-1] == "-":
|
||||
return False
|
||||
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-")
|
||||
return all(char in allowed for char in hostname)
|
||||
|
||||
|
||||
def apply_hostname_change(new_hostname):
|
||||
try:
|
||||
subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True)
|
||||
if os.path.exists(HOSTS_FILE):
|
||||
subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_change_hostname():
|
||||
return change_hostname()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
new_hostname = run_change_hostname()
|
||||
if new_hostname:
|
||||
print(f"Новое доменное имя: {new_hostname}")
|
||||
else:
|
||||
print("Изменение доменного имени отменено.")
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
AVAILABLE_PACKAGES = [
|
||||
"neofetch",
|
||||
"webmin",
|
||||
"usermin",
|
||||
"cups",
|
||||
"wireguard",
|
||||
"wg-cmd",
|
||||
]
|
||||
|
||||
|
||||
def check_sudo():
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, message):
|
||||
"""Run a command and log output, showing a message box on success."""
|
||||
try:
|
||||
subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
logging.info(f"{message}: успешно выполнено.")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"{message}: ошибка - {e}")
|
||||
dialog.msgbox(f"{message}: ошибка.\n{e}")
|
||||
return False
|
||||
|
||||
|
||||
def install_packages():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
if not check_sudo():
|
||||
d.msgbox("Для установки пакетов необходимы права суперпользователя.")
|
||||
return
|
||||
|
||||
# Создаем список пакетов для выбора
|
||||
choices = [(package, "", False) for package in AVAILABLE_PACKAGES]
|
||||
|
||||
# Показываем checklist для выбора пакетов
|
||||
code, tags = d.checklist(
|
||||
"Выберите пакеты для установки:", choices=choices, title="Установка программ"
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
if not tags:
|
||||
d.msgbox("Вы не выбрали ни одного пакета для установки.")
|
||||
return
|
||||
|
||||
# Подтверждение установки
|
||||
code = d.yesno(
|
||||
f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?"
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
# Создаем gauge для отображения прогресса
|
||||
d.gauge_start("Установка пакетов...", percent=0)
|
||||
|
||||
try:
|
||||
# Обновление списка пакетов
|
||||
d.gauge_update(10, "Обновление списка пакетов...")
|
||||
subprocess.run(["apt", "update"], check=True, capture_output=True)
|
||||
|
||||
logging.info(f"Установка пакетов: {', '.join(tags)}")
|
||||
# Установка выбранных пакетов
|
||||
d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...")
|
||||
process = subprocess.Popen(
|
||||
["apt", "install", "-y"] + tags,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
# Чтение вывода процесса и обновление gauge
|
||||
for i, line in enumerate(process.stdout):
|
||||
d.gauge_update(20 + (i % 70), line.strip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
error = process.stderr.read()
|
||||
raise subprocess.CalledProcessError(process.returncode, process.args, error)
|
||||
|
||||
# Проверяем, если вебмин был выбран для установки
|
||||
if "webmin" in tags:
|
||||
d.gauge_update(90, "Настройка Webmin...")
|
||||
# Установка cpanminus
|
||||
run_command_with_log(
|
||||
d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
d.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# # Установка модулей DBI и DBD::Pg через cpanminus
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"):
|
||||
# d.msgbox("Модуль DBI успешно установлен.")
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"):
|
||||
# d.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
d.msgbox("Webmin успешно перезагружен.")
|
||||
|
||||
d.gauge_update(100, "Установка завершена успешно!")
|
||||
d.gauge_stop()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при установке пакетов: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Ошибка при установке пакетов: {e}")
|
||||
|
||||
|
||||
def run_install_packages():
|
||||
install_packages()
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import subprocess
|
||||
import ipaddress
|
||||
from dialog import Dialog
|
||||
|
||||
|
||||
class NetworkConfig:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.temp_network_settings = {}
|
||||
|
||||
def select_interface(self):
|
||||
interfaces = self.get_network_interfaces()
|
||||
if not interfaces:
|
||||
self.d.msgbox("Нет доступных сетевых интерфейсов.")
|
||||
return None
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите интерфейс:", choices=[(iface, "") for iface in interfaces]
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
self.d.msgbox(f"Выбран интерфейс: {tag}")
|
||||
self.show_current_settings(tag) # Добавлено окно с текущими настройками
|
||||
return tag
|
||||
else:
|
||||
self.d.msgbox("Интерфейс не выбран.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_network_interfaces():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ls", "/sys/class/net/"], capture_output=True, text=True, check=True
|
||||
)
|
||||
interfaces = result.stdout.strip().split("\n")
|
||||
return interfaces
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def show_current_settings(self, interface):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True
|
||||
)
|
||||
current_settings = result.stdout.strip()
|
||||
|
||||
ip_address = None
|
||||
if "inet " in current_settings:
|
||||
ip_address = current_settings.split("inet ")[1].split("/")[0]
|
||||
|
||||
if "dhcp" in current_settings:
|
||||
config_type = "DHCP"
|
||||
elif ip_address:
|
||||
config_type = "Static"
|
||||
else:
|
||||
config_type = "Неизвестно"
|
||||
|
||||
message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен"
|
||||
|
||||
self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}")
|
||||
|
||||
def configure_dhcp(self, interface):
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
if code == self.d.OK:
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "dhcp",
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("DHCP настройка сохранена.")
|
||||
else:
|
||||
self.d.msgbox("DHCP настройка отменена.")
|
||||
|
||||
def configure_static(self, interface):
|
||||
code, ip = self.d.inputbox("Введите IP адрес:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, mask = self.d.inputbox("Введите маску подсети:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, gateway = self.d.inputbox("Введите шлюз:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
|
||||
if ip and mask and gateway:
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "static",
|
||||
"ip": ip,
|
||||
"mask": mask,
|
||||
"gateway": gateway,
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("Статические настройки сохранены.")
|
||||
else:
|
||||
self.d.msgbox("Некоторые параметры не заданы.")
|
||||
|
||||
def configure_cidr(self, interface):
|
||||
code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, gateway = self.d.inputbox("Введите шлюз:")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
|
||||
|
||||
if cidr:
|
||||
try:
|
||||
ip = ipaddress.ip_interface(cidr).ip
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
mask = network.netmask
|
||||
self.temp_network_settings[interface] = {
|
||||
"type": "static",
|
||||
"ip": str(ip),
|
||||
"mask": str(mask),
|
||||
"gateway": gateway,
|
||||
"dns": dns,
|
||||
"interface": interface,
|
||||
}
|
||||
self.d.msgbox("CIDR конфигурация сохранена.")
|
||||
except ValueError as e:
|
||||
self.d.msgbox(f"Неверный формат CIDR: {e}")
|
||||
else:
|
||||
self.d.msgbox("CIDR не задан.")
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
interface = self.select_interface()
|
||||
if not interface:
|
||||
break
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите действие для интерфейса:",
|
||||
choices=[
|
||||
("1", "Настроить DHCP"),
|
||||
("2", "Настроить статический IP"),
|
||||
("3", "Настроить CIDR"),
|
||||
("4", "Выбрать другой интерфейс"),
|
||||
("5", "Выход"),
|
||||
],
|
||||
)
|
||||
|
||||
if code == self.d.OK:
|
||||
if tag == "1":
|
||||
self.configure_dhcp(interface)
|
||||
elif tag == "2":
|
||||
self.configure_static(interface)
|
||||
elif tag == "3":
|
||||
self.configure_cidr(interface)
|
||||
elif tag == "4":
|
||||
continue
|
||||
elif tag == "5":
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return self.temp_network_settings
|
||||
|
||||
def save_and_apply_network_settings(self):
|
||||
for interface, settings in self.temp_network_settings.items():
|
||||
try:
|
||||
if settings["type"] == "dhcp":
|
||||
self.save_dhcp(interface, settings)
|
||||
elif settings["type"] == "static":
|
||||
self.save_static(interface, settings)
|
||||
if settings.get("dns"):
|
||||
self.update_resolv_conf(settings["dns"])
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "networking"], check=True)
|
||||
self.d.msgbox("Сетевые настройки успешно применены.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}")
|
||||
return False
|
||||
|
||||
def save_dhcp(self, interface, settings):
|
||||
interface_config = f"""
|
||||
auto {interface}
|
||||
iface {interface} inet dhcp
|
||||
"""
|
||||
self.update_interface_config(interface, interface_config)
|
||||
|
||||
def save_static(self, interface, settings):
|
||||
ip = settings["ip"]
|
||||
mask = settings["mask"]
|
||||
gateway = settings["gateway"]
|
||||
interface_config = f"""
|
||||
auto {interface}
|
||||
iface {interface} inet static
|
||||
address {ip}
|
||||
netmask {mask}
|
||||
"""
|
||||
if gateway:
|
||||
interface_config += f"gateway {gateway}\n"
|
||||
self.update_interface_config(interface, interface_config)
|
||||
|
||||
def update_interface_config(self, interface, interface_config):
|
||||
try:
|
||||
with open("/etc/network/interfaces", "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
with open("/etc/network/interfaces", "w") as f:
|
||||
skip_section = False
|
||||
for line in lines:
|
||||
if interface in line:
|
||||
skip_section = True
|
||||
continue
|
||||
if skip_section and line.strip() == "":
|
||||
skip_section = False
|
||||
if not skip_section:
|
||||
f.write(line)
|
||||
f.write(interface_config.strip() + "\n")
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}")
|
||||
|
||||
def update_resolv_conf(self, dns):
|
||||
if not dns:
|
||||
return
|
||||
|
||||
dns_entry = f"nameserver {dns}"
|
||||
try:
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
resolv_lines = f.readlines()
|
||||
|
||||
if any(line.strip() == dns_entry for line in resolv_lines):
|
||||
return
|
||||
|
||||
with open("/etc/resolv.conf", "a") as f:
|
||||
f.write(dns_entry + "\n")
|
||||
except Exception as e:
|
||||
self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
configurator = NetworkConfig()
|
||||
configurator.run()
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import pwd
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_users():
|
||||
users = []
|
||||
try:
|
||||
for user in pwd.getpwall():
|
||||
if user.pw_uid >= 1000 and user.pw_name != "nobody":
|
||||
users.append(user.pw_name)
|
||||
if "root" not in users:
|
||||
users.append("root")
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении списка пользователей: {e}")
|
||||
return users
|
||||
|
||||
|
||||
def install_zsh():
|
||||
try:
|
||||
subprocess.run(["apt", "update"], check=True)
|
||||
subprocess.run(["apt", "install", "-y", "zsh"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Ошибка при установке zsh: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def change_shell():
|
||||
d = Dialog(dialog="dialog")
|
||||
users = get_users()
|
||||
|
||||
# Первый шаг - выбор опции с помощью radiolist
|
||||
code, choice = d.radiolist(
|
||||
"Выберите способ изменения оболочки:",
|
||||
choices=[
|
||||
("Для всех пользователей", "", False),
|
||||
("Выбрать конкретных пользователей", "", True)
|
||||
]
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
selected_users = []
|
||||
|
||||
if choice == "Для всех пользователей":
|
||||
# Если выбрано "Для всех пользователей"
|
||||
selected_users = users
|
||||
elif choice == "Выбрать конкретных пользователей":
|
||||
# Если выбран выбор конкретных пользователей, показываем checklist
|
||||
code, selected_users = d.checklist(
|
||||
"Выберите пользователей для изменения оболочки:",
|
||||
choices=[(user, "", False) for user in users],
|
||||
clear=True
|
||||
)
|
||||
if code != d.OK or not selected_users:
|
||||
return
|
||||
|
||||
# Выбор оболочки
|
||||
code, shell = d.menu(
|
||||
"Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")]
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
shell_path = "/bin/bash" if shell == "1" else "/bin/zsh"
|
||||
|
||||
if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"):
|
||||
logging.info("zsh не установлен. Начинаем установку...")
|
||||
d.msgbox("zsh не установлен. Начинаем установку...")
|
||||
if not install_zsh():
|
||||
d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.")
|
||||
return
|
||||
|
||||
# Применение изменений
|
||||
if choice == "Для всех пользователей":
|
||||
# Применение изменений для всех пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
d.msgbox("Оболочка изменена для всех существующих пользователей")
|
||||
logging.info("Оболочка изменена для всех существующих пользователей")
|
||||
else:
|
||||
# Применение изменений для конкретных пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
|
||||
|
||||
def run_change_shell():
|
||||
change_shell()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_change_shell()
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import logging
|
||||
import os
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
class SSHConfig:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.current_port = self.get_current_ssh_port()
|
||||
self.current_auth_method = self.get_current_auth_method()
|
||||
self.new_port = None
|
||||
self.new_auth_method = None
|
||||
|
||||
def get_sshd_status(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", "sshd"], capture_output=True, text=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return "unknown"
|
||||
|
||||
def toggle_sshd_service(self, action):
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", action, "sshd"], check=True)
|
||||
self.d.msgbox(
|
||||
f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}."
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при {action} службы sshd: {e}")
|
||||
self.d.msgbox(
|
||||
f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd."
|
||||
)
|
||||
|
||||
def get_current_ssh_port(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning(
|
||||
"Не удалось получить порт SSH, используется порт по умолчанию"
|
||||
)
|
||||
return "22"
|
||||
|
||||
def change_ssh_port(self):
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
code, new_port = self.d.inputbox(
|
||||
f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:",
|
||||
init=port_display,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
try:
|
||||
new_port = int(new_port)
|
||||
if new_port < 1 or new_port > 65535:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.")
|
||||
return
|
||||
code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_port = str(new_port)
|
||||
|
||||
def get_current_auth_method(self):
|
||||
try:
|
||||
with open(SSH_AUTH_CONFIG_PATH, "r") as f:
|
||||
content = f.read()
|
||||
password_auth = "yes" if "PasswordAuthentication yes" in content else "no"
|
||||
pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no"
|
||||
|
||||
if password_auth == "yes" and pubkey_auth == "yes":
|
||||
return "По ключу и паролю"
|
||||
elif password_auth == "no" and pubkey_auth == "yes":
|
||||
return "Только по ключу"
|
||||
elif password_auth == "yes" and pubkey_auth == "no":
|
||||
return "Только по паролю"
|
||||
else:
|
||||
return "По ключу и паролю" # Default value
|
||||
except FileNotFoundError:
|
||||
logging.warning("Файл конфигурации аутентификации SSH не найден")
|
||||
return "По ключу и паролю" # Default value
|
||||
except Exception as e:
|
||||
logging.warning(f"Не удалось получить метод аутентификации SSH: {e}")
|
||||
return "По ключу и паролю" # Default value
|
||||
|
||||
def change_auth_method(self):
|
||||
auth_method_display = (
|
||||
self.new_auth_method if self.new_auth_method else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
auth_choices = [
|
||||
(
|
||||
"1",
|
||||
f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}",
|
||||
),
|
||||
(
|
||||
"2",
|
||||
f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}",
|
||||
),
|
||||
(
|
||||
"3",
|
||||
f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}",
|
||||
),
|
||||
]
|
||||
code, tag = self.d.menu(
|
||||
f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:",
|
||||
choices=auth_choices,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_auth_method = {
|
||||
"1": "Только по паролю",
|
||||
"2": "Только по ключу",
|
||||
"3": "По ключу и паролю",
|
||||
}[tag]
|
||||
|
||||
def apply_changes(self):
|
||||
if self.new_port:
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^#*Port .*/Port {self.new_port}/",
|
||||
SSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={self.new_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(
|
||||
["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
self.current_port = self.new_port
|
||||
self.new_port = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении порта SSH: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении порта SSH: {e}")
|
||||
|
||||
if self.new_auth_method:
|
||||
try:
|
||||
auth_config = ""
|
||||
if self.new_auth_method == "Только по паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication no\n"
|
||||
)
|
||||
elif self.new_auth_method == "Только по ключу":
|
||||
auth_config = (
|
||||
"PasswordAuthentication no\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
elif self.new_auth_method == "По ключу и паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True)
|
||||
with open(SSH_AUTH_CONFIG_PATH, "w") as f:
|
||||
f.write(auth_config)
|
||||
|
||||
self.current_auth_method = self.new_auth_method
|
||||
self.new_auth_method = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox("Метод аутентификации SSH успешно изменен.")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
|
||||
def main_menu(self):
|
||||
while True:
|
||||
status = self.get_sshd_status()
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
auth_method_display = (
|
||||
self.new_auth_method
|
||||
if self.new_auth_method
|
||||
else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
|
||||
code, tag = self.d.menu(
|
||||
f"Статус sshd: {status}\n"
|
||||
f"Текущий порт: {port_display}{port_status}\n"
|
||||
f"Метод аутентификации: {auth_method_display}{auth_status}",
|
||||
choices=[
|
||||
("1", "Включить sshd" if status != "active" else "Выключить sshd"),
|
||||
("2", "Изменить порт SSH"),
|
||||
("3", "Изменить метод аутентификации"),
|
||||
],
|
||||
)
|
||||
if code != self.d.OK:
|
||||
break
|
||||
if tag == "1":
|
||||
self.toggle_sshd_service("start" if status != "active" else "stop")
|
||||
elif tag == "2":
|
||||
self.change_ssh_port()
|
||||
elif tag == "3":
|
||||
self.change_auth_method()
|
||||
|
||||
return self.new_port is not None or self.new_auth_method is not None
|
||||
|
||||
|
||||
def run_ssh_config():
|
||||
ssh_config = SSHConfig()
|
||||
changes_made = ssh_config.main_menu()
|
||||
return ssh_config, changes_made
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ssh_config, changes_made = run_ssh_config()
|
||||
# Note: Changes will be applied in the main menu when "Save and exit" is selected
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def run_update():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
code = d.yesno("Вы действительно хотите обновить систему?", width=50)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
commands = [["apt", "update"], ["apt", "upgrade", "-y"]]
|
||||
|
||||
d.gauge_start("Обновление системы", percent=0)
|
||||
|
||||
try:
|
||||
for i, command in enumerate(commands):
|
||||
d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
["sudo"] + command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
error = process.stderr.read()
|
||||
raise subprocess.CalledProcessError(process.returncode, command, error)
|
||||
|
||||
d.gauge_update(100, "Обновление системы завершено успешно")
|
||||
d.gauge_stop()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при обновлении системы: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Ошибка при обновлении системы: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Неожиданная ошибка при обновлении системы: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}")
|
||||
|
||||
d.msgbox("Обновление системы завершено", width=50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_update()
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def check_root():
|
||||
if os.geteuid() != 0:
|
||||
print("Запустите скрипт с правами суперпользователя")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
check_root()
|
||||
Binary file not shown.
21
deb_dist/postinstall-1.5.0/debian/rules
Executable file
21
deb_dist/postinstall-1.5.0/debian/rules
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
# This file was automatically generated by stdeb 0.10.0 at
|
||||
# Wed, 27 Nov 2024 15:29:20 +0500
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=python_distutils
|
||||
|
||||
override_dh_auto_clean:
|
||||
python3 setup.py clean -a
|
||||
find . -name \*.pyc -exec rm {} \;
|
||||
|
||||
override_dh_auto_build:
|
||||
python3 setup.py build --force
|
||||
|
||||
override_dh_auto_install:
|
||||
python3 setup.py install --force --root=debian/python3-postinstall --no-compile -O0 --install-layout=deb --prefix=/usr
|
||||
|
||||
override_dh_python2:
|
||||
dh_python2 --no-guessing-versions
|
||||
|
||||
1
deb_dist/postinstall-1.5.0/debian/source/format
Normal file
1
deb_dist/postinstall-1.5.0/debian/source/format
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
||||
1
deb_dist/postinstall-1.5.0/debian/source/options
Normal file
1
deb_dist/postinstall-1.5.0/debian/source/options
Normal file
|
|
@ -0,0 +1 @@
|
|||
extend-diff-ignore="\.egg-info$"
|
||||
243
deb_dist/postinstall-1.5.0/postinstall.egg-info/PKG-INFO
Normal file
243
deb_dist/postinstall-1.5.0/postinstall.egg-info/PKG-INFO
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: postinstall
|
||||
Version: 1.5.0
|
||||
Summary: A Dialog TUI application for postinstall system configuration
|
||||
Author: LLC Technologies and Communication
|
||||
Author-email: devteam@texnico.ru
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent :: Linux
|
||||
Requires-Python: >=3.11
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: pythondialog<4.0.0,>=3.5.3
|
||||
|
||||
## Документация Postinstall
|
||||
|
||||
Панель Postinstall представляет собой интерфейс для настройки системы после её установки. Она обеспечивает удобный способ управления различными аспектами конфигурации системы, начиная от сетевых настроек и заканчивая установкой баз данных и веб-интерфейсов. Панель позволяет пользователю выбирать необходимые параметры через графический интерфейс на основе диалоговых окон.
|
||||
### **1. Сетевые настройки**
|
||||
|
||||
Раздел "Сетевые настройки" предназначен для конфигурации сетевых параметров системы, включая настройку как статического IP-адреса, так и динамического (DHCP). Этот раздел позволяет управлять подключениями к сети через интуитивно понятный интерфейс, обеспечивая гибкость и удобство.
|
||||
|
||||
#### **1.1 Выбор сетевого интерфейса**
|
||||
|
||||
Прежде чем приступить к настройке сети, необходимо выбрать сетевой интерфейс, который вы хотите сконфигурировать. На экране будет представлен примерный список доступных интерфейсов. Вы выбираете нужный интерфейс, после чего ему будут доступны дальнейшие настройки.
|
||||
|
||||

|
||||
|
||||
|
||||
#### **1.2 Настройка DHCP**
|
||||
|
||||
Если вы предпочитаете автоматическое назначение сетевых параметров, выберите опцию "Настроить DHCP". Эта опция автоматически получит IP-адрес и другие параметры сети от DHCP-сервера и внесет изменения в файл конфигурации `/etc/network/interfaces`. Также система обновит DNS-серверы в файле `/etc/resolv.conf`.
|
||||
|
||||
#### **1.3 Настройка статического IP**
|
||||
|
||||
Для ручной настройки сетевых параметров выберите "Настроить статический IP". Здесь вам будет предложено ввести IP-адрес, маску подсети и шлюз вручную. Эти значения будут записаны в файл `/etc/network/interfaces` и применены при следующем перезапуске сетевого интерфейса.
|
||||
|
||||
#### **1.4 Настройка CIDR**
|
||||
|
||||
Опция "Настроить CIDR" позволяет указать сетевой диапазон в формате CIDR (например, `192.168.1.0/24`). CIDR (Classless Inter-Domain Routing) предоставляет гибкость при настройке подсетей, позволяя администратору точно указать, какой диапазон IP-адресов будет использоваться в сети.
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
|
||||
### **5. Настройки SSH**
|
||||
|
||||
SSH является важным инструментом для удаленного управления серверами. В этом разделе можно настроить SSH-сервер, управлять ключами и изменить порт по умолчанию.
|
||||
|
||||
#### **5.1 Включение/отключение SSH**
|
||||
|
||||
Вы можете включить или отключить SSH-сервер через интерфейс панели. Включение SSH позволяет удаленно управлять сервером, тогда как отключение SSH помогает предотвратить несанкционированный доступ.
|
||||
|
||||
#### **5.2 Управление ключами SSH**
|
||||
|
||||
Для повышения безопасности можно использовать аутентификацию с помощью SSH-ключей. В этом разделе можно добавить или удалить ключи SSH, что позволяет усилить защиту сервера от атак.
|
||||
|
||||
#### **5.3 Изменение порта SSH**
|
||||
|
||||
Смена порта SSH с порта по умолчанию (`22`) на другой может помочь уменьшить риск автоматических атак на сервер. Вы можете изменить порт SSH в панели, что автоматически обновит конфигурационный файл `/etc/ssh/sshd_config`.
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
|
||||
---
|
||||
### **11. Откат последних изменений**
|
||||
|
||||
Этот раздел предоставляет функцию отката к предыдущему состоянию системы, что позволяет отменить изменения, внесенные в конфигурацию через панель.
|
||||
|
||||
---
|
||||
|
||||
### **12. Сохранить и выйти**
|
||||
|
||||
Последний шаг в панели — это подтверждение изменений и их окончательное применение. Вы можете сохранить все внесенные изменения или выйти без их сохранения.
|
||||
37
deb_dist/postinstall-1.5.0/postinstall.egg-info/SOURCES.txt
Normal file
37
deb_dist/postinstall-1.5.0/postinstall.egg-info/SOURCES.txt
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
MANIFEST.in
|
||||
README.md
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
setup.py
|
||||
deb/0002-postinstall.sh
|
||||
deb/postinst
|
||||
deb/postinstall-autostart.desktop
|
||||
deb/postinstall.desktop
|
||||
deb/postinstall.service
|
||||
deb/postinstall_start.sh
|
||||
deb/python3-textual_0.1.13-1_all.deb
|
||||
deb/textual-0.72.0-py3-none-any.whl
|
||||
postinstall/__init__.py
|
||||
postinstall/__main__.py
|
||||
postinstall/logger.py
|
||||
postinstall/root.py
|
||||
postinstall.egg-info/PKG-INFO
|
||||
postinstall.egg-info/SOURCES.txt
|
||||
postinstall.egg-info/dependency_links.txt
|
||||
postinstall.egg-info/entry_points.txt
|
||||
postinstall.egg-info/requires.txt
|
||||
postinstall.egg-info/top_level.txt
|
||||
postinstall/option/__init__.py
|
||||
postinstall/option/add_user.py
|
||||
postinstall/option/autostart.py
|
||||
postinstall/option/database.py
|
||||
postinstall/option/docker.py
|
||||
postinstall/option/hostname.py
|
||||
postinstall/option/install_packages.py
|
||||
postinstall/option/network.py
|
||||
postinstall/option/shell.py
|
||||
postinstall/option/ssh.py
|
||||
postinstall/option/update.py
|
||||
tests/__init__.py
|
||||
tests/docker_config.py
|
||||
tests/test_docker.py
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[console_scripts]
|
||||
postinstall = postinstall:main
|
||||
|
|
@ -0,0 +1 @@
|
|||
pythondialog<4.0.0,>=3.5.3
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
postinstall
|
||||
tests
|
||||
395
deb_dist/postinstall-1.5.0/postinstall/__init__.py
Normal file
395
deb_dist/postinstall-1.5.0/postinstall/__init__.py
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
from dialog import Dialog
|
||||
from postinstall.root import check_root
|
||||
from postinstall.option import (
|
||||
network,
|
||||
update,
|
||||
add_user,
|
||||
install_packages,
|
||||
ssh,
|
||||
hostname,
|
||||
shell,
|
||||
database,
|
||||
docker,
|
||||
)
|
||||
import json
|
||||
|
||||
check_root()
|
||||
|
||||
from postinstall.logger import logger
|
||||
|
||||
# import debugpy
|
||||
|
||||
# debugpy.listen(("0.0.0.0", 5678))
|
||||
# debugpy.wait_for_client()
|
||||
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
HOSTNAME_FILE = "/etc/hostname"
|
||||
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
|
||||
BACKUPS_FILE = "/usr/share/postinstall/backups.json"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_hostname() -> str:
|
||||
try:
|
||||
with open(HOSTNAME_FILE, "r") as f:
|
||||
return f.read().strip()
|
||||
except IOError as e:
|
||||
logging.error(f"Ошибка при чтении hostname: {e}")
|
||||
return "localhost"
|
||||
|
||||
|
||||
def get_ssh_port() -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Не удалось получить порт SSH, используется порт по умолчанию")
|
||||
return "22"
|
||||
|
||||
|
||||
class MenuScreen:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.ssh_port = get_ssh_port()
|
||||
self.hostname = get_hostname()
|
||||
self.ssh_changes = None
|
||||
self.new_hostname = None
|
||||
self.network = None
|
||||
self.autostart_enabled = self.check_auto_start()
|
||||
self.new_autostart_enabled = self.autostart_enabled
|
||||
|
||||
self.options = [
|
||||
{
|
||||
"name": "Сетевые настройки",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "network",
|
||||
},
|
||||
{
|
||||
"name": "Обновление системы",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "update",
|
||||
},
|
||||
{
|
||||
"name": "Добавить пользователя",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "add_user",
|
||||
},
|
||||
{
|
||||
"name": "Установка программ",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "install_packages",
|
||||
},
|
||||
{
|
||||
"name": "Настройки ssh",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "ssh",
|
||||
},
|
||||
{
|
||||
"name": "Изменить доменное имя ПК",
|
||||
"value": self.hostname,
|
||||
"tabs": "\t \t",
|
||||
"option": "hostname",
|
||||
},
|
||||
{"name": "Выбор оболочки", "value": "", "tabs": "\t \t", "option": "shell"},
|
||||
{
|
||||
"name": "Автозапуск при следующем запуске",
|
||||
"value": "Включен" if self.new_autostart_enabled else "Отключен",
|
||||
"tabs": "\t",
|
||||
"option": "auto_start",
|
||||
},
|
||||
{
|
||||
"name": "Настройка базы данных",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "database",
|
||||
},
|
||||
{
|
||||
"name": "Настройка docker",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "docker",
|
||||
},
|
||||
{
|
||||
"name": "Откат последних изменений",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "rollback",
|
||||
},
|
||||
{
|
||||
"name": "Сохранить и выйти",
|
||||
"value": "",
|
||||
"tabs": "\t \t",
|
||||
"option": "save_and_exit",
|
||||
},
|
||||
]
|
||||
|
||||
os.makedirs(os.path.dirname(AUTOSTART_FILE), exist_ok=True)
|
||||
|
||||
def show_menu(self):
|
||||
choices = []
|
||||
for i, option in enumerate(self.options, 1):
|
||||
value = option["value"]
|
||||
if option["option"] == "hostname":
|
||||
value = (
|
||||
self.new_hostname
|
||||
if self.new_hostname is not None
|
||||
else self.hostname
|
||||
)
|
||||
if self.new_hostname is not None:
|
||||
value = f"{value} (не применено)"
|
||||
elif option["option"] == "network":
|
||||
value = "Изменено (не применено)" if self.network else ""
|
||||
|
||||
choice_text = f"{option['name']}{option['tabs']}{value}"
|
||||
choices.append((str(i), choice_text))
|
||||
|
||||
code, tag = self.d.menu(
|
||||
"Выберите опцию:",
|
||||
choices=choices,
|
||||
title="Главное меню",
|
||||
cancel_label="Выйти без сохранения",
|
||||
width=70,
|
||||
height=15,
|
||||
)
|
||||
|
||||
if code == self.d.OK and tag:
|
||||
return self.options[int(tag) - 1]["option"]
|
||||
return None
|
||||
|
||||
def set_temp_network(self, settings):
|
||||
self.temp_network = settings
|
||||
|
||||
def check_auto_start(self) -> bool:
|
||||
return not os.path.exists(AUTOSTART_FILE)
|
||||
|
||||
def set_auto_start(self, enable: bool) -> None:
|
||||
try:
|
||||
if enable:
|
||||
if os.path.exists(AUTOSTART_FILE):
|
||||
os.remove(AUTOSTART_FILE)
|
||||
logging.info("Автозапуск включен")
|
||||
else:
|
||||
with open(AUTOSTART_FILE, "w") as f:
|
||||
f.write("# Это файл создан для отключения автозапуска\n")
|
||||
logging.info("Автозапуск отключен")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении автозапуска: {e}")
|
||||
|
||||
def update_ssh_port(self, new_port):
|
||||
self.new_ssh_port = new_port
|
||||
|
||||
def update_hostname(self, new_hostname):
|
||||
self.new_hostname = new_hostname
|
||||
|
||||
def update_autostart(self, new_state):
|
||||
self.new_autostart_enabled = new_state
|
||||
|
||||
|
||||
def choose(option, menu):
|
||||
if menu.new_autostart_enabled != menu.autostart_enabled:
|
||||
menu.set_auto_start(menu.new_autostart_enabled)
|
||||
if option == "network":
|
||||
net_config = network.NetworkConfig()
|
||||
if net_config.run():
|
||||
menu.network = net_config
|
||||
elif option == "update":
|
||||
logging.info("Начало обновления системы.")
|
||||
update.run_update()
|
||||
logging.info("Завершено обновление системы.")
|
||||
elif option == "add_user":
|
||||
logging.info("Началось добавления пользователя.")
|
||||
add_user.run_add_user()
|
||||
logging.info("Завершено добавление пользователя.")
|
||||
elif option == "install_packages":
|
||||
logging.info("Начало установки пакетов.")
|
||||
install_packages.run_install_packages()
|
||||
logging.info("Завершена установка пакетов.")
|
||||
elif option == "ssh":
|
||||
ssh_config, changes_made = ssh.run_ssh_config()
|
||||
if changes_made:
|
||||
menu.ssh_changes = ssh_config
|
||||
elif option == "hostname":
|
||||
new_hostname = hostname.run_change_hostname()
|
||||
if new_hostname:
|
||||
menu.new_hostname = new_hostname
|
||||
menu.options[5]["value"] = f"{new_hostname} (не применено)"
|
||||
elif option == "shell":
|
||||
logging.info("Началось изменение shell.")
|
||||
shell.run_change_shell()
|
||||
logging.info("Завершено изменение shell.")
|
||||
elif option == "auto_start":
|
||||
menu.new_autostart_enabled = not menu.new_autostart_enabled
|
||||
menu.options[7]["value"] = (
|
||||
"Включен" if menu.new_autostart_enabled else "Отключен"
|
||||
)
|
||||
logging.info(
|
||||
f"Автозапуск {'включен' if menu.new_autostart_enabled else 'отключен'}"
|
||||
)
|
||||
elif option == "database":
|
||||
logging.info("Запуск настройки базы данных.")
|
||||
|
||||
# Открытие диалогового окна для выбора базы данных
|
||||
code, choice = menu.d.menu("Выберите базу данных для установки:",
|
||||
choices=[("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB")])
|
||||
|
||||
if code == menu.d.OK:
|
||||
if choice == "PostgreSQL":
|
||||
if not database.is_installed('postgresql-16'):
|
||||
database.install_postgresql(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("PostgreSQL и Harlequin уже установлены.")
|
||||
|
||||
elif choice == "MariaDB":
|
||||
if not database.is_installed('mariadb-server'):
|
||||
database.install_mariadb(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("MariaDB уже установлена.")
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if not database.is_installed('webmin'):
|
||||
code = menu.d.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
if code == menu.d.OK:
|
||||
database.install_webmin(menu.d)
|
||||
else:
|
||||
menu.d.msgbox("Webmin уже установлен.")
|
||||
|
||||
logging.info("Настройка базы данных завершена.")
|
||||
else:
|
||||
menu.d.msgbox("Выбор базы данных отменён")
|
||||
elif option == "docker":
|
||||
docker.run()
|
||||
|
||||
elif option == "rollback":
|
||||
try:
|
||||
with open(BACKUPS_FILE, "r") as f:
|
||||
backups = json.load(f)
|
||||
f.close()
|
||||
for i in backups:
|
||||
with open(i["file"], "w") as f:
|
||||
f.write(i["content"])
|
||||
f.close()
|
||||
subprocess.run(["systemctl", "restart", "networking"], check=True)
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
ssh_port = get_ssh_port()
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={ssh_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
|
||||
menu.d.msgbox("Резервные копии успешно восстановлены")
|
||||
logging.info("Резервные копии успешно восстановлены")
|
||||
except Exception as e:
|
||||
menu.d.msgbox(f"Вы еще не применяли изменения.{e}")
|
||||
return False
|
||||
|
||||
elif option == "save_and_exit":
|
||||
with open("/etc/network/interfaces", "r") as f:
|
||||
interfaces = f.read()
|
||||
f.close()
|
||||
with open("/etc/resolv.conf", "r") as f:
|
||||
dns_settings = f.read()
|
||||
f.close()
|
||||
with open("/etc/ssh/sshd_config", "r") as f:
|
||||
ssh_settings = f.read()
|
||||
f.close()
|
||||
try:
|
||||
with open("/etc/ssh/sshd_config.d/auth.conf", "r") as f:
|
||||
ssh_auth = f.read()
|
||||
except Exception:
|
||||
ssh_auth = ""
|
||||
backup = [
|
||||
{
|
||||
"file": "/etc/network/interfaces",
|
||||
"content": interfaces,
|
||||
},
|
||||
{
|
||||
"file": "/etc/resolv.conf",
|
||||
"content": dns_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config",
|
||||
"content": ssh_settings,
|
||||
},
|
||||
{
|
||||
"file": "/etc/ssh/sshd_config.d/auth.conf",
|
||||
"content": ssh_auth,
|
||||
},
|
||||
]
|
||||
|
||||
with open(BACKUPS_FILE, "w") as f:
|
||||
f.write(json.dumps(backup))
|
||||
f.close()
|
||||
|
||||
if menu.ssh_changes:
|
||||
logging.info(
|
||||
f"Применены новые настройки SSH. {'Новый порт SSH: ' + str(menu.ssh_changes.new_port) if menu.ssh_changes.new_port is not None else ''} {'Новый метод авторизации: ' + menu.ssh_changes.new_auth_method if menu.ssh_changes.new_auth_method is not None else ''}"
|
||||
)
|
||||
menu.ssh_changes.apply_changes()
|
||||
if menu.new_hostname:
|
||||
logging.info(f"Применено новое имя хоста: {menu.new_hostname}")
|
||||
if hostname.apply_hostname_change(menu.new_hostname):
|
||||
menu.hostname = menu.new_hostname
|
||||
if menu.network:
|
||||
logging.info(
|
||||
f"Применены новые настройки сети: {menu.network.temp_network_settings}"
|
||||
)
|
||||
if not menu.network.save_and_apply_network_settings():
|
||||
return True
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
logging.info("Приложение запущено")
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
menu = MenuScreen()
|
||||
while True:
|
||||
option = menu.show_menu()
|
||||
if option is None:
|
||||
break
|
||||
if not choose(option, menu):
|
||||
break
|
||||
logging.info("Приложение закрыто")
|
||||
subprocess.run("clear", shell=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
deb_dist/postinstall-1.5.0/postinstall/__main__.py
Normal file
3
deb_dist/postinstall-1.5.0/postinstall/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import postinstall
|
||||
|
||||
postinstall.main()
|
||||
25
deb_dist/postinstall-1.5.0/postinstall/logger.py
Normal file
25
deb_dist/postinstall-1.5.0/postinstall/logger.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
def setup_logger():
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s")
|
||||
|
||||
file_handler = logging.FileHandler("/var/log/postinstall.log")
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
class UserFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.user = getpass.getuser()
|
||||
return True
|
||||
|
||||
file_handler.addFilter(UserFilter())
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
132
deb_dist/postinstall-1.5.0/postinstall/option/add_user.py
Normal file
132
deb_dist/postinstall-1.5.0/postinstall/option/add_user.py
Normal 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()
|
||||
59
deb_dist/postinstall-1.5.0/postinstall/option/autostart.py
Normal file
59
deb_dist/postinstall-1.5.0/postinstall/option/autostart.py
Normal 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()
|
||||
157
deb_dist/postinstall-1.5.0/postinstall/option/database.py
Normal file
157
deb_dist/postinstall-1.5.0/postinstall/option/database.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
|
||||
|
||||
def main():
|
||||
dialog = Dialog(dialog="dialog")
|
||||
|
||||
# Диалоговое окно с выбором базы данных
|
||||
code, choice = dialog.menu(
|
||||
"Выберите базу данных для установки:",
|
||||
choices=[
|
||||
("PostgreSQL", "Установить PostgreSQL"),
|
||||
("MariaDB", "Установить MariaDB"),
|
||||
],
|
||||
)
|
||||
|
||||
if code == dialog.OK:
|
||||
dialog.msgbox(f"Вы выбрали: {choice}", title="Выбор подтверждён")
|
||||
if choice == "PostgreSQL":
|
||||
install_postgresql(dialog)
|
||||
elif choice == "MariaDB":
|
||||
install_mariadb(dialog)
|
||||
|
||||
# Независимо от выбранной базы данных, предложить установку Webmin
|
||||
if (
|
||||
dialog.yesno("Установить Webmin?", yes_label="Да", no_label="Нет")
|
||||
== dialog.OK
|
||||
):
|
||||
install_webmin(dialog)
|
||||
else:
|
||||
dialog.msgbox("Выбор базы данных отменён", title="Отмена")
|
||||
|
||||
|
||||
def is_installed(package_name):
|
||||
"""Проверяет, установлен ли пакет"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["dpkg-query", "-W", "-f=${Status}", package_name],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return "install ok installed" in result.stdout
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, title):
|
||||
"""Запуск команды с выводом лога в диалоговое окно"""
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
log_output = []
|
||||
for line in iter(process.stdout.readline, ""):
|
||||
log_output.append(line.strip())
|
||||
|
||||
stderr_output = process.stderr.read().strip()
|
||||
if stderr_output:
|
||||
log_output.append(stderr_output)
|
||||
|
||||
process.stdout.close()
|
||||
process.stderr.close()
|
||||
process.wait()
|
||||
|
||||
dialog.msgbox("\n".join(log_output), title=title, height=20, width=60)
|
||||
return process.returncode == 0
|
||||
|
||||
|
||||
def install_package(dialog, package_name):
|
||||
"""Устанавливает указанный пакет с выводом лога"""
|
||||
dialog.infobox(f"Начало установки {package_name}...")
|
||||
|
||||
if not run_command_with_log(dialog, ["apt-get", "update"], "Обновление пакетов"):
|
||||
dialog.msgbox(f"Ошибка при обновлении пакетов для {package_name}.")
|
||||
return
|
||||
|
||||
if not run_command_with_log(
|
||||
dialog, ["apt-get", "install", "-y", package_name], f"Установка {package_name}"
|
||||
):
|
||||
dialog.msgbox(f"Ошибка при установке {package_name}.")
|
||||
return
|
||||
|
||||
dialog.msgbox(f"{package_name} успешно установлен.")
|
||||
|
||||
|
||||
def install_postgresql(dialog):
|
||||
"""Установка PostgreSQL и Harlequin"""
|
||||
if not is_installed("postgresql-16"):
|
||||
install_package(dialog, "postgresql-16")
|
||||
else:
|
||||
dialog.msgbox("PostgreSQL уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
if not is_installed("python3-harlequin-postgres"):
|
||||
install_package(dialog, "python3-harlequin-postgres")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin-postgres уже установлен.")
|
||||
|
||||
|
||||
def install_mariadb(dialog):
|
||||
"""Установка MariaDB"""
|
||||
if not is_installed("mariadb-server"):
|
||||
install_package(dialog, "mariadb-server")
|
||||
else:
|
||||
dialog.msgbox("MariaDB уже установлена.")
|
||||
|
||||
if not is_installed("python3-harlequin"):
|
||||
install_package(dialog, "python3-harlequin")
|
||||
else:
|
||||
dialog.msgbox("python3-harlequin уже установлен.")
|
||||
|
||||
|
||||
def install_webmin(dialog):
|
||||
"""Установка Webmin и дополнительных пакетов"""
|
||||
if not is_installed("webmin"):
|
||||
install_package(dialog, "webmin")
|
||||
|
||||
# Установка cpanminus
|
||||
install_package(dialog, "postgresql-server-dev-16")
|
||||
install_package(dialog, "cpanminus")
|
||||
|
||||
run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "webmin"], "Настройка Firewall"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
dialog, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
dialog.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# Установка модулей DBI и DBD::Pg через cpanminus
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBI"], "Установка модуля DBI"
|
||||
):
|
||||
dialog.msgbox("Модуль DBI успешно установлен.")
|
||||
if run_command_with_log(
|
||||
dialog, ["cpanm", "--quit", "DBD::Pg"], "Установка модуля DBD::Pg"
|
||||
):
|
||||
dialog.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
dialog, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
dialog.msgbox("Webmin успешно перезагружен.")
|
||||
else:
|
||||
dialog.msgbox("Webmin уже установлен.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
533
deb_dist/postinstall-1.5.0/postinstall/option/docker.py
Normal file
533
deb_dist/postinstall-1.5.0/postinstall/option/docker.py
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Callable
|
||||
|
||||
from dialog import Dialog
|
||||
from postinstall.logger import logger
|
||||
|
||||
import socket
|
||||
|
||||
REGISTRY_URL = "dr.gosrv.ru"
|
||||
REGISTRY_LOGIN = "anon_user"
|
||||
REGISTRY_PASSWORD = "Qweasdzxc123C@"
|
||||
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
|
||||
|
||||
|
||||
class CommandExecutor:
|
||||
@staticmethod
|
||||
def run_command(
|
||||
command: str,
|
||||
shell: bool = False,
|
||||
input_data: Optional[str] = None,
|
||||
ignore_errors: List[int] = [],
|
||||
) -> subprocess.CompletedProcess:
|
||||
try:
|
||||
return subprocess.run(
|
||||
command if shell else shlex.split(command),
|
||||
shell=shell,
|
||||
check=True,
|
||||
input=input_data,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode not in ignore_errors:
|
||||
logger.error(f"Ошибка выполнения команды: {e}")
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}"
|
||||
)
|
||||
return e
|
||||
|
||||
|
||||
class SystemChecker:
|
||||
@staticmethod
|
||||
def is_command_installed(command: str) -> bool:
|
||||
try:
|
||||
return CommandExecutor.run_command(f"which {command}").returncode == 0
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
class ProgressGauge:
|
||||
def __init__(self, dialog: Dialog, title: str, total_steps: int):
|
||||
self.dialog = dialog
|
||||
self.title = title
|
||||
self.total_steps = total_steps
|
||||
self.current_step = 0
|
||||
|
||||
def update(self, step: int, message: str):
|
||||
self.current_step = step
|
||||
percent = int((step / self.total_steps) * 100)
|
||||
self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True)
|
||||
|
||||
def __enter__(self):
|
||||
self.dialog.gauge_start(self.title, percent=0)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.dialog.gauge_stop()
|
||||
|
||||
|
||||
class DockerManager:
|
||||
@staticmethod
|
||||
def install(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Установка Docker...")
|
||||
progress_callback(1, "Обновление пакетов")
|
||||
CommandExecutor.run_command("apt-get update")
|
||||
progress_callback(2, "Установка Docker")
|
||||
CommandExecutor.run_command("apt-get install -y docker.io")
|
||||
progress_callback(3, "Установка Docker Compose")
|
||||
CommandExecutor.run_command("apt-get install -y docker-compose")
|
||||
progress_callback(4, "Выполнение входа в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(5, "Docker успешно установлен")
|
||||
logger.info("Docker успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Docker...")
|
||||
progress_callback(1, "Удаление Docker и Docker Compose")
|
||||
CommandExecutor.run_command("apt-get remove -y docker.io docker-compose")
|
||||
progress_callback(2, "Очистка зависимостей")
|
||||
CommandExecutor.run_command("apt-get autoremove -y")
|
||||
progress_callback(3, "Удаление файлов Docker")
|
||||
CommandExecutor.run_command("rm -rf /var/run/docker")
|
||||
CommandExecutor.run_command("rm -rf /var/lib/docker")
|
||||
progress_callback(4, "Docker успешно удален")
|
||||
logger.info("Docker успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def login_to_registry():
|
||||
logger.info("Выполняется вход в Docker registry...")
|
||||
try:
|
||||
result = CommandExecutor.run_command(
|
||||
f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}",
|
||||
shell=True,
|
||||
)
|
||||
logger.info("Успешный вход в Docker registry")
|
||||
logger.debug(f"Docker login output: {result.stdout}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка входа в Docker registry: {e}")
|
||||
logger.error(f"Stderr: {e.stderr}")
|
||||
raise
|
||||
|
||||
|
||||
class UFWManager:
|
||||
@staticmethod
|
||||
def add_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Добавление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_rule(port: str):
|
||||
rule_name = "portainer"
|
||||
logger.info(
|
||||
f"Удаление именованного правила UFW '{rule_name}' для порта {port}..."
|
||||
)
|
||||
CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp")
|
||||
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
|
||||
logger.info(
|
||||
f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено"
|
||||
)
|
||||
|
||||
|
||||
class PortainerManager:
|
||||
@staticmethod
|
||||
def install(
|
||||
port: str, password: str, ip: str, progress_callback: Callable[[int, str], None]
|
||||
):
|
||||
logger.info("Установка Portainer...")
|
||||
progress_callback(1, "Вход в Docker registry")
|
||||
DockerManager.login_to_registry()
|
||||
progress_callback(2, "Остановка предыдущей версии Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(3, "Удаление старых данных Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
"docker volume ls -q -f name=portainer* | xargs -r docker volume rm",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
progress_callback(4, "Установка apache2-utils для htpasswd")
|
||||
if not SystemChecker.is_command_installed("htpasswd"):
|
||||
CommandExecutor.run_command("apt-get install -y apache2-utils")
|
||||
|
||||
progress_callback(5, "Создание конфигурации Portainer")
|
||||
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
|
||||
password_hash = PortainerManager._generate_password_hash(password)
|
||||
PortainerManager._create_docker_compose_file(port, password_hash, ip)
|
||||
|
||||
progress_callback(6, "Запуск Portainer")
|
||||
CommandExecutor.run_command(
|
||||
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d"
|
||||
)
|
||||
|
||||
# if ip != "127.0.0.1":
|
||||
if True:
|
||||
progress_callback(7, "Добавление правила UFW")
|
||||
UFWManager.add_rule(port)
|
||||
|
||||
progress_callback(8, "Portainer успешно установлен")
|
||||
logger.info("Portainer успешно установлен")
|
||||
|
||||
@staticmethod
|
||||
def uninstall(progress_callback: Callable[[int, str], None]):
|
||||
logger.info("Удаление Portainer...")
|
||||
progress_callback(1, "Остановка Portainer")
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes",
|
||||
ignore_errors=[1],
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
progress_callback(2, "Удаление файлов конфигурации")
|
||||
CommandExecutor.run_command(
|
||||
f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"
|
||||
)
|
||||
# try:
|
||||
# with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f:
|
||||
# config = f.read()
|
||||
# port_ip_match = re.search(r'"(.+):(\d+):9000"', config)
|
||||
# if port_ip_match:
|
||||
# ip, port = port_ip_match.groups()
|
||||
# # if True:
|
||||
# # progress_callback(3, "Удаление правила UFW")
|
||||
# # UFWManager.remove_rule(port)
|
||||
# except FileNotFoundError:
|
||||
# logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.")
|
||||
|
||||
progress_callback(4, "Portainer успешно удален")
|
||||
logger.info("Portainer успешно удален")
|
||||
|
||||
@staticmethod
|
||||
def _generate_password_hash(password: str) -> str:
|
||||
return CommandExecutor.run_command(
|
||||
f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'",
|
||||
shell=True,
|
||||
).stdout.strip()
|
||||
|
||||
@staticmethod
|
||||
def _create_docker_compose_file(port: str, password_hash: str, ip: str):
|
||||
docker_compose_content = f"""
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: {REGISTRY_URL}/base_images/portainer:latest
|
||||
ports:
|
||||
- "{ip}:{port}:9000"
|
||||
command: --admin-password '{password_hash}'
|
||||
networks:
|
||||
- local
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- portainer-data:/data
|
||||
environment:
|
||||
- PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1
|
||||
deploy:
|
||||
mode: global
|
||||
|
||||
networks:
|
||||
local:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
portainer-data:
|
||||
"""
|
||||
with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f:
|
||||
f.write(docker_compose_content)
|
||||
|
||||
|
||||
class InputValidator:
|
||||
@staticmethod
|
||||
def validate(value: str, pattern: str) -> bool:
|
||||
return bool(re.match(pattern, value))
|
||||
|
||||
|
||||
class UIElement(ABC):
|
||||
def __init__(self, dialog: Dialog):
|
||||
self.dialog = dialog
|
||||
|
||||
@abstractmethod
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
|
||||
class Menu(UIElement):
|
||||
def show(self):
|
||||
choices = []
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
choices.append(("1", "Установить Docker"))
|
||||
else:
|
||||
choices.append(("1", "Удалить Docker"))
|
||||
|
||||
if SystemChecker.is_command_installed("docker"):
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
choices.append(("2", "Установить Portainer"))
|
||||
else:
|
||||
choices.append(("2", "Удалить Portainer"))
|
||||
|
||||
code, choice = self.dialog.menu("Выберите действие:", choices=choices)
|
||||
return code, choice
|
||||
|
||||
|
||||
class YesNoUI(UIElement):
|
||||
def show(self, text: str):
|
||||
return self.dialog.yesno(text) == self.dialog.OK
|
||||
|
||||
|
||||
class PasswordInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, password = self.dialog.passwordbox(
|
||||
"Введите пароль для Portainer:", insecure=True
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(password, r"^.{12,}$"):
|
||||
return password
|
||||
self.dialog.msgbox(
|
||||
"Пароль должен содержать минимум 12 символов.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class PortInput(UIElement):
|
||||
def show(self):
|
||||
while True:
|
||||
code, port = self.dialog.inputbox(
|
||||
"Введите порт для Portainer:", init="9000"
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"):
|
||||
return port
|
||||
self.dialog.msgbox(
|
||||
"Некорректный порт. Введите число от 1 до 65535.", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
class IPInput(UIElement):
|
||||
def get_default_interface_ip(self):
|
||||
try:
|
||||
# Выполнение команды для получения IP-адреса основного интерфейса
|
||||
command = "ip -o -4 addr show $(ip route | grep default | awk '{print $5}') | awk '{print $4}' | cut -d/ -f1"
|
||||
result = subprocess.check_output(command, shell=True, text=True)
|
||||
return result.strip() # Возвращаем IP-адрес без лишних символов
|
||||
except subprocess.CalledProcessError:
|
||||
return "127.0.0.1" # В случае ошибки вернём стандартный IP
|
||||
|
||||
def show(self):
|
||||
# Получаем IP-адрес основного сетевого интерфейса
|
||||
default_ip = self.get_default_interface_ip()
|
||||
|
||||
while True:
|
||||
# Используем полученный IP-адрес как начальное значение
|
||||
code, ip = self.dialog.inputbox(
|
||||
"Введите IP адрес для Portainer:", init=default_ip
|
||||
)
|
||||
if code == self.dialog.CANCEL:
|
||||
return None
|
||||
if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"):
|
||||
return ip
|
||||
self.dialog.msgbox(
|
||||
"Некорректный IP адрес. Введите адрес в формате X.X.X.X",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
|
||||
|
||||
class NetworkInterfaceInput(UIElement):
|
||||
def get_network_interfaces(self):
|
||||
try:
|
||||
# Get all network interfaces
|
||||
interfaces = subprocess.check_output(
|
||||
"ip -o link show", shell=True, text=True
|
||||
)
|
||||
# Extract interface names and their IP addresses
|
||||
interface_list = []
|
||||
for line in interfaces.split("\n"):
|
||||
if line:
|
||||
interface = line.split(":")[1].strip()
|
||||
ip = self.get_ip_address(interface)
|
||||
if ip:
|
||||
interface_list.append((interface, ip))
|
||||
return interface_list
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
def get_ip_address(self, interface):
|
||||
try:
|
||||
ip = subprocess.check_output(
|
||||
f"ip -o -4 addr list {interface}", shell=True, text=True
|
||||
)
|
||||
return ip.split()[3].split("/")[0]
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def show(self):
|
||||
interfaces = self.get_network_interfaces()
|
||||
if not interfaces:
|
||||
self.dialog.msgbox(
|
||||
"Не удалось получить список сетевых интерфейсов.", width=50, height=10
|
||||
)
|
||||
return None
|
||||
|
||||
choices = [(f"{interface} ({ip})", interface) for interface, ip in interfaces]
|
||||
code, choice = self.dialog.menu(
|
||||
"Выберите сетевой интерфейс для Docker Swarm:", choices=choices
|
||||
)
|
||||
|
||||
if code == self.dialog.OK:
|
||||
return choice
|
||||
return None
|
||||
|
||||
|
||||
class DockerConfigUI:
|
||||
def __init__(self):
|
||||
self.dialog = Dialog(dialog="dialog")
|
||||
self.menu = Menu(self.dialog)
|
||||
self.password_input = PasswordInput(self.dialog)
|
||||
self.port_input = PortInput(self.dialog)
|
||||
self.ip_input = IPInput(self.dialog)
|
||||
self.network_interface_input = NetworkInterfaceInput(self.dialog)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
code, choice = self.menu.show()
|
||||
if code == self.dialog.OK:
|
||||
if choice == "1":
|
||||
self._handle_docker()
|
||||
elif choice == "2":
|
||||
self.handle_portainer()
|
||||
elif choice == "3":
|
||||
break
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Произошла ошибка: {e}")
|
||||
self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10)
|
||||
|
||||
def _handle_docker(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge:
|
||||
DockerManager.install(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно установлен.", width=50, height=10)
|
||||
logger.info("Docker успешно установлен.")
|
||||
yes_no_ui = YesNoUI(self.dialog)
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"
|
||||
):
|
||||
interface = self.network_interface_input.show()
|
||||
if interface:
|
||||
ip = interface.split("(")[1].split(")")[0]
|
||||
if ip:
|
||||
try:
|
||||
CommandExecutor.run_command(
|
||||
f"docker swarm init --advertise-addr {ip}"
|
||||
)
|
||||
self.dialog.msgbox(
|
||||
f"Docker Swarm успешно инициализирован на интерфейсе {interface}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при инициализации Docker Swarm: {str(e)}",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
f"Не удалось получить IP-адрес для интерфейса {interface}.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
else:
|
||||
self.dialog.msgbox(
|
||||
"Инициализация Docker Swarm отменена.", width=50, height=10
|
||||
)
|
||||
|
||||
if yes_no_ui.show(
|
||||
text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?"
|
||||
):
|
||||
self.handle_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
DockerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Docker успешно удален.", width=50, height=10)
|
||||
logger.info("Docker успешно удален.")
|
||||
|
||||
def handle_portainer(self):
|
||||
if not SystemChecker.is_command_installed("docker"):
|
||||
self.dialog.msgbox(
|
||||
"Docker не установлен. Пожалуйста, сначала установите Docker.",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
return
|
||||
|
||||
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
|
||||
self._install_portainer()
|
||||
else:
|
||||
with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge:
|
||||
PortainerManager.uninstall(gauge.update)
|
||||
self.dialog.msgbox("Portainer успешно удален.", width=50, height=10)
|
||||
logger.info("Portainer успешно удален.")
|
||||
|
||||
def _install_portainer(self):
|
||||
password = self.password_input.show()
|
||||
if not password:
|
||||
return
|
||||
|
||||
port = self.port_input.show()
|
||||
if not port:
|
||||
return
|
||||
|
||||
ip = self.ip_input.show()
|
||||
if not ip:
|
||||
return
|
||||
|
||||
try:
|
||||
with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge:
|
||||
PortainerManager.install(port, password, ip, gauge.update)
|
||||
self.dialog.msgbox(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль",
|
||||
width=50,
|
||||
height=10,
|
||||
)
|
||||
logger.info(
|
||||
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль"
|
||||
)
|
||||
except Exception as e:
|
||||
self.dialog.msgbox(
|
||||
f"Ошибка при установке Portainer: {str(e)}", width=50, height=10
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
ui = DockerConfigUI()
|
||||
ui.run()
|
||||
76
deb_dist/postinstall-1.5.0/postinstall/option/hostname.py
Normal file
76
deb_dist/postinstall-1.5.0/postinstall/option/hostname.py
Normal 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("Изменение доменного имени отменено.")
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
AVAILABLE_PACKAGES = [
|
||||
"neofetch",
|
||||
"webmin",
|
||||
"usermin",
|
||||
"cups",
|
||||
"wireguard",
|
||||
"wg-cmd",
|
||||
]
|
||||
|
||||
|
||||
def check_sudo():
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def run_command_with_log(dialog, command, message):
|
||||
"""Run a command and log output, showing a message box on success."""
|
||||
try:
|
||||
subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
logging.info(f"{message}: успешно выполнено.")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"{message}: ошибка - {e}")
|
||||
dialog.msgbox(f"{message}: ошибка.\n{e}")
|
||||
return False
|
||||
|
||||
|
||||
def install_packages():
|
||||
d = Dialog(dialog="dialog")
|
||||
|
||||
if not check_sudo():
|
||||
d.msgbox("Для установки пакетов необходимы права суперпользователя.")
|
||||
return
|
||||
|
||||
# Создаем список пакетов для выбора
|
||||
choices = [(package, "", False) for package in AVAILABLE_PACKAGES]
|
||||
|
||||
# Показываем checklist для выбора пакетов
|
||||
code, tags = d.checklist(
|
||||
"Выберите пакеты для установки:", choices=choices, title="Установка программ"
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
if not tags:
|
||||
d.msgbox("Вы не выбрали ни одного пакета для установки.")
|
||||
return
|
||||
|
||||
# Подтверждение установки
|
||||
code = d.yesno(
|
||||
f"Вы уверены, что хотите установить следующие пакеты:\n{', '.join(tags)}?"
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
# Создаем gauge для отображения прогресса
|
||||
d.gauge_start("Установка пакетов...", percent=0)
|
||||
|
||||
try:
|
||||
# Обновление списка пакетов
|
||||
d.gauge_update(10, "Обновление списка пакетов...")
|
||||
subprocess.run(["apt", "update"], check=True, capture_output=True)
|
||||
|
||||
logging.info(f"Установка пакетов: {', '.join(tags)}")
|
||||
# Установка выбранных пакетов
|
||||
d.gauge_update(20, f"Установка пакетов: {', '.join(tags)}...")
|
||||
process = subprocess.Popen(
|
||||
["apt", "install", "-y"] + tags,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
# Чтение вывода процесса и обновление gauge
|
||||
for i, line in enumerate(process.stdout):
|
||||
d.gauge_update(20 + (i % 70), line.strip())
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
error = process.stderr.read()
|
||||
raise subprocess.CalledProcessError(process.returncode, process.args, error)
|
||||
|
||||
# Проверяем, если вебмин был выбран для установки
|
||||
if "webmin" in tags:
|
||||
d.gauge_update(90, "Настройка Webmin...")
|
||||
# Установка cpanminus
|
||||
run_command_with_log(
|
||||
d, ["apt", "install", "-y", "cpanminus"], "Установка cpanminus"
|
||||
)
|
||||
|
||||
# Разрешение порта 10000/tcp в ufw
|
||||
if run_command_with_log(
|
||||
d, ["sudo", "ufw", "allow", "10000/tcp"], "Настройка Firewall"
|
||||
):
|
||||
d.msgbox("Порт 10000/tcp разрешен в ufw.")
|
||||
|
||||
# # Установка модулей DBI и DBD::Pg через cpanminus
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBI'], "Установка модуля DBI"):
|
||||
# d.msgbox("Модуль DBI успешно установлен.")
|
||||
# if run_command_with_log(d, ['cpanm', '--quit', 'DBD::Pg'], "Установка модуля DBD::Pg"):
|
||||
# d.msgbox("Модуль DBD::Pg успешно установлен.")
|
||||
|
||||
# Перезагрузка Webmin
|
||||
if run_command_with_log(
|
||||
d, ["systemctl", "reload", "webmin"], "Перезагрузка Webmin"
|
||||
):
|
||||
d.msgbox("Webmin успешно перезагружен.")
|
||||
|
||||
d.gauge_update(100, "Установка завершена успешно!")
|
||||
d.gauge_stop()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при установке пакетов: {e}")
|
||||
d.gauge_stop()
|
||||
d.msgbox(f"Ошибка при установке пакетов: {e}")
|
||||
|
||||
|
||||
def run_install_packages():
|
||||
install_packages()
|
||||
243
deb_dist/postinstall-1.5.0/postinstall/option/network.py
Normal file
243
deb_dist/postinstall-1.5.0/postinstall/option/network.py
Normal 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()
|
||||
106
deb_dist/postinstall-1.5.0/postinstall/option/shell.py
Normal file
106
deb_dist/postinstall-1.5.0/postinstall/option/shell.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import pwd
|
||||
import os
|
||||
from postinstall.logger import logger
|
||||
|
||||
logging = logger
|
||||
|
||||
|
||||
def get_users():
|
||||
users = []
|
||||
try:
|
||||
for user in pwd.getpwall():
|
||||
if user.pw_uid >= 1000 and user.pw_name != "nobody":
|
||||
users.append(user.pw_name)
|
||||
if "root" not in users:
|
||||
users.append("root")
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении списка пользователей: {e}")
|
||||
return users
|
||||
|
||||
|
||||
def install_zsh():
|
||||
try:
|
||||
subprocess.run(["apt", "update"], check=True)
|
||||
subprocess.run(["apt", "install", "-y", "zsh"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Ошибка при установке zsh: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def change_shell():
|
||||
d = Dialog(dialog="dialog")
|
||||
users = get_users()
|
||||
|
||||
# Первый шаг - выбор опции с помощью radiolist
|
||||
code, choice = d.radiolist(
|
||||
"Выберите способ изменения оболочки:",
|
||||
choices=[
|
||||
("Для всех пользователей", "", False),
|
||||
("Выбрать конкретных пользователей", "", True)
|
||||
]
|
||||
)
|
||||
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
selected_users = []
|
||||
|
||||
if choice == "Для всех пользователей":
|
||||
# Если выбрано "Для всех пользователей"
|
||||
selected_users = users
|
||||
elif choice == "Выбрать конкретных пользователей":
|
||||
# Если выбран выбор конкретных пользователей, показываем checklist
|
||||
code, selected_users = d.checklist(
|
||||
"Выберите пользователей для изменения оболочки:",
|
||||
choices=[(user, "", False) for user in users],
|
||||
clear=True
|
||||
)
|
||||
if code != d.OK or not selected_users:
|
||||
return
|
||||
|
||||
# Выбор оболочки
|
||||
code, shell = d.menu(
|
||||
"Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")]
|
||||
)
|
||||
if code != d.OK:
|
||||
return
|
||||
|
||||
shell_path = "/bin/bash" if shell == "1" else "/bin/zsh"
|
||||
|
||||
if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"):
|
||||
logging.info("zsh не установлен. Начинаем установку...")
|
||||
d.msgbox("zsh не установлен. Начинаем установку...")
|
||||
if not install_zsh():
|
||||
d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.")
|
||||
return
|
||||
|
||||
# Применение изменений
|
||||
if choice == "Для всех пользователей":
|
||||
# Применение изменений для всех пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
d.msgbox("Оболочка изменена для всех существующих пользователей")
|
||||
logging.info("Оболочка изменена для всех существующих пользователей")
|
||||
else:
|
||||
# Применение изменений для конкретных пользователей
|
||||
for user in selected_users:
|
||||
try:
|
||||
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
|
||||
d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
|
||||
|
||||
|
||||
def run_change_shell():
|
||||
change_shell()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_change_shell()
|
||||
236
deb_dist/postinstall-1.5.0/postinstall/option/ssh.py
Normal file
236
deb_dist/postinstall-1.5.0/postinstall/option/ssh.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import subprocess
|
||||
from dialog import Dialog
|
||||
import logging
|
||||
import os
|
||||
|
||||
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||
SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf"
|
||||
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
|
||||
|
||||
|
||||
class SSHConfig:
|
||||
def __init__(self):
|
||||
self.d = Dialog(dialog="dialog")
|
||||
self.current_port = self.get_current_ssh_port()
|
||||
self.current_auth_method = self.get_current_auth_method()
|
||||
self.new_port = None
|
||||
self.new_auth_method = None
|
||||
|
||||
def get_sshd_status(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", "sshd"], capture_output=True, text=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return "unknown"
|
||||
|
||||
def toggle_sshd_service(self, action):
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", action, "sshd"], check=True)
|
||||
self.d.msgbox(
|
||||
f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}."
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при {action} службы sshd: {e}")
|
||||
self.d.msgbox(
|
||||
f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd."
|
||||
)
|
||||
|
||||
def get_current_ssh_port(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["grep", "^#*Port", SSH_CONFIG_PATH],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
port = result.stdout.strip().split()[-1]
|
||||
return port if port.isdigit() else "22"
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning(
|
||||
"Не удалось получить порт SSH, используется порт по умолчанию"
|
||||
)
|
||||
return "22"
|
||||
|
||||
def change_ssh_port(self):
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
code, new_port = self.d.inputbox(
|
||||
f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:",
|
||||
init=port_display,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
try:
|
||||
new_port = int(new_port)
|
||||
if new_port < 1 or new_port > 65535:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.")
|
||||
return
|
||||
code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?")
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_port = str(new_port)
|
||||
|
||||
def get_current_auth_method(self):
|
||||
try:
|
||||
with open(SSH_AUTH_CONFIG_PATH, "r") as f:
|
||||
content = f.read()
|
||||
password_auth = "yes" if "PasswordAuthentication yes" in content else "no"
|
||||
pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no"
|
||||
|
||||
if password_auth == "yes" and pubkey_auth == "yes":
|
||||
return "По ключу и паролю"
|
||||
elif password_auth == "no" and pubkey_auth == "yes":
|
||||
return "Только по ключу"
|
||||
elif password_auth == "yes" and pubkey_auth == "no":
|
||||
return "Только по паролю"
|
||||
else:
|
||||
return "По ключу и паролю" # Default value
|
||||
except FileNotFoundError:
|
||||
logging.warning("Файл конфигурации аутентификации SSH не найден")
|
||||
return "По ключу и паролю" # Default value
|
||||
except Exception as e:
|
||||
logging.warning(f"Не удалось получить метод аутентификации SSH: {e}")
|
||||
return "По ключу и паролю" # Default value
|
||||
|
||||
def change_auth_method(self):
|
||||
auth_method_display = (
|
||||
self.new_auth_method if self.new_auth_method else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
auth_choices = [
|
||||
(
|
||||
"1",
|
||||
f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}",
|
||||
),
|
||||
(
|
||||
"2",
|
||||
f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}",
|
||||
),
|
||||
(
|
||||
"3",
|
||||
f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}",
|
||||
),
|
||||
]
|
||||
code, tag = self.d.menu(
|
||||
f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:",
|
||||
choices=auth_choices,
|
||||
)
|
||||
if code != self.d.OK:
|
||||
return
|
||||
self.new_auth_method = {
|
||||
"1": "Только по паролю",
|
||||
"2": "Только по ключу",
|
||||
"3": "По ключу и паролю",
|
||||
}[tag]
|
||||
|
||||
def apply_changes(self):
|
||||
if self.new_port:
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^#*Port .*/Port {self.new_port}/",
|
||||
SSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"sudo",
|
||||
"sed",
|
||||
"-i",
|
||||
f"s/^ports=.*/ports={self.new_port}\/tcp/",
|
||||
UFW_OPENSSH_CONFIG_PATH,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
|
||||
subprocess.run(
|
||||
["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True
|
||||
)
|
||||
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
|
||||
self.current_port = self.new_port
|
||||
self.new_port = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Ошибка при изменении порта SSH: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении порта SSH: {e}")
|
||||
|
||||
if self.new_auth_method:
|
||||
try:
|
||||
auth_config = ""
|
||||
if self.new_auth_method == "Только по паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication no\n"
|
||||
)
|
||||
elif self.new_auth_method == "Только по ключу":
|
||||
auth_config = (
|
||||
"PasswordAuthentication no\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
elif self.new_auth_method == "По ключу и паролю":
|
||||
auth_config = (
|
||||
"PasswordAuthentication yes\nPubkeyAuthentication yes\n"
|
||||
)
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True)
|
||||
with open(SSH_AUTH_CONFIG_PATH, "w") as f:
|
||||
f.write(auth_config)
|
||||
|
||||
self.current_auth_method = self.new_auth_method
|
||||
self.new_auth_method = None
|
||||
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
|
||||
self.d.msgbox("Метод аутентификации SSH успешно изменен.")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}")
|
||||
|
||||
def main_menu(self):
|
||||
while True:
|
||||
status = self.get_sshd_status()
|
||||
port_display = self.new_port if self.new_port else self.current_port
|
||||
port_status = " (не применено)" if self.new_port else ""
|
||||
auth_method_display = (
|
||||
self.new_auth_method
|
||||
if self.new_auth_method
|
||||
else self.current_auth_method
|
||||
)
|
||||
auth_status = " (не применено)" if self.new_auth_method else ""
|
||||
|
||||
code, tag = self.d.menu(
|
||||
f"Статус sshd: {status}\n"
|
||||
f"Текущий порт: {port_display}{port_status}\n"
|
||||
f"Метод аутентификации: {auth_method_display}{auth_status}",
|
||||
choices=[
|
||||
("1", "Включить sshd" if status != "active" else "Выключить sshd"),
|
||||
("2", "Изменить порт SSH"),
|
||||
("3", "Изменить метод аутентификации"),
|
||||
],
|
||||
)
|
||||
if code != self.d.OK:
|
||||
break
|
||||
if tag == "1":
|
||||
self.toggle_sshd_service("start" if status != "active" else "stop")
|
||||
elif tag == "2":
|
||||
self.change_ssh_port()
|
||||
elif tag == "3":
|
||||
self.change_auth_method()
|
||||
|
||||
return self.new_port is not None or self.new_auth_method is not None
|
||||
|
||||
|
||||
def run_ssh_config():
|
||||
ssh_config = SSHConfig()
|
||||
changes_made = ssh_config.main_menu()
|
||||
return ssh_config, changes_made
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ssh_config, changes_made = run_ssh_config()
|
||||
# Note: Changes will be applied in the main menu when "Save and exit" is selected
|
||||
57
deb_dist/postinstall-1.5.0/postinstall/option/update.py
Normal file
57
deb_dist/postinstall-1.5.0/postinstall/option/update.py
Normal 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()
|
||||
11
deb_dist/postinstall-1.5.0/postinstall/root.py
Normal file
11
deb_dist/postinstall-1.5.0/postinstall/root.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def check_root():
|
||||
if os.geteuid() != 0:
|
||||
print("Запустите скрипт с правами суперпользователя")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
check_root()
|
||||
19
deb_dist/postinstall-1.5.0/pyproject.toml
Normal file
19
deb_dist/postinstall-1.5.0/pyproject.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[tool.poetry]
|
||||
name = "postinstall"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
pythondialog = "^3.5.3"
|
||||
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
postinstall = "postinstall:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
4
deb_dist/postinstall-1.5.0/setup.cfg
Normal file
4
deb_dist/postinstall-1.5.0/setup.cfg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
|
||||
45
deb_dist/postinstall-1.5.0/setup.py
Normal file
45
deb_dist/postinstall-1.5.0/setup.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
import shutil
|
||||
from setuptools import setup, find_packages
|
||||
import glob
|
||||
|
||||
deb_dist_dir = "deb_dist"
|
||||
if os.path.exists(deb_dist_dir):
|
||||
old_versions = glob.glob(os.path.join(deb_dist_dir, "postinstall-0.3.*"))
|
||||
for old_version in old_versions:
|
||||
if os.path.isdir(old_version):
|
||||
shutil.rmtree(old_version)
|
||||
else:
|
||||
os.remove(old_version)
|
||||
|
||||
setup(
|
||||
name="postinstall",
|
||||
version="1.5.0",
|
||||
description="A Dialog TUI application for postinstall system configuration",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="LLC Technologies and Communication",
|
||||
author_email="devteam@texnico.ru",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
package_data={
|
||||
# 'postinstall': ['styles.tcss'],
|
||||
},
|
||||
install_requires=["pythondialog>=3.5.3,<4.0.0"],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"postinstall=postinstall:main",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent :: Linux",
|
||||
],
|
||||
python_requires=">=3.11",
|
||||
data_files=[
|
||||
("/usr/share/postinstall", []),
|
||||
("/etc/systemd/system", ["deb/postinstall.service"]),
|
||||
("/etc/profile.d", ["deb/0002-postinstall.sh"]),
|
||||
],
|
||||
)
|
||||
9
deb_dist/postinstall-1.5.0/tests/__init__.py
Normal file
9
deb_dist/postinstall-1.5.0/tests/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import subprocess
|
||||
|
||||
|
||||
def get_network_interfaces():
|
||||
interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n")
|
||||
return interfaces[:-1]
|
||||
|
||||
|
||||
get_network_interfaces()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue