osgos_postinstall/deb_dist/postinstall-1.0.1/postinstall/option/docker.py

435 lines
16 KiB
Python
Raw Permalink Normal View History

2025-06-09 14:32:16 +05:00
import subprocess
import os
import re
import shlex
from abc import ABC, abstractmethod
from typing import Optional, List, Callable
from dialog import Dialog
from postinstall.logger import logger
REGISTRY_URL = "dr.gosrv.ru"
REGISTRY_LOGIN = "dr_admin"
REGISTRY_PASSWORD = "Qweasdzxc123C@"
DOCKER_COMPOSE_FILE_PATH = "/usr/local/share/postinstall/portainer/"
class CommandExecutor:
@staticmethod
def run_command(
command: str,
shell: bool = False,
input_data: Optional[str] = None,
ignore_errors: List[int] = [],
) -> subprocess.CompletedProcess:
try:
return subprocess.run(
command if shell else shlex.split(command),
shell=shell,
check=True,
input=input_data,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as e:
if e.returncode not in ignore_errors:
logger.error(f"Ошибка выполнения команды: {e}")
raise
else:
logger.warning(
f"Команда завершилась с игнорируемым кодом ошибки {e.returncode}: {e}"
)
return e
class SystemChecker:
@staticmethod
def is_command_installed(command: str) -> bool:
try:
return CommandExecutor.run_command(f"which {command}").returncode == 0
except subprocess.CalledProcessError:
return False
class ProgressGauge:
def __init__(self, dialog: Dialog, title: str, total_steps: int):
self.dialog = dialog
self.title = title
self.total_steps = total_steps
self.current_step = 0
def update(self, step: int, message: str):
self.current_step = step
percent = int((step / self.total_steps) * 100)
self.dialog.gauge_update(percent, f"{message} ({percent}%)", update_text=True)
def __enter__(self):
self.dialog.gauge_start(self.title, percent=0)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.dialog.gauge_stop()
class DockerManager:
@staticmethod
def install(progress_callback: Callable[[int, str], None]):
logger.info("Установка Docker...")
progress_callback(1, "Обновление пакетов")
CommandExecutor.run_command("apt-get update")
progress_callback(2, "Установка Docker")
CommandExecutor.run_command("apt-get install -y docker.io")
progress_callback(3, "Установка Docker Compose")
CommandExecutor.run_command("apt-get install -y docker-compose")
progress_callback(4, "Выполнение входа в Docker registry")
DockerManager.login_to_registry()
progress_callback(5, "Docker успешно установлен")
logger.info("Docker успешно установлен")
@staticmethod
def uninstall(progress_callback: Callable[[int, str], None]):
logger.info("Удаление Docker...")
progress_callback(1, "Удаление Docker и Docker Compose")
CommandExecutor.run_command("apt-get remove -y docker.io docker-compose")
progress_callback(2, "Очистка зависимостей")
CommandExecutor.run_command("apt-get autoremove -y")
progress_callback(3, "Удаление файлов Docker")
CommandExecutor.run_command("rm -rf /var/run/docker")
CommandExecutor.run_command("rm -rf /var/lib/docker")
progress_callback(4, "Docker успешно удален")
logger.info("Docker успешно удален")
@staticmethod
def login_to_registry():
logger.info("Выполняется вход в Docker registry...")
try:
result = CommandExecutor.run_command(
f"echo {REGISTRY_PASSWORD} | docker login --username {REGISTRY_LOGIN} --password-stdin {REGISTRY_URL}",
shell=True,
)
logger.info("Успешный вход в Docker registry")
logger.debug(f"Docker login output: {result.stdout}")
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка входа в Docker registry: {e}")
logger.error(f"Stderr: {e.stderr}")
raise
class UFWManager:
@staticmethod
def add_rule(port: str):
rule_name = "portainer"
logger.info(
f"Добавление именованного правила UFW '{rule_name}' для порта {port}..."
)
CommandExecutor.run_command(f"sudo ufw allow {port}/tcp comment '{rule_name}'")
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
logger.info(
f"Именованное правило UFW '{rule_name}' для порта {port} успешно добавлено"
)
@staticmethod
def remove_rule(port: str):
rule_name = "portainer"
logger.info(
f"Удаление именованного правила UFW '{rule_name}' для порта {port}..."
)
CommandExecutor.run_command(f"sudo ufw delete allow {port}/tcp")
CommandExecutor.run_command(f"sudo ufw app update {rule_name}")
logger.info(
f"Именованное правило UFW '{rule_name}' для порта {port} успешно удалено"
)
class PortainerManager:
@staticmethod
def install(
port: str, password: str, ip: str, progress_callback: Callable[[int, str], None]
):
logger.info("Установка Portainer...")
progress_callback(1, "Вход в Docker registry")
DockerManager.login_to_registry()
progress_callback(2, "Остановка предыдущей версии Portainer")
try:
CommandExecutor.run_command(
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down",
ignore_errors=[1],
)
except subprocess.CalledProcessError:
pass
progress_callback(3, "Удаление старых данных Portainer")
try:
CommandExecutor.run_command(
"docker volume ls -q -f name=portainer* | xargs -r docker volume rm",
ignore_errors=[1],
)
except subprocess.CalledProcessError:
pass
progress_callback(4, "Установка apache2-utils для htpasswd")
if not SystemChecker.is_command_installed("htpasswd"):
CommandExecutor.run_command("apt-get install -y apache2-utils")
progress_callback(5, "Создание конфигурации Portainer")
os.makedirs(os.path.dirname(DOCKER_COMPOSE_FILE_PATH), exist_ok=True)
password_hash = PortainerManager._generate_password_hash(password)
PortainerManager._create_docker_compose_file(port, password_hash, ip)
progress_callback(6, "Запуск Portainer")
CommandExecutor.run_command(
f"sudo docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml up -d"
)
if ip != "127.0.0.1":
progress_callback(7, "Добавление правила UFW")
UFWManager.add_rule(port)
progress_callback(8, "Portainer успешно установлен")
logger.info("Portainer успешно установлен")
@staticmethod
def uninstall(progress_callback: Callable[[int, str], None]):
logger.info("Удаление Portainer...")
progress_callback(1, "Остановка Portainer")
try:
CommandExecutor.run_command(
f"docker-compose -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml down --volumes",
ignore_errors=[1],
)
except subprocess.CalledProcessError:
pass
progress_callback(2, "Удаление файлов конфигурации")
CommandExecutor.run_command(
f"rm -f {DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"
)
try:
with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "r") as f:
config = f.read()
port_ip_match = re.search(r'"(.+):(\d+):9000"', config)
if port_ip_match:
ip, port = port_ip_match.groups()
if ip != "127.0.0.1":
progress_callback(3, "Удаление правила UFW")
UFWManager.remove_rule(port)
except FileNotFoundError:
logger.warning("Файл конфигурации не найден. Пропуск удаления правила UFW.")
progress_callback(4, "Portainer успешно удален")
logger.info("Portainer успешно удален")
@staticmethod
def _generate_password_hash(password: str) -> str:
return CommandExecutor.run_command(
f"htpasswd -nbB admin {shlex.quote(password)} | cut -d ':' -f 2 | sed 's/\\$/\\$\\$/g'",
shell=True,
).stdout.strip()
@staticmethod
def _create_docker_compose_file(port: str, password_hash: str, ip: str):
docker_compose_content = f"""
version: '2'
services:
portainer:
image: {REGISTRY_URL}/portainer:latest
ports:
- "{ip}:{port}:9000"
command: --admin-password '{password_hash}'
networks:
- local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer-data:/data
environment:
- PORTAINER_LOCAL_IMAGES_ENVIRONMENT_ID=1
deploy:
mode: global
networks:
local:
driver: bridge
volumes:
portainer-data:
"""
with open(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml", "w") as f:
f.write(docker_compose_content)
class InputValidator:
@staticmethod
def validate(value: str, pattern: str) -> bool:
return bool(re.match(pattern, value))
class UIElement(ABC):
def __init__(self, dialog: Dialog):
self.dialog = dialog
@abstractmethod
def show(self):
pass
class Menu(UIElement):
def show(self):
choices = []
if not SystemChecker.is_command_installed("docker"):
choices.append(("1", "Установить Docker"))
else:
choices.append(("1", "Удалить Docker"))
code, choice = self.dialog.menu("Выберите действие:", choices=choices)
return code, choice
class YesNoUI(UIElement):
def show(self, text: str):
return self.dialog.yesno(text) == self.dialog.OK
class PasswordInput(UIElement):
def show(self):
while True:
code, password = self.dialog.passwordbox(
"Введите пароль для Portainer:", insecure=True
)
if code == self.dialog.CANCEL:
return None
if InputValidator.validate(password, r"^.{12,}$"):
return password
self.dialog.msgbox(
"Пароль должен содержать минимум 12 символов.", width=50, height=10
)
class PortInput(UIElement):
def show(self):
while True:
code, port = self.dialog.inputbox(
"Введите порт для Portainer:", init="9000"
)
if code == self.dialog.CANCEL:
return None
if InputValidator.validate(port, r"^[1-9][0-9]{0,4}$"):
return port
self.dialog.msgbox(
"Некорректный порт. Введите число от 1 до 65535.", width=50, height=10
)
class IPInput(UIElement):
def show(self):
while True:
code, ip = self.dialog.inputbox(
"Введите IP адрес для Portainer:", init="127.0.0.1"
)
if code == self.dialog.CANCEL:
return None
if InputValidator.validate(ip, r"^(\d{1,3}\.){3}\d{1,3}$"):
return ip
self.dialog.msgbox(
"Некорректный IP адрес. Введите адрес в формате X.X.X.X",
width=50,
height=10,
)
class DockerConfigUI:
def __init__(self):
self.dialog = Dialog(dialog="dialog")
self.menu = Menu(self.dialog)
self.password_input = PasswordInput(self.dialog)
self.port_input = PortInput(self.dialog)
self.ip_input = IPInput(self.dialog)
def run(self):
try:
while True:
code, choice = self.menu.show()
if code == self.dialog.OK:
if choice == "1":
self._handle_docker()
elif choice == "2":
self._handle_portainer()
elif choice == "3":
break
else:
break
except Exception as e:
logger.error(f"Произошла ошибка: {e}")
self.dialog.msgbox(f"Произошла ошибка: {str(e)}", width=50, height=10)
def _handle_docker(self):
if not SystemChecker.is_command_installed("docker"):
with ProgressGauge(self.dialog, "Установка Docker", 4) as gauge:
DockerManager.install(gauge.update)
self.dialog.msgbox("Docker успешно установлен.", width=50, height=10)
logger.info("Docker успешно установлен.")
yes_no_ui = YesNoUI(self.dialog)
if yes_no_ui.show(text="Вы хотите установить Docker Swarm - инструмент для управления кластером докер серверов?"):
CommandExecutor.run_command("docker swarm init")
if yes_no_ui.show(
text="Вы хотите установить Portainer - удобный Web интерфейс для управления докер контейнерами?"
):
self.handle_portainer()
else:
with ProgressGauge(self.dialog, "Удаление Docker", 4) as gauge:
PortainerManager.uninstall(gauge.update)
DockerManager.uninstall(gauge.update)
self.dialog.msgbox("Docker успешно удален.", width=50, height=10)
logger.info("Docker успешно удален.")
def handle_portainer(self):
if not SystemChecker.is_command_installed("docker"):
self.dialog.msgbox(
"Docker не установлен. Пожалуйста, сначала установите Docker.",
width=50,
height=10,
)
return
if not os.path.exists(f"{DOCKER_COMPOSE_FILE_PATH}docker-compose.yml"):
self._install_portainer()
else:
with ProgressGauge(self.dialog, "Удаление Portainer", 3) as gauge:
PortainerManager.uninstall(gauge.update)
self.dialog.msgbox("Portainer успешно удален.", width=50, height=10)
logger.info("Portainer успешно удален.")
def _install_portainer(self):
password = self.password_input.show()
if not password:
return
port = self.port_input.show()
if not port:
return
ip = self.ip_input.show()
if not ip:
return
try:
with ProgressGauge(self.dialog, "Установка Portainer", 6) as gauge:
PortainerManager.install(port, password, ip, gauge.update)
self.dialog.msgbox(
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль",
width=50,
height=10,
)
logger.info(
f"Portainer успешно установлен.\nВы можете войти в Portainer по адресу {ip}:{port} используя логин admin и введенный пароль"
)
except Exception as e:
self.dialog.msgbox(
f"Ошибка при установке Portainer: {str(e)}", width=50, height=10
)
def run():
ui = DockerConfigUI()
ui.run()