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

434 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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