From 5bbb585d9f1343b3476ddaf35c083c39131b9ef0 Mon Sep 17 00:00:00 2001 From: neon Date: Mon, 9 Feb 2026 20:45:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D1=8E=20=D1=83=D0=B6=D0=B5=20=D1=81=D1=83=D1=89=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=20=D0=B2=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B9=20GIT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 1c-migration.py | 65 +++++ README.md | 16 ++ c1_cluster.py | 400 +++++++++++++++++++++++++++++ config.py | 112 +++++++++ example_c1_cluster.py | 385 ++++++++++++++++++++++++++++ example_postgresql.py | 569 ++++++++++++++++++++++++++++++++++++++++++ postgresql.py | 368 +++++++++++++++++++++++++++ pyvenv.cfg | 5 + requirements.txt | 16 ++ ssh.py | 49 ++++ ssh_base.py | 213 ++++++++++++++++ 11 files changed, 2198 insertions(+) create mode 100755 1c-migration.py create mode 100644 README.md create mode 100644 c1_cluster.py create mode 100644 config.py create mode 100755 example_c1_cluster.py create mode 100755 example_postgresql.py create mode 100644 postgresql.py create mode 100644 pyvenv.cfg create mode 100644 requirements.txt create mode 100755 ssh.py create mode 100644 ssh_base.py diff --git a/1c-migration.py b/1c-migration.py new file mode 100755 index 0000000..c976b7d --- /dev/null +++ b/1c-migration.py @@ -0,0 +1,65 @@ +#!/usr/bin/python3 +import ssh as s +import config + +# Загружаем конфигурацию +cfg = config.get_config() + +# Извлекаем настройки из конфигурации +server = cfg['ssh']['hostname'] +arhive_srv_pgsql = cfg['postgresql']['archive_server'] +restore_srv_pgsql = cfg['postgresql']['restore_server'] +backup_date = cfg['postgresql']['backup_date'] +extra = cfg['postgresql']['extra_backup'] +postgres_user = cfg['postgresql']['postgres_user'] +postgres_password = cfg['postgresql']['postgres_password'] + +arhive_bases_name = cfg['migration']['archive_bases_name'] +restore_bases_name = cfg['migration']['restore_bases_name'] +scheduled_jobs_deny = cfg['migration']['scheduled_jobs_deny'] +sessions_deny = cfg['migration']['sessions_deny'] + +lxc_conteiner_name = cfg['c1']['lxc_container_name'] +c1_claster_user = cfg['c1']['cluster_user'] +c1_claster_pass = cfg['c1']['cluster_password'] +db_server = cfg['c1']['db_server'] +db_name = cfg['c1'].get('db_name', '') # Если не указано, будет использовано имя базы 1С +db_user = cfg['c1']['db_user'] +db_password = cfg['c1']['db_password'] + +infobase_user = cfg['c1']['infobase_user'] +infobase_password = cfg['c1']['infobase_password'] + + +c = s.ssh(server) +c.connect() + +# Устанавливаем конфигурацию кластера 1С +c.set_c1_config(lxc_conteiner_name, c1_claster_user, c1_claster_pass) + +#version_c1 = c.cluster_version() +#print(f"Версия кластера 1с: {version_c1}") + +#c.cluster_daemon_start() + +#c1_id = c.cluster_id() +#print("id кластера 1с: "+c1_id) + +for arhive_base_name, restore_base_name in zip(arhive_bases_name, restore_bases_name): + # Используем db_name из конфига, если не указано - используем имя базы 1С + actual_db_name = db_name if db_name else arhive_base_name + base_update = c.base_info_update( + arhive_base_name, db_server, actual_db_name, db_user, db_password, + infobase_user, infobase_password, + scheduled_jobs_deny, sessions_deny + ) + +# base_info = c.base_info( +# arhive_base_name, infobase_user, infobase_password +# ) + +# print(f"Название базы : {base_info.get('name')}") +# print(f"Сервер баз данных : {base_info.get('db-server')}") +# print(f"Пользователь баз данных : {base_info.get('db-user')}") + +c.close() diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a8d014 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Проект: Автоматизация задач с использованием Cursor AI + +![Логотип](https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB:Cursor_logo.svg) + +> Краткое описание проекта + +## 🚀 Работа с GIT: +```bash +lxc shell code +cd /root/lib/ssh_client +source bin/activate +git status +git add . +git commit -a -m 'Реструктуризировал проект' +git push -u origin main +exit diff --git a/c1_cluster.py b/c1_cluster.py new file mode 100644 index 0000000..b4e9ddb --- /dev/null +++ b/c1_cluster.py @@ -0,0 +1,400 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Модуль для работы с кластером 1С через SSH +""" +import logging +from typing import Optional, List, Dict + +logger = logging.getLogger(__name__) + + +class C1ClusterOperations: + """ + Класс для операций с кластером 1С через SSH + """ + def __init__(self, ssh_client, srv_1c: str = "", c1_claster_user: str = "", c1_claster_pass: str = ""): + """ + Инициализация модуля 1С кластера + + Args: + ssh_client: Экземпляр SSHBase для выполнения команд + srv_1c: Имя LXC контейнера с 1С + c1_claster_user: Пользователь кластера 1С + c1_claster_pass: Пароль кластера 1С + """ + self.ssh = ssh_client + self.srv_1c = srv_1c + self.c1_claster_user = c1_claster_user + self.c1_claster_pass = 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}") + + diff --git a/config.py b/config.py new file mode 100644 index 0000000..af1a5bf --- /dev/null +++ b/config.py @@ -0,0 +1,112 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Конфигурационный файл для SSH клиента и миграции 1С +ВНИМАНИЕ: Не коммитьте этот файл в публичные репозитории! +Пароли хранятся в открытом виде в этом файле. +""" + +# SSH настройки +SSH_CONFIG = { + "hostname": "g.it.cln.su", + "port": 22222, # Порт должен быть int + "username": "root", + "pkey_file": "/root/.ssh/id_rsa", + "host_keys": "~/.ssh/known_hosts" +} + +# PostgreSQL настройки +POSTGRESQL_CONFIG = { + "archive_server": "1c.it.cln.su", + "restore_server": "postgres.it.cln.su", + "backup_date": "16.12.2025", + "extra_backup": True, + "postgres_user": "postgres", + "postgres_password": "PrestigePostgres" +} + +# 1C настройки +C1_CONFIG = { + "lxc_container_name": "c1", # Имя LXC контейнера с сервером 1С + "cluster_user": "neon", + "cluster_password": "Pre$tige310582", + + # Настройки для обновления базы 1С (используются в c1_base_info_update) +# "db_server": "/tmp", +# "db_user": "usr1cv8", +# "db_password": "", + + "db_server": "postgres.it.cln.su", + "db_user": "postgres", + "db_password": "PrestigePostgres", + +# "db_name": "", # Имя базы данных PostgreSQL (если пустое, будет использовано имя базы 1С) + + "infobase_user": "neon", + "infobase_password": "$F%G^H&J*K" +} + +# Списки баз данных для миграции и примеров использования +MIGRATION_CONFIG = { + "archive_bases_name": [ +# 'konsaltpt-buhg', +# 'vpr-ut-crm', +# 'quant-ut', +# 'kompromis-test', +# 'luna-ut', +# 'messinia-buhg', +# 'morea-buhg', +# 'horen-ut', + 'salon', + 'lmotor-ut', + 'staretail', + 'uran-ut', + ], + "restore_bases_name": None, # Если None, будет использован archive_bases_name + "bases": None, # Список баз для обработки в примерах (example_c1_cluster.py, example_postgresql.py) + # Если None, будет использован archive_bases_name + "scheduled_jobs_deny": "on", # Запрет запланированных заданий для всех баз (on/off) + "sessions_deny": "off" # Запрет сеансов для всех баз (on/off) +} + +def get_config(): + """ + Возвращает конфигурацию проекта + + Returns: + dict: Словарь с конфигурацией, содержащий секции: + - ssh: настройки SSH подключения + - postgresql: настройки PostgreSQL + - c1: настройки 1С кластера + - migration: настройки миграции баз данных (включая список баз для примеров) + """ + config = { + 'ssh': SSH_CONFIG.copy(), + 'postgresql': POSTGRESQL_CONFIG.copy(), + 'c1': C1_CONFIG.copy(), + 'migration': MIGRATION_CONFIG.copy() + } + + # Если restore_bases_name не указан, используем archive_bases_name + if config['migration']['restore_bases_name'] is None: + config['migration']['restore_bases_name'] = config['migration']['archive_bases_name'].copy() + + # Если bases не указан, используем archive_bases_name для примеров + if config['migration']['bases'] is None: + config['migration']['bases'] = config['migration']['archive_bases_name'].copy() + + # Валидация scheduled_jobs_deny и sessions_deny + scheduled_jobs_deny = config['migration'].get('scheduled_jobs_deny', 'off') + sessions_deny = config['migration'].get('sessions_deny', 'off') + + if scheduled_jobs_deny not in ['on', 'off']: + raise ValueError(f"scheduled_jobs_deny должен быть 'on' или 'off', получено: {scheduled_jobs_deny}") + if sessions_deny not in ['on', 'off']: + raise ValueError(f"sessions_deny должен быть 'on' или 'off', получено: {sessions_deny}") + + # Устанавливаем значения по умолчанию, если не указаны + config['migration']['scheduled_jobs_deny'] = scheduled_jobs_deny + config['migration']['sessions_deny'] = sessions_deny + + return config + diff --git a/example_c1_cluster.py b/example_c1_cluster.py new file mode 100755 index 0000000..86fdaaa --- /dev/null +++ b/example_c1_cluster.py @@ -0,0 +1,385 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Примеры использования модуля c1_cluster.py новым способом +Демонстрирует прямое использование C1ClusterOperations без основного класса ssh +""" +import config +from ssh_base import SSHBase +from c1_cluster import C1ClusterOperations + + +def example_basic_usage(): + """ + Пример базового использования модуля c1_cluster + """ + print("=" * 60) + print("Пример 1: Базовое использование модуля c1_cluster") + print("=" * 60) + + # Загружаем конфигурацию + cfg = config.get_config() + + # Создаем SSH подключение + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'], + host_keys=cfg['ssh']['host_keys'] + ) + + # Подключаемся + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + # Получаем версию кластера + version = c1_cluster.cluster_version() + print(f"Версия кластера 1С: {version}") + + # Получаем ID кластера + cluster_id = c1_cluster.cluster_id() + print(f"ID кластера 1С: {cluster_id}") + + # Закрываем соединение + ssh_client.close() + print("\n") + + +def example_get_base_list(): + """ + Пример получения списка баз данных 1С + """ + print("=" * 60) + print("Пример 2: Получение списка баз данных 1С") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + # Получаем список баз данных + bases = c1_cluster.base_list() + + print(f"Найдено баз данных: {len(bases)}") + for base in bases: + if base.get('name') and len(base['name']) > 0: + base_name = base['name'][0] + base_id = base['id'][0] if base.get('id') and len(base['id']) > 0 else 'N/A' + print(f" - {base_name} (ID: {base_id})") + + ssh_client.close() + print("\n") + + +def example_get_base_info(): + """ + Пример получения информации о конкретных базах данных из списка в конфиге + """ + print("=" * 60) + print("Пример 3: Получение информации о базах данных 1С") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + infobase_user = cfg['c1']['infobase_user'] + infobase_password = cfg['c1']['infobase_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + # Получаем список баз из конфигурации + bases_to_process = cfg['migration'].get('bases', []) + + if not bases_to_process: + print("Список баз для обработки не указан в конфигурации (migration.bases)") + ssh_client.close() + return + + print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:") + print(f" Список баз: {', '.join(bases_to_process)}\n") + + # Обрабатываем каждую базу из списка + for base_name in bases_to_process: + print(f"{'=' * 60}") + print(f"База данных: {base_name}") + print(f"{'=' * 60}") + + try: + # Получаем ID базы + base_id = c1_cluster.base_id(base_name) + if base_id: + print(f"ID базы данных '{base_name}': {base_id}") + + # Получаем полную информацию о базе + base_info = c1_cluster.base_info(base_name, infobase_user, infobase_password) + + if base_info: + print(f"\nИнформация о базе данных '{base_name}':") + for key, value in base_info.items(): + print(f" {key}: {value}") + else: + print(f"Не удалось получить информацию о базе '{base_name}'") + else: + print(f"База данных '{base_name}' не найдена в кластере") + except Exception as e: + print(f"Ошибка при обработке базы '{base_name}': {e}") + + print() + + ssh_client.close() + print("\n") + + +def example_update_base_info(): + """ + Пример обновления информации о базах данных 1С из списка в конфиге + """ + print("=" * 60) + print("Пример 4: Обновление информации о базах данных 1С") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + infobase_user = cfg['c1']['infobase_user'] + infobase_password = cfg['c1']['infobase_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + # Параметры базы данных PostgreSQL + db_server = cfg['c1']['db_server'] + db_name = cfg['c1'].get('db_name', '') + db_user = cfg['c1']['db_user'] + db_password = cfg['c1']['db_password'] + + # Параметры управления доступом из конфигурации миграции + scheduled_jobs_deny = cfg['migration'].get('scheduled_jobs_deny', 'off') + sessions_deny = cfg['migration'].get('sessions_deny', 'off') + + # Получаем список баз из конфигурации + bases_to_process = cfg['migration'].get('bases', []) + + if not bases_to_process: + print("Список баз для обработки не указан в конфигурации (migration.bases)") + ssh_client.close() + return + + print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:") + print(f" Список баз: {', '.join(bases_to_process)}") + print(f" Параметры подключения:") + print(f" Сервер БД: {db_server}") + print(f" Пользователь БД: {db_user}") + print(f" db_name из конфига: {db_name if db_name else '(будет использовано имя базы 1С)'}") + print(f" Параметры управления доступом:") + print(f" Запрет запланированных заданий (scheduled_jobs_deny): {scheduled_jobs_deny}") + print(f" Запрет сеансов (sessions_deny): {sessions_deny}") + print() + + # Обрабатываем каждую базу из списка + for base_name in bases_to_process: + print(f"{'=' * 60}") + print(f"База данных: {base_name}") + print(f"{'=' * 60}") + + try: + # Если db_name не указан, используем имя базы 1С + actual_db_name = db_name if db_name else base_name + + print(f" Обновление параметров:") + print(f" Сервер БД: {db_server}") + print(f" Имя БД: {actual_db_name}") + print(f" Пользователь БД: {db_user}") + print(f" Запрет запланированных заданий: {scheduled_jobs_deny}") + print(f" Запрет сеансов: {sessions_deny}") + + # Обновляем информацию о базе + updated_base_id = c1_cluster.base_info_update( + base_name, db_server, actual_db_name, db_user, db_password, + infobase_user, infobase_password, + scheduled_jobs_deny, sessions_deny + ) + + if updated_base_id: + print(f" ✓ База данных успешно обновлена. ID: {updated_base_id}") + else: + print(f" ✗ Не удалось обновить базу данных '{base_name}'") + except Exception as e: + print(f" ✗ Ошибка при обновлении базы '{base_name}': {e}") + + print() + + ssh_client.close() + print("\n") + + +def example_workflow(): + """ + Пример полного рабочего процесса: получение версии, запуск демона, получение списка баз + """ + print("=" * 60) + print("Пример 5: Полный рабочий процесс") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + try: + # Шаг 1: Получаем версию кластера + print("Шаг 1: Получение версии кластера...") + version = c1_cluster.cluster_version() + print(f" Версия: {version}") + + # Шаг 2: Запускаем демон кластера + print("\nШаг 2: Запуск демона кластера...") + err = c1_cluster.cluster_daemon_start() + if err: + print(f" Предупреждение: {err}") + else: + print(" Демон запущен успешно") + + # Шаг 3: Получаем ID кластера + print("\nШаг 3: Получение ID кластера...") + cluster_id = c1_cluster.cluster_id() + print(f" ID кластера: {cluster_id}") + + # Шаг 4: Получаем список баз данных + print("\nШаг 4: Получение списка баз данных...") + bases = c1_cluster.base_list() + print(f" Найдено баз: {len(bases)}") + + # Шаг 5: Для каждой базы получаем ID + print("\nШаг 5: Получение ID для каждой базы...") + for base in bases: + if base.get('name') and len(base['name']) > 0: + base_name = base['name'][0] + base_id = c1_cluster.base_id(base_name) + print(f" {base_name}: {base_id}") + + except Exception as e: + print(f"Ошибка: {e}") + finally: + ssh_client.close() + + print("\n") + + +def example_context_manager_style(): + """ + Пример использования в стиле context manager (с try/finally) + """ + print("=" * 60) + print("Пример 6: Использование с обработкой ошибок") + print("=" * 60) + + cfg = config.get_config() + ssh_client = None + + try: + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + # Создаем экземпляр модуля 1С кластера с параметрами из конфига + lxc_container = cfg['c1']['lxc_container_name'] + cluster_user = cfg['c1']['cluster_user'] + cluster_password = cfg['c1']['cluster_password'] + c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password) + + # Получаем версию и список баз + version = c1_cluster.cluster_version() + print(f"Версия кластера: {version}") + + bases = c1_cluster.base_list() + print(f"Список баз данных ({len(bases)} шт.):") + for base in bases[:3]: # Показываем только первые 3 + if base.get('name') and len(base['name']) > 0: + print(f" - {base['name'][0]}") + + except Exception as e: + print(f"Произошла ошибка: {e}") + finally: + if ssh_client: + ssh_client.close() + print("Соединение закрыто") + + print("\n") + + +if __name__ == "__main__": + print("\n" + "=" * 60) + print("Примеры использования модуля c1_cluster.py") + print("=" * 60 + "\n") + + # Раскомментируйте нужные примеры для запуска + + # example_basic_usage() + # example_get_base_list() + # example_get_base_info() + example_update_base_info() + # example_workflow() + # example_context_manager_style() + + print("Для запуска примеров раскомментируйте соответствующие функции в конце файла") + print("\nПримеры демонстрируют:") + print(" 1. Базовое использование модуля") + print(" 2. Получение списка баз данных") + print(" 3. Получение информации о базе данных") + print(" 4. Обновление информации о базе данных") + print(" 5. Полный рабочий процесс") + print(" 6. Использование с обработкой ошибок") + diff --git a/example_postgresql.py b/example_postgresql.py new file mode 100755 index 0000000..ef73459 --- /dev/null +++ b/example_postgresql.py @@ -0,0 +1,569 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Примеры использования модуля postgresql.py новым способом +Демонстрирует прямое использование PostgreSQLOperations без основного класса ssh +""" +import config +from ssh_base import SSHBase +from postgresql import PostgreSQLOperations + + +def example_basic_usage(): + """ + Пример базового использования модуля postgresql + """ + print("=" * 60) + print("Пример 1: Базовое использование модуля postgresql") + print("=" * 60) + + # Загружаем конфигурацию + cfg = config.get_config() + + # Создаем SSH подключение + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'], + host_keys=cfg['ssh']['host_keys'] + ) + + # Подключаемся + ssh_client.connect() + + # Создаем экземпляр модуля PostgreSQL + pg = PostgreSQLOperations(ssh_client) + + # Получаем список баз данных + srv_pgsql = cfg['postgresql']['archive_server'] + bases = pg.bases_list(srv_pgsql) + + print(f"Сервер PostgreSQL: {srv_pgsql}") + print(f"Найдено баз данных: {len(bases)}") + for base in bases[:5]: # Показываем первые 5 + print(f" - {base}") + + # Закрываем соединение + ssh_client.close() + print("\n") + + +def example_get_bases_list(): + """ + Пример получения и вывода списка баз данных + """ + print("=" * 60) + print("Пример 2: Получение списка баз данных PostgreSQL") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + # Получаем список баз данных + bases = pg.bases_list(srv_pgsql) + + print(f"Сервер: {srv_pgsql}") + print(f"Всего баз данных: {len(bases)}") + print("\nСписок баз данных:") + pg.bases_list_print(srv_pgsql) + + ssh_client.close() + print("\n") + + +def example_get_bases_size(): + """ + Пример получения размеров баз данных + """ + print("=" * 60) + print("Пример 3: Получение размеров баз данных PostgreSQL") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + print(f"Сервер: {srv_pgsql}") + print("\nРазмеры баз данных:") + print("-" * 60) + print(f"{'База данных':<30} | Размер") + print("-" * 60) + pg.bases_size_print(srv_pgsql) + + ssh_client.close() + print("\n") + + +def example_backup_single_base(): + """ + Пример создания бэкапа одной базы данных + """ + print("=" * 60) + print("Пример 4: Создание бэкапа одной базы данных") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + # Получаем список баз и берем первую для примера + bases = pg.bases_list(srv_pgsql) + if bases: + base_name = bases[0] + print(f"Создание бэкапа базы данных: {base_name}") + print(f"Сервер: {srv_pgsql}") + + try: + results = pg.bases_backup(srv_pgsql, base_name) + for result in results: + if result['success']: + print(f"✓ Бэкап базы '{result['base']}' создан успешно") + else: + print(f"✗ Ошибка при создании бэкапа '{result['base']}': {result['stderr']}") + except Exception as e: + print(f"Ошибка: {e}") + else: + print("Базы данных не найдены") + + ssh_client.close() + print("\n") + + +def example_backup_multiple_bases(): + """ + Пример создания бэкапа нескольких баз данных из списка в конфиге + """ + print("=" * 60) + print("Пример 5: Создание бэкапа нескольких баз данных") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + # Получаем список баз из конфигурации для примеров + bases_to_backup = cfg['migration'].get('bases', []) + + if not bases_to_backup: + print("Список баз для бэкапа не указан в конфигурации (migration.bases)") + print("Используем все базы с сервера") + bases_to_backup = None + + print(f"Сервер: {srv_pgsql}") + if bases_to_backup: + print(f"Базы для бэкапа: {', '.join(bases_to_backup)}") + else: + print("Бэкап всех баз данных") + + try: + # Если указан список баз, создаем бэкап для каждой + if bases_to_backup: + for base_name in bases_to_backup: + print(f"\nСоздание бэкапа базы: {base_name}") + results = pg.bases_backup(srv_pgsql, base_name) + for result in results: + if result['success']: + print(f" ✓ Бэкап базы '{result['base']}' создан успешно") + else: + print(f" ✗ Ошибка: {result['stderr']}") + else: + # Бэкап всех баз + results = pg.bases_backup(srv_pgsql, None) + print(f"\nОбработано баз: {len(results)}") + successful = sum(1 for r in results if r['success']) + failed = len(results) - successful + print(f"Успешно: {successful}, Ошибок: {failed}") + + for result in results: + if not result['success']: + print(f" ✗ Ошибка при бэкапе '{result['base']}': {result['stderr']}") + except Exception as e: + print(f"Ошибка: {e}") + + ssh_client.close() + print("\n") + + +def example_backup_all_bases(): + """ + Пример создания бэкапа всех баз данных + """ + print("=" * 60) + print("Пример 6: Создание бэкапа всех баз данных") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + print(f"Сервер: {srv_pgsql}") + print("Создание бэкапа всех баз данных...") + + try: + result = pg.bases_backup_all(srv_pgsql) + print("✓ Бэкап всех баз данных завершен успешно") + except Exception as e: + print(f"✗ Ошибка при создании бэкапа: {e}") + + ssh_client.close() + print("\n") + + +def example_create_and_drop_base(): + """ + Пример создания и удаления базы данных + """ + print("=" * 60) + print("Пример 7: Создание и удаление базы данных") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['restore_server'] + test_base_name = "test_base_example" + + print(f"Сервер: {srv_pgsql}") + print(f"Тестовая база: {test_base_name}") + + try: + # Создаем базу данных + print("\n1. Создание базы данных...") + pg.base_create(srv_pgsql, test_base_name) + print(f"✓ База данных '{test_base_name}' создана") + + # Проверяем, что база создана + bases = pg.bases_list(srv_pgsql) + if test_base_name in bases: + print(f"✓ База '{test_base_name}' найдена в списке баз") + + # Удаляем базу данных + print("\n2. Удаление базы данных...") + pg.base_drop(srv_pgsql, test_base_name) + print(f"✓ База данных '{test_base_name}' удалена") + + # Проверяем, что база удалена + bases = pg.bases_list(srv_pgsql) + if test_base_name not in bases: + print(f"✓ База '{test_base_name}' отсутствует в списке баз") + + except Exception as e: + print(f"✗ Ошибка: {e}") + + ssh_client.close() + print("\n") + + +def example_restore_base(): + """ + Пример восстановления базы данных из бэкапа + """ + print("=" * 60) + print("Пример 8: Восстановление базы данных из бэкапа") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + archive_server = cfg['postgresql']['archive_server'] + restore_server = cfg['postgresql']['restore_server'] + backup_date = cfg['postgresql']['backup_date'] + extra = cfg['postgresql']['extra_backup'] + + # Используем первую базу из списка миграции + archive_bases = cfg['migration']['archive_bases_name'] + restore_bases = cfg['migration']['restore_bases_name'] + + if archive_bases and restore_bases: + for archive_base, restore_base in zip(archive_bases, restore_bases): + print(f"Восстановление базы данных:") + print(f" Архив: {archive_server}") + print(f" База в архиве: {archive_base}") + print(f" Сервер восстановления: {restore_server}") + print(f" База для восстановления: {restore_base}") + print(f" Дата бэкапа: {backup_date}") + print(f" Экстра-бэкап: {extra}") + + try: + # Сначала удаляем старую базу, если существует + print(f"\n1. Удаление старой базы '{restore_base}' (если существует)...") + try: + pg.base_drop(restore_server, restore_base) + print(f" ✓ Старая база удалена") + except Exception: + print(f" База не существует или уже удалена") + + # Создаем новую базу + print(f"\n2. Создание новой базы '{restore_base}'...") + pg.base_create(restore_server, restore_base) + print(f" ✓ База создана") + + # Восстанавливаем из бэкапа + print(f"\n3. Восстановление из бэкапа...") + pg.base_restore( + archive_server, restore_server, backup_date, + archive_base, restore_base, extra + ) + print(f" ✓ База '{restore_base}' восстановлена успешно") + + except Exception as e: + print(f" ✗ Ошибка при восстановлении: {e}") + + ssh_client.close() + print("\n") + + +def example_manage_backups(): + """ + Пример управления старыми бэкапами + """ + print("=" * 60) + print("Пример 9: Управление старыми бэкапами") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + backup_path = f'/backup/pgsql/{srv_pgsql}' + days_old = 30 # Удалять бэкапы старше 30 дней + + print(f"Путь к бэкапам: {backup_path}") + print(f"Удаление бэкапов старше: {days_old} дней") + + try: + # Получаем список старых директорий + print("\n1. Поиск старых бэкапов...") + old_dirs = pg.file_list(backup_path, days_old) + if old_dirs.strip(): + dirs_list = [d.strip() for d in old_dirs.split('\n') if d.strip()] + print(f"Найдено директорий для удаления: {len(dirs_list)}") + for d in dirs_list[:5]: # Показываем первые 5 + print(f" - {d}") + else: + print("Старые бэкапы не найдены") + + # Удаляем старые бэкапы + print(f"\n2. Удаление бэкапов старше {days_old} дней...") + pg.delete_old_backups(backup_path, days_old) + print("✓ Удаление завершено") + + except Exception as e: + print(f"✗ Ошибка: {e}") + + ssh_client.close() + print("\n") + + +def example_full_workflow(): + """ + Пример полного рабочего процесса: список -> размеры -> бэкап + """ + print("=" * 60) + print("Пример 10: Полный рабочий процесс") + print("=" * 60) + + cfg = config.get_config() + + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + try: + # Шаг 1: Получаем список баз + print("Шаг 1: Получение списка баз данных...") + bases = pg.bases_list(srv_pgsql) + print(f" Найдено баз: {len(bases)}") + + # Шаг 2: Получаем размеры баз + print("\nШаг 2: Получение размеров баз данных...") + sizes = pg.bases_size(srv_pgsql) + print(f" Получены размеры для {len(sizes)} баз") + for size_info in sizes[:3]: # Показываем первые 3 + base_name = size_info[0] + size = ''.join(size_info[1]) if size_info[1] else 'N/A' + print(f" {base_name}: {size}") + + # Шаг 3: Создаем бэкап первой базы (если есть) + if bases: + print(f"\nШаг 3: Создание бэкапа базы '{bases[0]}'...") + results = pg.bases_backup(srv_pgsql, bases[0]) + for result in results: + if result['success']: + print(f" ✓ Бэкап создан успешно") + else: + print(f" ✗ Ошибка: {result['stderr']}") + + except Exception as e: + print(f"Ошибка: {e}") + finally: + ssh_client.close() + + print("\n") + + +def example_context_manager_style(): + """ + Пример использования с обработкой ошибок + """ + print("=" * 60) + print("Пример 11: Использование с обработкой ошибок") + print("=" * 60) + + cfg = config.get_config() + ssh_client = None + + try: + ssh_client = SSHBase( + hostname=cfg['ssh']['hostname'], + port=cfg['ssh']['port'], + username=cfg['ssh']['username'], + pkey_file=cfg['ssh']['pkey_file'] + ) + ssh_client.connect() + + pg = PostgreSQLOperations(ssh_client) + + srv_pgsql = cfg['postgresql']['archive_server'] + + # Получаем список баз и их размеры + bases = pg.bases_list(srv_pgsql) + print(f"Сервер: {srv_pgsql}") + print(f"Найдено баз данных: {len(bases)}") + + if bases: + print("\nПервые 5 баз данных:") + for base in bases[:5]: + print(f" - {base}") + + except Exception as e: + print(f"Произошла ошибка: {e}") + finally: + if ssh_client: + ssh_client.close() + print("\nСоединение закрыто") + + print("\n") + + +if __name__ == "__main__": + print("\n" + "=" * 60) + print("Примеры использования модуля postgresql.py") + print("=" * 60 + "\n") + + # Раскомментируйте нужные примеры для запуска + + # example_basic_usage() + example_get_bases_list() + # example_get_bases_size() + # example_backup_single_base() + # example_backup_multiple_bases() + # example_backup_all_bases() + # example_create_and_drop_base() + # example_restore_base() + # example_manage_backups() + # example_full_workflow() + # example_context_manager_style() + + print("Для запуска примеров раскомментируйте соответствующие функции в конце файла") + print("\nПримеры демонстрируют:") + print(" 1. Базовое использование модуля") + print(" 2. Получение списка баз данных") + print(" 3. Получение размеров баз данных") + print(" 4. Создание бэкапа одной базы") + print(" 5. Создание бэкапа нескольких баз из конфига") + print(" 6. Создание бэкапа всех баз") + print(" 7. Создание и удаление базы данных") + print(" 8. Восстановление базы из бэкапа") + print(" 9. Управление старыми бэкапами") + print(" 10. Полный рабочий процесс") + print(" 11. Использование с обработкой ошибок") + + diff --git a/postgresql.py b/postgresql.py new file mode 100644 index 0000000..63c603a --- /dev/null +++ b/postgresql.py @@ -0,0 +1,368 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Модуль для работы с PostgreSQL через SSH +""" +import logging +from datetime import datetime +from typing import Optional, List, Dict, Union + +logger = logging.getLogger(__name__) + + +class PostgreSQLOperations: + """ + Класс для операций с PostgreSQL через SSH + """ + def __init__(self, ssh_client): + """ + Инициализация модуля PostgreSQL + + Args: + ssh_client: Экземпляр SSHBase для выполнения команд + """ + self.ssh = ssh_client + + def bases_list(self, srv_pgsql: str) -> List[str]: + """ + Получение списка баз данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + + Returns: + list: Список имен баз данных + """ + try: + logger.info(f"Получение списка баз данных PostgreSQL с сервера {srv_pgsql}") + std, err = self.ssh.cmd(f'psql -h {srv_pgsql} -U postgres -t -A -q -c "select datname from pg_database"') + if err and err.strip(): + logger.error(f"Ошибка получения списка баз: {err}") + raise Exception(f"Ошибка получения списка баз: {err}") + + bases = [b.strip() for b in std.split('\n') if b.strip()] + # Удаляем системные базы + for sys_base in ['postgres', 'template0', 'template1']: + if sys_base in bases: + bases.remove(sys_base) + logger.info(f"Найдено {len(bases)} баз данных") + return bases + except Exception as e: + logger.error(f"Ошибка при получении списка баз данных: {e}") + raise Exception(f"Ошибка при получении списка баз данных: {e}") + + def bases_size(self, srv_pgsql: str) -> List[List[Union[str, List[str]]]]: + """ + Получение размеров всех баз данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + + Returns: + list: Список списков [имя_базы, размер, ошибки] + """ + if not srv_pgsql or not isinstance(srv_pgsql, str) or not srv_pgsql.strip(): + raise ValueError("srv_pgsql должен быть непустой строкой") + + try: + logger.info(f"Получение размеров баз данных PostgreSQL с сервера {srv_pgsql}") + base_size = [] + for base in self.bases_list(srv_pgsql): + std, err = self.ssh.cmd(f'psql -h {srv_pgsql} -U postgres -c "SELECT pg_size_pretty( pg_database_size( \'{base}\' ) );"') + base_size.append([base, std.split('\n')[2:-3], err.split('\n')]) + logger.info(f"Получены размеры для {len(base_size)} баз данных") + return base_size + except Exception as e: + logger.error(f"Ошибка при получении размеров баз данных: {e}") + raise Exception(f"Ошибка при получении размеров баз данных: {e}") + + def bases_size_print(self, srv_pgsql: str) -> None: + """ + Вывод размеров баз данных PostgreSQL в консоль + + Args: + srv_pgsql: Сервер PostgreSQL + """ + for base_info in self.bases_size(srv_pgsql): + print(f"{base_info[0].ljust(20)} | {''.join(base_info[1])}") + + def bases_list_print(self, srv_pgsql: str) -> None: + """ + Вывод списка баз данных PostgreSQL в консоль + + Args: + srv_pgsql: Сервер PostgreSQL + """ + for base in self.bases_list(srv_pgsql): + print(base) + + def bases_backup(self, srv_pgsql: str, base_name: Optional[str] = None) -> List[Dict[str, Union[str, bool]]]: + """ + Создание бэкапа базы данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + base_name: Имя базы данных (если None, бэкапятся все базы) + + Returns: + list: Список словарей с результатами бэкапа, каждый содержит: + - 'base': имя базы данных + - 'stdout': стандартный вывод + - 'stderr': вывод ошибок + - 'success': True/False в зависимости от успешности операции + + Raises: + ValueError: При невалидных входных данных + Exception: При ошибке создания бэкапа + """ + # Валидация входных данных + if not srv_pgsql or not isinstance(srv_pgsql, str) or not srv_pgsql.strip(): + raise ValueError("srv_pgsql должен быть непустой строкой") + + if base_name is not None and (not isinstance(base_name, str) or not base_name.strip()): + raise ValueError("base_name должен быть None или непустой строкой") + + try: + date = datetime.now().strftime("%d.%m.%Y") + backup_dir = f'/backup/pgsql/{srv_pgsql}/{date}-extra/' + logger.info(f"Создание бэкапа базы данных {base_name or 'всех баз'} с сервера {srv_pgsql}") + std, err = self.ssh.cmd(f'mkdir -p {backup_dir}') + if err and err.strip(): + logger.error(f"Ошибка создания директории: {err}") + raise Exception(f"Ошибка создания директории: {err}") + + results = [] + if base_name: + logger.info(f"Создание бэкапа базы {base_name}") + std, err = self.ssh.cmd(f'pg_dump -h {srv_pgsql} -U postgres -Fd -w {base_name} -j 10 -f {backup_dir}{base_name}.dump') + success = not (err and err.strip()) + results.append({ + 'base': base_name, + 'stdout': std, + 'stderr': err, + 'success': success + }) + if not success: + logger.error(f"Ошибка бэкапа базы {base_name}: {err}") + raise Exception(f"Ошибка бэкапа базы {base_name}: {err}") + logger.info(f"Бэкап базы {base_name} создан успешно") + else: + bases = self.bases_list(srv_pgsql) + logger.info(f"Создание бэкапа {len(bases)} баз данных") + for base in bases: + logger.info(f"Создание бэкапа базы {base}") + std, err = self.ssh.cmd(f'pg_dump -h {srv_pgsql} -U postgres -Fd -w {base} -j 10 -f {backup_dir}{base}.dump') + success = not (err and err.strip()) + results.append({ + 'base': base, + 'stdout': std, + 'stderr': err, + 'success': success + }) + if success: + logger.info(f"Бэкап базы {base} создан успешно") + else: + logger.warning(f"Ошибка при создании бэкапа базы {base}: {err}") + return results + except Exception as e: + logger.error(f"Ошибка при создании бэкапа: {e}") + raise Exception(f"Ошибка при создании бэкапа: {e}") + + def bases_backup_all(self, srv_pgsql: str) -> str: + """ + Создание бэкапа всех баз данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + + Returns: + str: Результат последней операции бэкапа + """ + try: + date = datetime.now().strftime("%d.%m.%Y") + backup_dir = f'/backup/pgsql/{srv_pgsql}-{date}/' + logger.info(f"Создание бэкапа всех баз данных с сервера {srv_pgsql}") + std, err = self.ssh.cmd(f'mkdir -p {backup_dir}') + if err and err.strip(): + logger.error(f"Ошибка создания директории: {err}") + raise Exception(f"Ошибка создания директории: {err}") + + bases = self.bases_list(srv_pgsql) + logger.info(f"Создание бэкапа {len(bases)} баз данных") + for base in bases: + logger.info(f"Создание бэкапа базы {base}") + std, err = self.ssh.cmd(f'pg_dump -h {srv_pgsql} -U postgres -Fd -w {base} -j 10 -f {backup_dir}{base}.dump') + if err and err.strip(): + logger.error(f"Ошибка бэкапа базы {base}: {err}") + raise Exception(f"Ошибка бэкапа базы {base}: {err}") + logger.info(f"Бэкап базы {base} создан успешно") + logger.info(f"Бэкап всех баз данных завершен") + return std + except Exception as e: + logger.error(f"Ошибка при создании бэкапа всех баз: {e}") + raise Exception(f"Ошибка при создании бэкапа всех баз: {e}") + + def base_create(self, srv_pgsql: str, base_name: str) -> str: + """ + Создание базы данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + base_name: Имя базы данных + + Returns: + str: Результат выполнения команды + """ + if not srv_pgsql or not isinstance(srv_pgsql, str) or not srv_pgsql.strip(): + raise ValueError("srv_pgsql должен быть непустой строкой") + if not base_name or not isinstance(base_name, str) or not base_name.strip(): + raise ValueError("base_name должен быть непустой строкой") + + try: + logger.info(f"Создание базы данных {base_name} на сервере {srv_pgsql}") + std, err = self.ssh.cmd(f'createdb -h {srv_pgsql} -U postgres -w {base_name}') + if err and err.strip(): + logger.error(f"Ошибка создания базы данных: {err}") + raise Exception(f"Ошибка создания базы данных: {err}") + logger.info(f"База данных {base_name} создана успешно") + return std + except Exception as e: + logger.error(f"Ошибка при создании базы данных: {e}") + raise Exception(f"Ошибка при создании базы данных: {e}") + + def base_drop(self, srv_pgsql: str, base_name: str) -> str: + """ + Удаление базы данных PostgreSQL + + Args: + srv_pgsql: Сервер PostgreSQL + base_name: Имя базы данных + + Returns: + str: Результат выполнения команды + """ + if not srv_pgsql or not isinstance(srv_pgsql, str) or not srv_pgsql.strip(): + raise ValueError("srv_pgsql должен быть непустой строкой") + if not base_name or not isinstance(base_name, str) or not base_name.strip(): + raise ValueError("base_name должен быть непустой строкой") + + try: + logger.info(f"Удаление базы данных {base_name} с сервера {srv_pgsql}") + std, err = self.ssh.cmd(f'dropdb -h {srv_pgsql} -U postgres -w {base_name} -f') + if err and err.strip(): + logger.error(f"Ошибка удаления базы данных: {err}") + raise Exception(f"Ошибка удаления базы данных: {err}") + logger.info(f"База данных {base_name} удалена успешно") + return std + except Exception as e: + logger.error(f"Ошибка при удалении базы данных: {e}") + raise Exception(f"Ошибка при удалении базы данных: {e}") + + def base_restore(self, arhive_srv_pgsql: str, restore_srv_pgsql: str, backup_date: str, + arhive_base_name: str, restore_base_name: str, extra: bool = False) -> str: + """ + Восстановление базы данных PostgreSQL из бэкапа + + Args: + arhive_srv_pgsql: Сервер с архивом + restore_srv_pgsql: Сервер для восстановления + backup_date: Дата бэкапа + arhive_base_name: Имя базы в архиве + restore_base_name: Имя базы для восстановления + extra: Флаг экстра-бэкапа + + Returns: + str: Результат выполнения команды + """ + # Валидация входных данных + for param_name, param_value in [ + ('arhive_srv_pgsql', arhive_srv_pgsql), + ('restore_srv_pgsql', restore_srv_pgsql), + ('backup_date', backup_date), + ('arhive_base_name', arhive_base_name), + ('restore_base_name', restore_base_name) + ]: + if not param_value or not isinstance(param_value, str) or not param_value.strip(): + raise ValueError(f"{param_name} должен быть непустой строкой") + + try: + if extra: + backup_path = f'/backup/pgsql/{arhive_srv_pgsql}/{backup_date}-extra/{arhive_base_name}.dump' + else: + backup_path = f'/backup/pgsql/{arhive_srv_pgsql}-{backup_date}/{arhive_base_name}.dump' + + logger.info(f"Восстановление базы данных {restore_base_name} из {backup_path}") + std, err = self.ssh.cmd( + f'pg_restore -h {restore_srv_pgsql} -U postgres -w -Fd -j10 -d {restore_base_name} {backup_path}' + ) + if err and err.strip(): + logger.error(f"Ошибка восстановления базы данных: {err}") + raise Exception(f"Ошибка восстановления базы данных: {err}") + logger.info(f"База данных {restore_base_name} восстановлена успешно") + return std + except Exception as e: + logger.error(f"Ошибка при восстановлении базы данных: {e}") + raise Exception(f"Ошибка при восстановлении базы данных: {e}") + + def file_list(self, path: str, days: Union[int, str]) -> str: + """ + Получение списка директорий старше указанного количества дней + + Args: + path: Путь для поиска + days: Количество дней (строка или число) + + Returns: + str: Список директорий + """ + if not path or not isinstance(path, str) or not path.strip(): + raise ValueError("path должен быть непустой строкой") + + try: + days_str = str(days) + logger.info(f"Поиск директорий старше {days} дней в {path}") + std, err = self.ssh.cmd(f'/usr/bin/find {path} -type d -maxdepth 1 -mtime +{days_str} -print') + if err and err.strip(): + logger.error(f"Ошибка поиска файлов: {err}") + raise Exception(f"Ошибка поиска файлов: {err}") + return std + except Exception as e: + logger.error(f"Ошибка при получении списка файлов: {e}") + raise Exception(f"Ошибка при получении списка файлов: {e}") + + def delete_old_backups(self, path: str, days: Union[int, str]) -> None: + """ + Удаление старых бэкапов + + Args: + path: Путь к директории с бэкапами + days: Количество дней (строка или число) + """ + if not path or not isinstance(path, str) or not path.strip(): + raise ValueError("path должен быть непустой строкой") + + try: + days_str = str(days) + logger.info(f"Удаление бэкапов старше {days} дней из {path}") + dirs, err = self.ssh.cmd(f'/usr/bin/find {path} -type d -maxdepth 1 -mtime +{days_str} -print') + if err and err.strip(): + logger.error(f"Ошибка поиска директорий: {err}") + raise Exception(f"Ошибка поиска директорий: {err}") + + dirs_list = [d.strip() for d in dirs.split('\n') if d.strip()] + logger.info(f"Найдено {len(dirs_list)} директорий для удаления") + for d in dirs_list: + logger.info(f"Удаление директории {d}") + std, err = self.ssh.cmd(f'rm -r {d}') + if err and err.strip(): + logger.error(f"Ошибка удаления директории {d}: {err}") + raise Exception(f"Ошибка удаления директории {d}: {err}") + logger.info(f"Удаление завершено") + except Exception as e: + logger.error(f"Ошибка при удалении старых бэкапов: {e}") + raise Exception(f"Ошибка при удалении старых бэкапов: {e}") + + + + + diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..1218222 --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /root/lib/ssh_client diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0f17dbc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +# Зависимости проекта SSH Client для миграции 1С +# +# Установка зависимостей: +# pip install -r requirements.txt +# +# Или с указанием версии Python: +# python3 -m pip install -r requirements.txt + +# SSH клиент для подключения к удаленным серверам +paramiko>=2.12.0,<4.0.0 + + + + + + diff --git a/ssh.py b/ssh.py new file mode 100755 index 0000000..8c13051 --- /dev/null +++ b/ssh.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Основной модуль SSH клиента для миграции 1С +Объединяет базовые SSH операции, PostgreSQL и 1С кластер +""" +from ssh_base import SSHBase +from postgresql import PostgreSQLOperations +from c1_cluster import C1ClusterOperations + + +class ssh(SSHBase, PostgreSQLOperations, C1ClusterOperations): + """ + Класс SSH клиента с поддержкой PostgreSQL и 1С кластера + + Наследует функциональность от: + - SSHBase: базовые SSH операции (connect, cmd, close) + - PostgreSQLOperations: операции с PostgreSQL + - C1ClusterOperations: операции с кластером 1С + """ + def __init__(self, hostname: str = "test", port: int = 22222, username: str = "root", + pkey_file: str = "/root/.ssh/id_rsa", host_keys: str = "~/.ssh/known_hosts") -> None: + """ + Инициализация SSH клиента + + Args: + hostname: Имя хоста или IP адрес + port: Порт SSH (int) + username: Имя пользователя + pkey_file: Путь к приватному ключу + host_keys: Путь к файлу known_hosts + """ + # Инициализируем базовый SSH класс + SSHBase.__init__(self, hostname, port, username, pkey_file, host_keys) + # Инициализируем модули операций + PostgreSQLOperations.__init__(self, self) + C1ClusterOperations.__init__(self, self, "", "", "") + + def set_c1_config(self, srv_1c: str, c1_claster_user: str, c1_claster_pass: str) -> None: + """ + Установка конфигурации кластера 1С + + Args: + srv_1c: Имя LXC контейнера с 1С + c1_claster_user: Пользователь кластера 1С + c1_claster_pass: Пароль кластера 1С + """ + self.set_srv_1c(srv_1c) + self.set_cluster_credentials(c1_claster_user, c1_claster_pass) diff --git a/ssh_base.py b/ssh_base.py new file mode 100644 index 0000000..7d8313b --- /dev/null +++ b/ssh_base.py @@ -0,0 +1,213 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Базовый модуль для SSH подключений +""" +import paramiko +import os +import time +import logging +from typing import List + +# Настройка логирования +logger = logging.getLogger(__name__) +if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + +class SSHBase: + """ + Базовый класс для SSH подключений + """ + def __init__(self, hostname: str = "test", port: int = 22222, username: str = "root", + pkey_file: str = "/root/.ssh/id_rsa", host_keys: str = "~/.ssh/known_hosts") -> None: + """ + Инициализация SSH клиента + + Args: + hostname: Имя хоста или IP адрес + port: Порт SSH (int) + username: Имя пользователя + pkey_file: Путь к приватному ключу + host_keys: Путь к файлу known_hosts + + Raises: + ValueError: При невалидных входных данных + FileNotFoundError: При отсутствии файла ключа + paramiko.ssh_exception.SSHException: При ошибке инициализации + """ + # Валидация входных данных + if not hostname or not isinstance(hostname, str) or not hostname.strip(): + raise ValueError("hostname должен быть непустой строкой") + + try: + self.port = int(port) + if not (1 <= self.port <= 65535): + raise ValueError("port должен быть в диапазоне 1-65535") + except (ValueError, TypeError): + raise ValueError(f"port должен быть целым числом, получено: {type(port).__name__}") + + if not username or not isinstance(username, str) or not username.strip(): + raise ValueError("username должен быть непустой строкой") + + if not pkey_file or not isinstance(pkey_file, str): + raise ValueError("pkey_file должен быть непустой строкой") + + if not os.path.exists(pkey_file): + raise FileNotFoundError(f"Файл приватного ключа не найден: {pkey_file}") + + self.hostname = hostname.strip() + self.user = username.strip() + self.host_keys = host_keys + self.s = None + self.stdin = None + self.stdout = None + self.stderr = None + + try: + logger.info(f"Инициализация SSH клиента для {hostname}:{port}") + self.key = paramiko.RSAKey.from_private_key_file(pkey_file) + self.s = paramiko.SSHClient() + self.s.load_system_host_keys() + if host_keys: + expanded_host_keys = os.path.expanduser(host_keys) + if os.path.exists(expanded_host_keys): + self.s.load_host_keys(expanded_host_keys) + self.s.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + logger.debug("SSH клиент успешно инициализирован") + except FileNotFoundError as e: + logger.error(f"Не найден файл ключа или known_hosts: {e}") + raise FileNotFoundError(f"Не найден файл ключа или known_hosts: {e}") + except paramiko.ssh_exception.SSHException as e: + logger.error(f"Ошибка инициализации SSH клиента: {e}") + raise paramiko.ssh_exception.SSHException(f"Ошибка инициализации SSH клиента: {e}") + + def connect(self) -> None: + """ + Подключение к SSH серверу + + Raises: + paramiko.ssh_exception.SSHException: При ошибке подключения + paramiko.ssh_exception.AuthenticationException: При ошибке аутентификации + """ + if not self.s: + logger.error("SSH клиент не инициализирован") + raise paramiko.ssh_exception.SSHException("SSH клиент не инициализирован") + + try: + logger.info(f"Подключение к {self.hostname}:{self.port}") + self.s.connect(self.hostname, self.port, pkey=self.key, username=self.user) + logger.info(f"Успешно подключено к {self.hostname}:{self.port}") + except paramiko.ssh_exception.AuthenticationException as e: + logger.error(f"Ошибка аутентификации при подключении к {self.hostname}:{self.port}: {e}") + raise paramiko.ssh_exception.AuthenticationException(f"Ошибка аутентификации: {e}") + except paramiko.ssh_exception.SSHException as e: + logger.error(f"Ошибка подключения к {self.hostname}:{self.port}: {e}") + raise paramiko.ssh_exception.SSHException(f"Ошибка подключения к {self.hostname}:{self.port}: {e}") + except Exception as e: + logger.error(f"Неожиданная ошибка при подключении к {self.hostname}:{self.port}: {e}") + raise Exception(f"Неожиданная ошибка при подключении: {e}") + + def cmd(self, command: str, sleep: float = 0.1, out_to_print: bool = False) -> List[str]: + """ + Выполнение команды на удаленном сервере + + Args: + command: Команда для выполнения + sleep: Задержка после выполнения команды (секунды) + out_to_print: Не используется (оставлено для обратной совместимости) + + Returns: + list: [stdout, stderr] - вывод команды и ошибки + + Raises: + ValueError: При невалидных входных данных + paramiko.ssh_exception.SSHException: При ошибке выполнения команды + """ + # Валидация входных данных + if not command or not isinstance(command, str) or not command.strip(): + raise ValueError("command должен быть непустой строкой") + + if not isinstance(sleep, (int, float)) or sleep < 0: + raise ValueError("sleep должен быть неотрицательным числом") + + if not self.s: + logger.error("SSH клиент не подключен") + raise paramiko.ssh_exception.SSHException("SSH клиент не подключен. Вызовите connect() перед выполнением команд.") + + try: + logger.debug(f"Выполнение команды: {command}") + self.stdin, self.stdout, self.stderr = self.s.exec_command(command) + + # Ждем завершения команды и получаем код возврата + exit_status = self.stdout.channel.recv_exit_status() + + time.sleep(sleep) + + stdout_data = self.stdout.read().decode('utf-8', errors='replace') + stderr_data = self.stderr.read().decode('utf-8', errors='replace') + + # Если команда завершилась с ошибкой, добавляем информацию в stderr + if exit_status != 0: + logger.warning(f"Команда завершилась с кодом возврата {exit_status}: {command}") + if not stderr_data.strip(): + stderr_data = f"Команда завершилась с кодом возврата {exit_status}" + else: + logger.debug(f"Команда выполнена успешно: {command}") + + return [stdout_data, stderr_data] + except paramiko.ssh_exception.SSHException as e: + logger.error(f"Ошибка выполнения команды '{command}': {e}") + raise paramiko.ssh_exception.SSHException(f"Ошибка выполнения команды '{command}': {e}") + except Exception as e: + logger.error(f"Неожиданная ошибка при выполнении команды '{command}': {e}") + raise Exception(f"Неожиданная ошибка при выполнении команды '{command}': {e}") + + def close(self) -> None: + """ + Закрытие SSH соединения + + Безопасно закрывает все открытые соединения и потоки, + игнорируя ошибки, которые могут возникнуть при закрытии + уже закрытых соединений. + """ + logger.info(f"Закрытие SSH соединения с {self.hostname}:{self.port}") + # Закрываем потоки в правильном порядке + streams_to_close = [ + ('stdin', self.stdin), + ('stdout', self.stdout), + ('stderr', self.stderr) + ] + + for name, stream in streams_to_close: + if stream is not None: + try: + if hasattr(stream, 'close'): + stream.close() + except (AttributeError, OSError, paramiko.ssh_exception.SSHException): + # Игнорируем ошибки при закрытии потоков + pass + finally: + setattr(self, name, None) + + # Закрываем SSH соединение + if self.s is not None: + try: + self.s.close() + except (AttributeError, OSError, paramiko.ssh_exception.SSHException): + # Игнорируем ошибки при закрытии соединения + pass + finally: + self.s = None + + + + +