#!/usr/bin/python3 # -*- coding: utf-8 -*- """ Модуль для работы с кластером 1С через SSH """ import logging from typing import Optional, List, Dict from .logger import get_logger from .protocols import SSHOperationsBase logger = get_logger("c1_cluster") class C1ClusterOperations(SSHOperationsBase): """ Класс для операций с кластером 1С через SSH. Реализует контракт операционного модуля (ssh: SSHProtocol). """ def __init__(self, ssh_client, srv_1c: str = "", c1_claster_user: str = "", c1_claster_pass: str = ""): """ Args: ssh_client: Экземпляр, реализующий SSHProtocol (SSHBase, SSHClient). srv_1c: Имя LXC контейнера с 1С c1_claster_user: Пользователь кластера 1С c1_claster_pass: Пароль кластера 1С """ super().__init__(ssh_client) self.srv_1c: str = srv_1c self.c1_claster_user: str = c1_claster_user self.c1_claster_pass: str = c1_claster_pass def set_srv_1c(self, srv_1c: str) -> None: """ Установка имени LXC контейнера с 1С Args: srv_1c: Имя LXC контейнера с 1С """ if not srv_1c or not isinstance(srv_1c, str) or not srv_1c.strip(): raise ValueError("srv_1c должен быть непустой строкой") self.srv_1c = srv_1c logger.info(f"Установлено имя контейнера 1С: {srv_1c}") def set_cluster_credentials(self, c1_claster_user: str, c1_claster_pass: str) -> None: """ Установка учетных данных кластера 1С Args: c1_claster_user: Пользователь кластера 1С c1_claster_pass: Пароль кластера 1С """ if not c1_claster_user or not isinstance(c1_claster_user, str) or not c1_claster_user.strip(): raise ValueError("c1_claster_user должен быть непустой строкой") if not c1_claster_pass or not isinstance(c1_claster_pass, str) or not c1_claster_pass.strip(): raise ValueError("c1_claster_pass должен быть непустой строкой") self.c1_claster_user = c1_claster_user self.c1_claster_pass = c1_claster_pass logger.info(f"Установлены учетные данные кластера 1С для пользователя: {c1_claster_user}") def set_cluster_user(self, c1_claster_user: str) -> None: """ Установка пользователя кластера 1С Args: c1_claster_user: Пользователь кластера 1С """ if not c1_claster_user or not isinstance(c1_claster_user, str) or not c1_claster_user.strip(): raise ValueError("c1_claster_user должен быть непустой строкой") self.c1_claster_user = c1_claster_user logger.info(f"Установлен пользователь кластера 1С: {c1_claster_user}") def set_cluster_password(self, c1_claster_pass: str) -> None: """ Установка пароля кластера 1С Args: c1_claster_pass: Пароль кластера 1С """ if not c1_claster_pass or not isinstance(c1_claster_pass, str) or not c1_claster_pass.strip(): raise ValueError("c1_claster_pass должен быть непустой строкой") self.c1_claster_pass = c1_claster_pass logger.info("Пароль кластера 1С обновлен") def _validate_config(self) -> None: """ Проверка наличия необходимых конфигурационных параметров Raises: ValueError: Если не установлены необходимые параметры """ if not self.srv_1c or not self.srv_1c.strip(): raise ValueError("srv_1c не установлен. Используйте set_srv_1c() или передайте при инициализации") if not self.c1_claster_user or not self.c1_claster_user.strip(): raise ValueError("c1_claster_user не установлен. Используйте set_cluster_user() или передайте при инициализации") if not self.c1_claster_pass or not self.c1_claster_pass.strip(): raise ValueError("c1_claster_pass не установлен. Используйте set_cluster_password() или передайте при инициализации") def cluster_version(self) -> str: """ Получение версии кластера 1С Returns: str: Версия кластера """ self._validate_config() try: logger.info(f"Получение версии кластера 1С из контейнера {self.srv_1c}") std, err = self.ssh.cmd(f'lxc exec {self.srv_1c} -- ls -lh /opt/1cv8/x86_64/ | grep 8.3') if err and err.strip(): logger.error(f"Ошибка получения версии кластера: {err}") raise Exception(f"Ошибка получения версии кластера: {err}") version = std.split(' ')[-1].strip() if not version: logger.error("Не удалось определить версию кластера") raise Exception("Не удалось определить версию кластера") logger.info(f"Версия кластера 1С: {version}") return version except Exception as e: logger.error(f"Ошибка при получении версии кластера: {e}") raise Exception(f"Ошибка при получении версии кластера: {e}") def cluster_daemon_start(self) -> str: """ Запуск демона кластера 1С Returns: str: Вывод ошибок (если есть) """ self._validate_config() try: logger.info(f"Запуск демона кластера 1С в контейнере {self.srv_1c}") std, err = self.ssh.cmd(f'lxc exec {self.srv_1c} -- /opt/1cv8/x86_64/{self.cluster_version()}/ras --daemon cluster') if err and err.strip(): logger.warning(f"Предупреждение при запуске демона: {err}") else: logger.info("Демон кластера 1С запущен успешно") return err except Exception as e: logger.error(f"Ошибка при запуске демона кластера: {e}") raise Exception(f"Ошибка при запуске демона кластера: {e}") def cluster_id(self) -> str: """ Получение ID кластера 1С Returns: str: ID кластера """ self._validate_config() try: logger.info(f"Получение ID кластера 1С из контейнера {self.srv_1c}") std, err = self.ssh.cmd(f'lxc exec {self.srv_1c} -- /opt/1cv8/x86_64/{self.cluster_version()}/rac cluster list') if err and err.strip(): logger.error(f"Ошибка получения ID кластера: {err}") raise Exception(f"Ошибка получения ID кластера: {err}") if not std or not std.strip(): logger.error("Пустой ответ при получении ID кластера") raise Exception("Пустой ответ при получении ID кластера") cluster_id = std.split('\n')[0].split(':')[1].strip() if not cluster_id: logger.error("Не удалось извлечь ID кластера") raise Exception("Не удалось извлечь ID кластера") logger.info(f"ID кластера 1С: {cluster_id}") return cluster_id except Exception as e: logger.error(f"Ошибка при получении ID кластера: {e}") raise Exception(f"Ошибка при получении ID кластера: {e}") def base_list(self) -> List[Dict[str, List[str]]]: """ Получение списка баз данных 1С Returns: list[dict]: Список словарей с информацией о базах данных """ self._validate_config() try: logger.info(f"Получение списка баз данных 1С из контейнера {self.srv_1c}") std, err = self.ssh.cmd( f"lxc exec {self.srv_1c} -- /opt/1cv8/x86_64/{self.cluster_version()}/rac " f"infobase summary list --cluster={self.cluster_id()} " f"--cluster-user={self.c1_claster_user} --cluster-pwd='{self.c1_claster_pass}'" ) if err and err.strip(): logger.error(f"Ошибка получения списка баз 1С: {err}") raise Exception(f"Ошибка получения списка баз 1С: {err}") list_of_base_info = [] # Разделяем по двойным переводам строк (каждая база отделена двумя \n) for raw_info in std.split('\n\n')[:-1]: if not raw_info.strip(): continue # Исправлено: raw_info уже разделен, не нужно снова split('\n\n') lines = raw_info.split('\n') if len(lines) < 3: continue try: bases_info = { "id": lines[0].split(' : ')[1].split() if ' : ' in lines[0] else [], "name": lines[1].split(' : ')[1].split() if ' : ' in lines[1] else [], "description": lines[2].split(' : ')[1].split() if ' : ' in lines[2] else [] } list_of_base_info.append(bases_info) except (IndexError, ValueError) as e: # Пропускаем некорректно отформатированные записи continue logger.info(f"Найдено {len(list_of_base_info)} баз данных 1С") return list_of_base_info except Exception as e: logger.error(f"Ошибка при получении списка баз 1С: {e}") raise Exception(f"Ошибка при получении списка баз 1С: {e}") def base_id(self, base_name: str) -> Optional[str]: """ Получает ID базы данных 1С по её имени Args: base_name: Имя базы данных Returns: str: ID базы данных или None, если база не найдена """ self._validate_config() if not base_name or not isinstance(base_name, str) or not base_name.strip(): raise ValueError("base_name должен быть непустой строкой") try: logger.debug(f"Поиск ID базы данных {base_name} в контейнере {self.srv_1c}") for base in self.base_list(): if base.get('name') and len(base['name']) > 0 and base['name'][0] == base_name: if base.get('id') and len(base['id']) > 0: base_id = base['id'][0] logger.info(f"Найден ID базы данных {base_name}: {base_id}") return base_id logger.warning(f"База данных {base_name} не найдена") return None except Exception as e: logger.error(f"Ошибка при получении ID базы данных: {e}") raise Exception(f"Ошибка при получении ID базы данных: {e}") def base_info(self, base_name: str, infobase_user: str, infobase_password: str) -> Optional[Dict[str, str]]: """ Получение информации о базе данных 1С Args: base_name: Имя базы данных infobase_user: Пользователь информационной базы infobase_password: Пароль пользователя информационной базы Returns: dict: Словарь с информацией о базе данных или None, если база не найдена """ self._validate_config() # Валидация входных данных for param_name, param_value in [ ('base_name', base_name), ('infobase_user', infobase_user), ('infobase_password', infobase_password) ]: if not param_value or not isinstance(param_value, str) or not param_value.strip(): raise ValueError(f"{param_name} должен быть непустой строкой") try: base_id = self.base_id(base_name) if not base_id: logger.warning(f"База данных {base_name} не найдена") return None logger.info(f"Получение информации о базе данных {base_name} (ID: {base_id})") std, err = self.ssh.cmd( f"lxc exec {self.srv_1c} -- /opt/1cv8/x86_64/{self.cluster_version()}/rac " f"infobase info --cluster={self.cluster_id()} " f"--infobase={base_id} " f"--infobase-user={infobase_user} --infobase-pwd='{infobase_password}' " f"--cluster-user={self.c1_claster_user} --cluster-pwd='{self.c1_claster_pass}'" ) if err and err.strip(): logger.error(f"Ошибка получения информации о базе данных: {err}") raise Exception(f"Ошибка получения информации о базе данных: {err}") result = {} # Разделяем входную строку по переводам строк lines = std.strip().split('\n') for line in lines: # Пропускаем пустые строки if not line.strip(): continue # Разделяем строку на имя параметра и значение if ':' in line: # Разделяем только по первому вхождению ':' # чтобы корректно обрабатывать значения с двоеточиями parts = line.split(':', 1) # Извлекаем и очищаем имя параметра и значение param_name = parts[0].strip() param_value = parts[1].strip() if len(parts) > 1 else '' # Добавляем в словарь if param_name: # добавляем только если имя параметра не пустое result[param_name] = param_value logger.info(f"Информация о базе данных {base_name} получена успешно") return result except Exception as e: logger.error(f"Ошибка при получении информации о базе данных: {e}") raise Exception(f"Ошибка при получении информации о базе данных: {e}") def base_info_update(self, base_name: str, db_server: str, db_name: str, db_user: str, db_password: str, infobase_user: str, infobase_password: str, scheduled_jobs_deny: str = "off", sessions_deny: str = "off") -> Optional[str]: """ Обновляет информацию о базе данных 1С Args: base_name: Имя базы данных 1С db_server: Сервер PostgreSQL db_name: Имя базы данных PostgreSQL db_user: Пользователь PostgreSQL db_password: Пароль PostgreSQL infobase_user: Пользователь информационной базы infobase_password: Пароль пользователя информационной базы scheduled_jobs_deny: Запрет запланированных заданий (on/off), по умолчанию "off" sessions_deny: Запрет сеансов (on/off), по умолчанию "off" Returns: str: ID обновленной базы данных или None при ошибке """ self._validate_config() # Валидация входных данных for param_name, param_value in [ ('base_name', base_name), ('db_server', db_server), ('db_name', db_name), ('db_user', db_user), ('infobase_user', infobase_user), ('infobase_password', infobase_password) ]: if not param_value or not isinstance(param_value, str) or not param_value.strip(): raise ValueError(f"{param_name} должен быть непустой строкой") # db_password может быть пустой строкой, проверяем только тип if not isinstance(db_password, str): raise ValueError("db_password должен быть строкой") # Валидация scheduled_jobs_deny и sessions_deny if scheduled_jobs_deny not in ["on", "off"]: raise ValueError("scheduled_jobs_deny должен быть 'on' или 'off'") if sessions_deny not in ["on", "off"]: raise ValueError("sessions_deny должен быть 'on' или 'off'") try: logger.info(f"Обновление информации о базе данных {base_name}") base_id = self.base_id(base_name) if not base_id: logger.error(f"База данных '{base_name}' не найдена") raise Exception(f"База данных '{base_name}' не найдена") logger.info(f"Обновление параметров базы данных {base_name} (ID: {base_id})") std, err = self.ssh.cmd( f"lxc exec {self.srv_1c} -- /opt/1cv8/x86_64/{self.cluster_version()}/rac " f"infobase update --cluster={self.cluster_id()} " f"--infobase={base_id} --dbms=PostgreSQL " f"--db-server={db_server} --db-name={db_name} " f"--db-user={db_user} --db-pwd={db_password} " f"--infobase-user={infobase_user} --infobase-pwd='{infobase_password}' " f"--cluster-user={self.c1_claster_user} --cluster-pwd='{self.c1_claster_pass}' " f"--scheduled-jobs-deny={scheduled_jobs_deny} --sessions-deny={sessions_deny}" ) if err and err.strip(): logger.error(f"Ошибка обновления базы данных: {err}") raise Exception(f"Ошибка обновления базы данных: {err}") # Проверяем, что обновление прошло успешно for base in self.base_list(): if base['name'] and len(base['name']) > 0 and base['name'][0] == base_name: if base['id'] and len(base['id']) > 0: logger.info(f"Информация о базе данных {base_name} обновлена успешно") return base['id'][0] logger.warning(f"Не удалось подтвердить обновление базы данных {base_name}") return None except Exception as e: logger.error(f"Ошибка при обновлении информации о базе данных: {e}") raise Exception(f"Ошибка при обновлении информации о базе данных: {e}")