Вынес все модули в отдельный каталог modules

This commit is contained in:
2026-02-09 22:24:39 +03:00
parent 5bbb585d9f
commit cf1cce3822
9 changed files with 26 additions and 8 deletions

13
modules/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Модули SSH клиента для миграции 1С
"""
from .ssh_base import SSHBase
from .postgresql import PostgreSQLOperations
from .c1_cluster import C1ClusterOperations
from .ssh import ssh
__all__ = ['SSHBase', 'PostgreSQLOperations', 'C1ClusterOperations', 'ssh']

400
modules/c1_cluster.py Normal file
View File

@@ -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}")

368
modules/postgresql.py Normal file
View File

@@ -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}")

49
modules/ssh.py Executable file
View File

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

213
modules/ssh_base.py Normal file
View File

@@ -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