Старый postinstall

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

View file

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

View file

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

View file

@ -0,0 +1,25 @@
import logging
import getpass
def setup_logger():
logger = logging.getLogger()
logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(asctime)s - User: %(user)s - %(message)s")
file_handler = logging.FileHandler("/var/log/postinstall.log")
file_handler.setFormatter(formatter)
class UserFilter(logging.Filter):
def filter(self, record):
record.user = getpass.getuser()
return True
file_handler.addFilter(UserFilter())
logger.addHandler(file_handler)
return logger
logger = setup_logger()

View file

@ -0,0 +1,132 @@
import subprocess
from dialog import Dialog
import pwd
from postinstall.logger import logger
logging = logger
def get_password_with_toggle(d, title):
"""Функция для ввода пароля с возможностью показать/скрыть пароль"""
password_hidden = True
password = ""
# Фиксированная ширина окна
window_width = 60
while True:
if password_hidden:
# Используем insecure=True для показа скрытого пароля снежинками
code, entered_password = d.passwordbox(
title, insecure=True, init=password, width=window_width,
extra_button=True, extra_label="Показать пароль"
)
else:
# Используем insecure=False для показа пароля открытым текстом
code, entered_password = d.inputbox(
title, init=password, width=window_width,
extra_button=True, extra_label="Скрыть пароль"
)
if code == d.EXTRA:
password_hidden = not password_hidden
password = entered_password # Сохраняем введённый пароль
continue # Перезапускаем окно с новым состоянием
if code == d.CANCEL:
return None
# Обновляем сохранённый пароль
password = entered_password
return password
def add_user():
d = Dialog(dialog="dialog")
fields = [
("Имя пользователя", 1, 1, "", 1, 20, 30, 0),
("Полное имя", 2, 1, "", 2, 20, 30, 0),
("Мобильный телефон", 3, 1, "", 3, 20, 30, 0),
("Городской телефон", 4, 1, "", 4, 20, 30, 0),
("Кабинет", 5, 1, "", 5, 20, 30, 0),
]
code, values = d.form("Введите данные нового пользователя:", fields)
if code != d.OK:
return # Пользователь отменил операцию, выходим из функции
username, fullname, mobile, city_phone, cabinet = values
# Проверка существования пользователя
try:
pwd.getpwnam(username)
d.msgbox(f"Пользователь {username} уже существует.")
return
except KeyError:
pass # Пользователь не существует, продолжаем
# Запрос пароля с возможностью его показать/скрыть
password = get_password_with_toggle(d, "Введите пароль для нового пользователя")
if not password:
return # Пользователь отменил операцию, выходим из функции
# Запрос подтверждения пароля с возможностью его показать/скрыть
confirm_password = get_password_with_toggle(d, "Подтвердите пароль")
if not confirm_password:
return # Пользователь отменил операцию, выходим из функции
if password != confirm_password:
d.msgbox("Пароли не совпадают. Попробуйте снова.")
return
# Запрос прав администратора
code = d.yesno("Сделать пользователя администратором?")
is_admin = code == d.OK
try:
# Создание пользователя
subprocess.run(
[
"sudo",
"useradd",
"-m",
"-c",
f"{fullname},{cabinet},{city_phone},{mobile}",
"-s",
"/bin/bash",
username,
],
check=True,
)
# Установка пароля
process = subprocess.Popen(
["sudo", "passwd", username],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
process.communicate(input=f"{password}\n{password}\n".encode())
# Добавление в группу sudo, если пользователь - администратор
if is_admin:
subprocess.run(["sudo", "usermod", "-aG", "sudo", username], check=True)
logging.info(
f"Добавлен пользователь {username}. Данные пользователя: {fullname}, {cabinet}, {city_phone}, {mobile}"
)
d.msgbox(f"Пользователь {username} успешно добавлен.")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при добавлении пользователя: {e}")
d.msgbox(f"Ошибка при добавлении пользователя: {e}")
def run_add_user():
add_user()
if __name__ == "__main__":
run_add_user()

View file

@ -0,0 +1,59 @@
import os
from dialog import Dialog
from postinstall.logger import logger
logging = logger
AUTOSTART_FILE = "/usr/share/postinstall/.norestart"
class AutoStartScreen:
def __init__(self):
self.d = Dialog(dialog="dialog")
def check_auto_start(self) -> bool:
return not os.path.exists(AUTOSTART_FILE)
def set_auto_start(self, enable: bool) -> None:
try:
if enable:
try:
if os.path.exists(AUTOSTART_FILE):
os.remove(AUTOSTART_FILE)
except Exception as e:
logging.error(f"Ошибка при включении автозапуска: {e}")
else:
with open(AUTOSTART_FILE, "w") as f:
f.write("# Это файл создан для отключения автозапуска\n")
except Exception as e:
logging.error(f"Ошибка при изменении автозапуска: {e}")
def show_screen(self):
auto_start_enabled = self.check_auto_start()
code, tags = self.d.checklist(
"Выберите действие",
choices=[
("autostart", "Автозапуск при следующем запуске", auto_start_enabled)
],
title="Настройки автозапуска",
backtitle="Postinstall Configuration",
ok_label="Применить",
cancel_label="Назад",
)
if code == self.d.OK:
self.set_auto_start("autostart" in tags)
self.d.msgbox("Изменения сохранены.")
else:
logging.info("Изменения отменены пользователем.")
def main():
auto_start_screen = AutoStartScreen()
auto_start_screen.show_screen()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,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()

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

View file

@ -0,0 +1,76 @@
import subprocess
from dialog import Dialog
import os
HOSTNAME_FILE = "/etc/hostname"
HOSTS_FILE = "/etc/hosts"
def get_current_hostname():
try:
with open(HOSTNAME_FILE, 'r') as f:
return f.read().strip()
except IOError as e:
print(f"Ошибка при чтении hostname: {e}")
return "localhost"
def change_hostname():
d = Dialog(dialog="dialog")
current_hostname = get_current_hostname()
code, new_hostname = d.inputbox(
f"Текущее доменное имя: {current_hostname}\nВведите новое доменное имя:",
init=current_hostname
)
if code != d.OK:
return None
if new_hostname == current_hostname:
d.msgbox("Введенное доменное имя совпадает с текущим. Изменения не требуются.")
return None
# Проверка валидности hostname
if not is_valid_hostname(new_hostname):
d.msgbox("Недопустимое доменное имя. Используйте только буквы, цифры и дефисы.")
return None
# Подтверждение изменения
code = d.yesno(f"Вы уверены, что хотите изменить доменное имя на {new_hostname}?")
if code != d.OK:
return None
return new_hostname
def is_valid_hostname(hostname):
if len(hostname) > 63:
return False
if hostname[-1] == "-":
return False
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-")
return all(char in allowed for char in hostname)
def apply_hostname_change(new_hostname):
try:
subprocess.run(["sudo", "hostnamectl", "set-hostname", new_hostname], check=True)
if os.path.exists(HOSTS_FILE):
subprocess.run(["sudo", "sed", "-i", f"s/^127.0.1.1.*/127.0.1.1\t{new_hostname}/", HOSTS_FILE], check=True)
return True
except subprocess.CalledProcessError as e:
Dialog(dialog="dialog").msgbox(f"Ошибка при изменении доменного имени: {e}")
return False
def run_change_hostname():
return change_hostname()
if __name__ == "__main__":
new_hostname = run_change_hostname()
if new_hostname:
print(f"Новое доменное имя: {new_hostname}")
else:
print("Изменение доменного имени отменено.")

View file

@ -0,0 +1,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()

View file

@ -0,0 +1,243 @@
import subprocess
import ipaddress
from dialog import Dialog
class NetworkConfig:
def __init__(self):
self.d = Dialog(dialog="dialog")
self.temp_network_settings = {}
def select_interface(self):
interfaces = self.get_network_interfaces()
if not interfaces:
self.d.msgbox("Нет доступных сетевых интерфейсов.")
return None
code, tag = self.d.menu(
"Выберите интерфейс:", choices=[(iface, "") for iface in interfaces]
)
if code == self.d.OK:
self.d.msgbox(f"Выбран интерфейс: {tag}")
self.show_current_settings(tag) # Добавлено окно с текущими настройками
return tag
else:
self.d.msgbox("Интерфейс не выбран.")
return None
@staticmethod
def get_network_interfaces():
try:
result = subprocess.run(
["ls", "/sys/class/net/"], capture_output=True, text=True, check=True
)
interfaces = result.stdout.strip().split("\n")
return interfaces
except subprocess.CalledProcessError:
return []
def show_current_settings(self, interface):
try:
result = subprocess.run(
["ip", "-o", "addr", "show", interface], capture_output=True, text=True, check=True
)
current_settings = result.stdout.strip()
ip_address = None
if "inet " in current_settings:
ip_address = current_settings.split("inet ")[1].split("/")[0]
if "dhcp" in current_settings:
config_type = "DHCP"
elif ip_address:
config_type = "Static"
else:
config_type = "Неизвестно"
message = f"Тип настройки: {config_type}\nIP-адрес: {ip_address}" if ip_address else "IP-адрес не настроен"
self.d.msgbox(f"Текущие настройки интерфейса {interface}:\n\n{message}", height=10, width=50)
except subprocess.CalledProcessError as e:
self.d.msgbox(f"Ошибка получения настроек для интерфейса {interface}: {e}")
def configure_dhcp(self, interface):
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
if code == self.d.OK:
self.temp_network_settings[interface] = {
"type": "dhcp",
"dns": dns,
"interface": interface,
}
self.d.msgbox("DHCP настройка сохранена.")
else:
self.d.msgbox("DHCP настройка отменена.")
def configure_static(self, interface):
code, ip = self.d.inputbox("Введите IP адрес:")
if code != self.d.OK:
return
code, mask = self.d.inputbox("Введите маску подсети:")
if code != self.d.OK:
return
code, gateway = self.d.inputbox("Введите шлюз:")
if code != self.d.OK:
return
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
if ip and mask and gateway:
self.temp_network_settings[interface] = {
"type": "static",
"ip": ip,
"mask": mask,
"gateway": gateway,
"dns": dns,
"interface": interface,
}
self.d.msgbox("Статические настройки сохранены.")
else:
self.d.msgbox("Некоторые параметры не заданы.")
def configure_cidr(self, interface):
code, cidr = self.d.inputbox("Введите CIDR (например, 192.168.1.154/24):")
if code != self.d.OK:
return
code, gateway = self.d.inputbox("Введите шлюз:")
if code != self.d.OK:
return
code, dns = self.d.inputbox("Введите DNS сервер (опционально):")
if cidr:
try:
ip = ipaddress.ip_interface(cidr).ip
network = ipaddress.ip_network(cidr, strict=False)
mask = network.netmask
self.temp_network_settings[interface] = {
"type": "static",
"ip": str(ip),
"mask": str(mask),
"gateway": gateway,
"dns": dns,
"interface": interface,
}
self.d.msgbox("CIDR конфигурация сохранена.")
except ValueError as e:
self.d.msgbox(f"Неверный формат CIDR: {e}")
else:
self.d.msgbox("CIDR не задан.")
def run(self):
while True:
interface = self.select_interface()
if not interface:
break
code, tag = self.d.menu(
"Выберите действие для интерфейса:",
choices=[
("1", "Настроить DHCP"),
("2", "Настроить статический IP"),
("3", "Настроить CIDR"),
("4", "Выбрать другой интерфейс"),
("5", "Выход"),
],
)
if code == self.d.OK:
if tag == "1":
self.configure_dhcp(interface)
elif tag == "2":
self.configure_static(interface)
elif tag == "3":
self.configure_cidr(interface)
elif tag == "4":
continue
elif tag == "5":
break
else:
break
return self.temp_network_settings
def save_and_apply_network_settings(self):
for interface, settings in self.temp_network_settings.items():
try:
if settings["type"] == "dhcp":
self.save_dhcp(interface, settings)
elif settings["type"] == "static":
self.save_static(interface, settings)
if settings.get("dns"):
self.update_resolv_conf(settings["dns"])
except Exception as e:
self.d.msgbox(f"Ошибка применения настроек для {interface}: {e}")
return False
try:
subprocess.run(["systemctl", "restart", "networking"], check=True)
self.d.msgbox("Сетевые настройки успешно применены.")
return True
except Exception as e:
self.d.msgbox(f"Ошибка перезапуска сетевой службы: {e}")
return False
def save_dhcp(self, interface, settings):
interface_config = f"""
auto {interface}
iface {interface} inet dhcp
"""
self.update_interface_config(interface, interface_config)
def save_static(self, interface, settings):
ip = settings["ip"]
mask = settings["mask"]
gateway = settings["gateway"]
interface_config = f"""
auto {interface}
iface {interface} inet static
address {ip}
netmask {mask}
"""
if gateway:
interface_config += f"gateway {gateway}\n"
self.update_interface_config(interface, interface_config)
def update_interface_config(self, interface, interface_config):
try:
with open("/etc/network/interfaces", "r") as f:
lines = f.readlines()
with open("/etc/network/interfaces", "w") as f:
skip_section = False
for line in lines:
if interface in line:
skip_section = True
continue
if skip_section and line.strip() == "":
skip_section = False
if not skip_section:
f.write(line)
f.write(interface_config.strip() + "\n")
except Exception as e:
self.d.msgbox(f"Ошибка обновления конфигурации интерфейса: {e}")
def update_resolv_conf(self, dns):
if not dns:
return
dns_entry = f"nameserver {dns}"
try:
with open("/etc/resolv.conf", "r") as f:
resolv_lines = f.readlines()
if any(line.strip() == dns_entry for line in resolv_lines):
return
with open("/etc/resolv.conf", "a") as f:
f.write(dns_entry + "\n")
except Exception as e:
self.d.msgbox(f"Ошибка обновления /etc/resolv.conf: {e}")
if __name__ == "__main__":
configurator = NetworkConfig()
configurator.run()

View file

@ -0,0 +1,106 @@
import subprocess
from dialog import Dialog
import pwd
import os
from postinstall.logger import logger
logging = logger
def get_users():
users = []
try:
for user in pwd.getpwall():
if user.pw_uid >= 1000 and user.pw_name != "nobody":
users.append(user.pw_name)
if "root" not in users:
users.append("root")
except Exception as e:
print(f"Ошибка при получении списка пользователей: {e}")
return users
def install_zsh():
try:
subprocess.run(["apt", "update"], check=True)
subprocess.run(["apt", "install", "-y", "zsh"], check=True)
except subprocess.CalledProcessError as e:
print(f"Ошибка при установке zsh: {e}")
return False
return True
def change_shell():
d = Dialog(dialog="dialog")
users = get_users()
# Первый шаг - выбор опции с помощью radiolist
code, choice = d.radiolist(
"Выберите способ изменения оболочки:",
choices=[
("Для всех пользователей", "", False),
("Выбрать конкретных пользователей", "", True)
]
)
if code != d.OK:
return
selected_users = []
if choice == "Для всех пользователей":
# Если выбрано "Для всех пользователей"
selected_users = users
elif choice == "Выбрать конкретных пользователей":
# Если выбран выбор конкретных пользователей, показываем checklist
code, selected_users = d.checklist(
"Выберите пользователей для изменения оболочки:",
choices=[(user, "", False) for user in users],
clear=True
)
if code != d.OK or not selected_users:
return
# Выбор оболочки
code, shell = d.menu(
"Выберите новую оболочку:", choices=[("1", "bash"), ("2", "zsh")]
)
if code != d.OK:
return
shell_path = "/bin/bash" if shell == "1" else "/bin/zsh"
if shell_path == "/bin/zsh" and not os.path.exists("/bin/zsh"):
logging.info("zsh не установлен. Начинаем установку...")
d.msgbox("zsh не установлен. Начинаем установку...")
if not install_zsh():
d.msgbox("Не удалось установить zsh. Изменение оболочки отменено.")
return
# Применение изменений
if choice == "Для всех пользователей":
# Применение изменений для всех пользователей
for user in selected_users:
try:
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
d.msgbox("Оболочка изменена для всех существующих пользователей")
logging.info("Оболочка изменена для всех существующих пользователей")
else:
# Применение изменений для конкретных пользователей
for user in selected_users:
try:
subprocess.run(["sudo", "chsh", "-s", shell_path, user], check=True)
d.msgbox(f"Оболочка для пользователя {user} изменена на {shell_path}")
logging.info(f"Оболочка для пользователя {user} изменена на {shell_path}")
except subprocess.CalledProcessError as e:
d.msgbox(f"Ошибка при изменении оболочки для пользователя {user}: {e}")
def run_change_shell():
change_shell()
if __name__ == "__main__":
run_change_shell()

View file

@ -0,0 +1,236 @@
import subprocess
from dialog import Dialog
import logging
import os
SSH_CONFIG_PATH = "/etc/ssh/sshd_config"
SSH_AUTH_CONFIG_PATH = "/etc/ssh/sshd_config.d/auth.conf"
UFW_OPENSSH_CONFIG_PATH = "/etc/ufw/applications.d/openssh-server"
class SSHConfig:
def __init__(self):
self.d = Dialog(dialog="dialog")
self.current_port = self.get_current_ssh_port()
self.current_auth_method = self.get_current_auth_method()
self.new_port = None
self.new_auth_method = None
def get_sshd_status(self):
try:
result = subprocess.run(
["systemctl", "is-active", "sshd"], capture_output=True, text=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return "unknown"
def toggle_sshd_service(self, action):
try:
subprocess.run(["sudo", "systemctl", action, "sshd"], check=True)
self.d.msgbox(
f"Служба sshd успешно {'запущена' if action == 'start' else 'остановлена'}."
)
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при {action} службы sshd: {e}")
self.d.msgbox(
f"Ошибка при {'запуске' if action == 'start' else 'остановке'} службы sshd."
)
def get_current_ssh_port(self):
try:
result = subprocess.run(
["grep", "^#*Port", SSH_CONFIG_PATH],
capture_output=True,
text=True,
check=True,
)
port = result.stdout.strip().split()[-1]
return port if port.isdigit() else "22"
except subprocess.CalledProcessError:
logging.warning(
"Не удалось получить порт SSH, используется порт по умолчанию"
)
return "22"
def change_ssh_port(self):
port_display = self.new_port if self.new_port else self.current_port
port_status = " (не применено)" if self.new_port else ""
code, new_port = self.d.inputbox(
f"Текущий порт SSH: {port_display}{port_status}\nВведите новый порт SSH:",
init=port_display,
)
if code != self.d.OK:
return
try:
new_port = int(new_port)
if new_port < 1 or new_port > 65535:
raise ValueError
except ValueError:
self.d.msgbox("Некорректный номер порта. Введите число от 1 до 65535.")
return
code = self.d.yesno(f"Вы уверены, что хотите изменить порт SSH на {new_port}?")
if code != self.d.OK:
return
self.new_port = str(new_port)
def get_current_auth_method(self):
try:
with open(SSH_AUTH_CONFIG_PATH, "r") as f:
content = f.read()
password_auth = "yes" if "PasswordAuthentication yes" in content else "no"
pubkey_auth = "yes" if "PubkeyAuthentication yes" in content else "no"
if password_auth == "yes" and pubkey_auth == "yes":
return "По ключу и паролю"
elif password_auth == "no" and pubkey_auth == "yes":
return "Только по ключу"
elif password_auth == "yes" and pubkey_auth == "no":
return "Только по паролю"
else:
return "По ключу и паролю" # Default value
except FileNotFoundError:
logging.warning("Файл конфигурации аутентификации SSH не найден")
return "По ключу и паролю" # Default value
except Exception as e:
logging.warning(f"Не удалось получить метод аутентификации SSH: {e}")
return "По ключу и паролю" # Default value
def change_auth_method(self):
auth_method_display = (
self.new_auth_method if self.new_auth_method else self.current_auth_method
)
auth_status = " (не применено)" if self.new_auth_method else ""
auth_choices = [
(
"1",
f"Только по паролю{' (текущий)' if auth_method_display == 'Только по паролю' else ''}",
),
(
"2",
f"Только по ключу{' (текущий)' if auth_method_display == 'Только по ключу' else ''}",
),
(
"3",
f"По ключу и паролю{' (текущий)' if auth_method_display == 'По ключу и паролю' else ''}",
),
]
code, tag = self.d.menu(
f"Текущий метод аутентификации: {auth_method_display}{auth_status}\nВыберите метод аутентификации:",
choices=auth_choices,
)
if code != self.d.OK:
return
self.new_auth_method = {
"1": "Только по паролю",
"2": "Только по ключу",
"3": "По ключу и паролю",
}[tag]
def apply_changes(self):
if self.new_port:
try:
subprocess.run(
[
"sudo",
"sed",
"-i",
f"s/^#*Port .*/Port {self.new_port}/",
SSH_CONFIG_PATH,
],
check=True,
)
subprocess.run(
[
"sudo",
"sed",
"-i",
f"s/^ports=.*/ports={self.new_port}\/tcp/",
UFW_OPENSSH_CONFIG_PATH,
],
check=True,
)
subprocess.run(["sudo", "ufw", "app", "update", "OpenSSH"], check=True)
subprocess.run(
["sudo", "ufw", "delete", "allow", "OpenSSH"], check=True
)
subprocess.run(["sudo", "ufw", "allow", "OpenSSH"], check=True)
self.current_port = self.new_port
self.new_port = None
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
self.d.msgbox(f"Порт SSH успешно изменен на {self.current_port}.")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при изменении порта SSH: {e}")
self.d.msgbox(f"Ошибка при изменении порта SSH: {e}")
if self.new_auth_method:
try:
auth_config = ""
if self.new_auth_method == "Только по паролю":
auth_config = (
"PasswordAuthentication yes\nPubkeyAuthentication no\n"
)
elif self.new_auth_method == "Только по ключу":
auth_config = (
"PasswordAuthentication no\nPubkeyAuthentication yes\n"
)
elif self.new_auth_method == "По ключу и паролю":
auth_config = (
"PasswordAuthentication yes\nPubkeyAuthentication yes\n"
)
os.makedirs(os.path.dirname(SSH_AUTH_CONFIG_PATH), exist_ok=True)
with open(SSH_AUTH_CONFIG_PATH, "w") as f:
f.write(auth_config)
self.current_auth_method = self.new_auth_method
self.new_auth_method = None
subprocess.run(["sudo", "systemctl", "restart", "sshd"], check=True)
self.d.msgbox("Метод аутентификации SSH успешно изменен.")
except Exception as e:
logging.error(f"Ошибка при изменении метода аутентификации: {e}")
self.d.msgbox(f"Ошибка при изменении метода аутентификации: {e}")
def main_menu(self):
while True:
status = self.get_sshd_status()
port_display = self.new_port if self.new_port else self.current_port
port_status = " (не применено)" if self.new_port else ""
auth_method_display = (
self.new_auth_method
if self.new_auth_method
else self.current_auth_method
)
auth_status = " (не применено)" if self.new_auth_method else ""
code, tag = self.d.menu(
f"Статус sshd: {status}\n"
f"Текущий порт: {port_display}{port_status}\n"
f"Метод аутентификации: {auth_method_display}{auth_status}",
choices=[
("1", "Включить sshd" if status != "active" else "Выключить sshd"),
("2", "Изменить порт SSH"),
("3", "Изменить метод аутентификации"),
],
)
if code != self.d.OK:
break
if tag == "1":
self.toggle_sshd_service("start" if status != "active" else "stop")
elif tag == "2":
self.change_ssh_port()
elif tag == "3":
self.change_auth_method()
return self.new_port is not None or self.new_auth_method is not None
def run_ssh_config():
ssh_config = SSHConfig()
changes_made = ssh_config.main_menu()
return ssh_config, changes_made
if __name__ == "__main__":
ssh_config, changes_made = run_ssh_config()
# Note: Changes will be applied in the main menu when "Save and exit" is selected

View file

@ -0,0 +1,57 @@
import subprocess
from dialog import Dialog
from postinstall.logger import logger
logging = logger
def run_update():
d = Dialog(dialog="dialog")
code = d.yesno("Вы действительно хотите обновить систему?", width=50)
if code != d.OK:
return
commands = [["apt", "update"], ["apt", "upgrade", "-y"]]
d.gauge_start("Обновление системы", percent=0)
try:
for i, command in enumerate(commands):
d.gauge_update(i * 50, f"Выполняется: {' '.join(command)}")
process = subprocess.Popen(
["sudo"] + command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
while True:
line = process.stdout.readline()
if not line:
break
d.gauge_update(i * 50 + (50 if i == 1 else 25), line.strip())
process.wait()
if process.returncode != 0:
error = process.stderr.read()
raise subprocess.CalledProcessError(process.returncode, command, error)
d.gauge_update(100, "Обновление системы завершено успешно")
d.gauge_stop()
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при обновлении системы: {e}")
d.gauge_stop()
d.msgbox(f"Ошибка при обновлении системы: {e}")
except Exception as e:
logging.error(f"Неожиданная ошибка при обновлении системы: {e}")
d.gauge_stop()
d.msgbox(f"Неожиданная ошибка при обновлении системы: {e}")
d.msgbox("Обновление системы завершено", width=50)
if __name__ == "__main__":
run_update()

View file

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

View file

@ -0,0 +1,9 @@
import subprocess
def get_network_interfaces():
interfaces = str.split(subprocess.run(["ls", "/sys/class/net/"], capture_output=True, text=True).stdout, "\n")
return interfaces[:-1]
get_network_interfaces()

View file

@ -0,0 +1,132 @@
import logging
import subprocess
import os
import re
REGISTRY_URL = "dr.gosrv.ru"
REGISTRY_LOGIN = "dr_admin"
REGISTRY_PASSWORD = "Qweasdzxc123C@"
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
class DockerConfig:
def check_installed(self, command: str) -> bool:
try:
return (
subprocess.run(
["which", command], capture_output=True, text=True
).returncode
== 0
)
except subprocess.SubprocessError as e:
logging.error(f"Error checking if {command} is installed: {e}")
return False
def install_docker(self):
try:
if not self.check_installed("docker"):
logging.info("Установка Docker...")
subprocess.run(["apt-get", "update"], check=True)
subprocess.run(["apt-get", "install", "-y", "docker.io"], check=True)
logging.info("Docker успешно установился")
if not self.check_installed("docker-compose"):
logging.info("Установка Docker Compose...")
subprocess.run(
["apt-get", "install", "-y", "docker-compose"], check=True
)
logging.info("Docker Compose успешно установился")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при установке Docker: {e}")
raise
def check_docker_login(self, registry_url):
try:
result = subprocess.run(
["docker", "login", registry_url, "--password-stdin"],
input=b"\n",
capture_output=True,
text=True,
check=True,
)
return "login succeeded" in result.stdout.lower()
except subprocess.CalledProcessError as e:
logging.error(f"Error checking Docker login: {e}")
return False
def login_to_registry(self):
try:
if not self.check_docker_login(REGISTRY_URL):
logging.info("Вход в Docker registry...")
subprocess.run(
["docker", "login", REGISTRY_URL],
input=f"{REGISTRY_LOGIN}\n{REGISTRY_PASSWORD}\n".encode(),
check=True,
)
logging.info("Успешный вход в Docker registry")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при входе в Docker registry: {e}")
raise
def install_portainer(self, port, login, password, ip):
try:
if not self.check_docker_login(REGISTRY_URL):
self.login_to_registry()
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
docker_compose_content = f"""
version: '3'
services:
portainer:
image: dr.gosrv.ru/portainer
container_name: portainer
restart: unless-stopped
security_opt:
- no-new-privileges:true
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
- ./portainer_password:{DOCKER_COMPOSE_FILE_PATH}/portainer_password
ports:
- "{ip}:{port}:9000"
environment:
- ADMIN_USER={login}
- ADMIN_PASSWORD_FILE={DOCKER_COMPOSE_FILE_PATH}/portainer_password
volumes:
portainer_data:
"""
with open(DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml", "w") as f:
f.write(docker_compose_content)
subprocess.run(
["htpasswd", "-nb", login, password],
stdout=open(DOCKER_COMPOSE_FILE_PATH + "portainer_password", "w"),
check=True,
)
subprocess.run(
[
"docker-compose",
"-f",
DOCKER_COMPOSE_FILE_PATH + "docker-compose.yml",
"up",
"-d",
],
check=True,
)
logging.info("Portainer успешно установлен")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при установке Portainer: {e}")
raise
except IOError as e:
logging.error(f"Ошибка при создании файла: {e}")
raise
def validate_input(self, value, pattern, error_message):
if not re.match(pattern, value):
return False
return True

View file

@ -0,0 +1,67 @@
import unittest
from unittest.mock import patch, MagicMock
from docker_config import DockerConfig, REGISTRY_URL, REGISTRY_LOGIN, REGISTRY_PASSWORD, DOCKER_COMPOSE_FILE_PATH
class TestDockerConfig(unittest.TestCase):
def setUp(self):
self.docker_config = DockerConfig()
@patch("subprocess.run")
def test_check_installed_true(self, mock_run):
mock_run.return_value.returncode = 0
self.assertTrue(self.docker_config.check_installed("docker"))
@patch("subprocess.run")
def test_check_installed_false(self, mock_run):
mock_run.return_value.returncode = 1
self.assertFalse(self.docker_config.check_installed("docker"))
@patch("subprocess.run")
def test_install_docker(self, mock_run):
self.docker_config.install_docker()
mock_run.assert_any_call(["apt-get", "update"], check=True)
mock_run.assert_any_call(["apt-get", "install", "-y", "docker.io"], check=True)
mock_run.assert_any_call(
["apt-get", "install", "-y", "docker-compose"], check=True
)
@patch("subprocess.run")
def test_check_docker_login_success(self, mock_run):
mock_run.return_value.stdout = "Login Succeeded"
self.assertTrue(self.docker_config.check_docker_login("registry_url"))
@patch("subprocess.run")
def test_check_docker_login_failure(self, mock_run):
mock_run.return_value.stdout = "Login Failed"
self.assertFalse(self.docker_config.check_docker_login("registry_url"))
@patch("subprocess.run")
@patch("docker_config.DockerConfig.check_docker_login")
def test_login_to_registry(self, mock_check_login, mock_run):
mock_check_login.return_value = False
self.docker_config.login_to_registry()
mock_run.assert_called_once()
@patch("subprocess.run")
@patch("docker_config.DockerConfig.check_docker_login")
@patch("docker_config.DockerConfig.login_to_registry")
@patch("builtins.open", new_callable=unittest.mock.mock_open)
def test_install_portainer(self, mock_open, mock_login, mock_check_login, mock_run):
mock_check_login.return_value = True
self.docker_config.install_portainer("9000", "admin", "password", "127.0.0.1")
mock_open.assert_called()
mock_run.assert_called()
def test_validate_input_success(self):
self.assertTrue(
self.docker_config.validate_input("admin", r"^[a-zA-Z0-9_-]+$", "Error")
)
def test_validate_input_failure(self):
self.assertFalse(
self.docker_config.validate_input("admin@", r"^[a-zA-Z0-9_-]+$", "Error")
)
if __name__ == "__main__":
unittest.main()