Старый postinstall
This commit is contained in:
commit
4337a3f94e
349 changed files with 21945 additions and 0 deletions
533
postinstall/option/docker.py
Normal file
533
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue