Логи и конфиги переработаны, добавлен модуль ZFS и ведется работа с телеграм-ботом
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ lib/
|
|||||||
lib64
|
lib64
|
||||||
include/
|
include/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
logs/
|
||||||
|
|||||||
92
README.md
92
README.md
@@ -1,16 +1,92 @@
|
|||||||
# Проект: Автоматизация задач с использованием Cursor AI
|
# SSH Client — автоматизация миграции 1С, ZFS Backup
|
||||||
|
|
||||||

|
Централизованное управление удалёнными серверами через SSH: миграция 1С, PostgreSQL, ZFS Backup.
|
||||||
|
|
||||||
> Краткое описание проекта
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# или (с pyproject.toml):
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Все конфиги в каталоге `config/` (YAML):
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------------------------|--------------------------------------|
|
||||||
|
| `config/migration.yaml` | SSH, PostgreSQL, 1С, migration |
|
||||||
|
| `config/zfs_backup.yaml` | ZFS Backup (серверы, пулы) |
|
||||||
|
| `config/config_log.yaml` | Логирование (файл, Telegram, PRD v1.8) |
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── migration.yaml # Миграция 1С, примеры
|
||||||
|
├── zfs_backup.yaml # ZFS Backup
|
||||||
|
├── config_log.yaml # Логирование (файл, Telegram)
|
||||||
|
modules/
|
||||||
|
├── ssh_base.py # SSHBase — базовые SSH операции (connect, cmd, close)
|
||||||
|
├── ssh.py # SSHClient — объединённый клиент (SSH + Postgres + 1C)
|
||||||
|
├── logger.py # Единый логгер проекта
|
||||||
|
├── protocols.py # SSHProtocol, SSHOperationsBase — контракты модулей
|
||||||
|
├── postgresql.py # PostgreSQLOperations — операции с PostgreSQL
|
||||||
|
├── c1_cluster.py # C1ClusterOperations — операции с кластером 1С
|
||||||
|
└── zfs_backup_ops.py # ZFS Backup — снапшоты, репликация, очистка
|
||||||
|
```
|
||||||
|
|
||||||
|
**Паттерн использования:**
|
||||||
|
```python
|
||||||
|
from modules import SSHClient
|
||||||
|
|
||||||
|
client = SSHClient(hostname="host", port=22222, ...)
|
||||||
|
client.connect()
|
||||||
|
# PostgreSQL
|
||||||
|
bases = client.bases_list(srv_pgsql)
|
||||||
|
# 1С
|
||||||
|
client.set_c1_config(lxc_name, user, password)
|
||||||
|
client.base_info_update(...)
|
||||||
|
client.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сценарии
|
||||||
|
|
||||||
|
| Сценарий | Точка входа | Описание |
|
||||||
|
|------------|----------------------|-----------------------------------|
|
||||||
|
| Миграция 1С| `1c-migration.py` | Скрипт миграции баз |
|
||||||
|
| PostgreSQL | `example_postgresql.py` | Примеры: список баз, бэкап, restore |
|
||||||
|
| Кластер 1С | `example_c1_cluster.py` | Примеры: версия, базы, base_info_update |
|
||||||
|
| ZFS Backup | `zfs_backup.py` | Cron (ежедневно 20:00), CLI |
|
||||||
|
|
||||||
|
## ZFS Backup
|
||||||
|
|
||||||
|
Конфигурация: `config/zfs_backup.yaml` (формат v1.1 с `servers[].pools[]`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python zfs_backup.py --config=config/zfs_backup.yaml
|
||||||
|
# или (по умолчанию):
|
||||||
|
python zfs_backup.py
|
||||||
|
# после pip install -e .:
|
||||||
|
zfs-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Логи: настраиваются в `config/config_log.yaml` (по умолчанию `/var/log/zfs_backup.log`).
|
||||||
|
Telegram: при `telegram.enabled: true` логи дублируются в Telegram-группу (PRD v1.8).
|
||||||
|
|
||||||
|
## Рекомендации
|
||||||
|
|
||||||
|
- **SSHClient** — рекомендуемый класс (PEP8). **ssh** — alias для обратной совместимости.
|
||||||
|
- Конфиги в `config/`: `migration.yaml` (миграция, примеры), `zfs_backup.yaml` (ZFS).
|
||||||
|
- Новые операционные модули: наследовать от `SSHOperationsBase`, реализовать контракт `ssh: SSHProtocol`.
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
## 🚀 Работа с GIT:
|
|
||||||
```bash
|
```bash
|
||||||
lxc shell code
|
|
||||||
cd /root/lib/ssh_client
|
cd /root/lib/ssh_client
|
||||||
source bin/activate
|
|
||||||
git status
|
git status
|
||||||
git add .
|
git add .
|
||||||
git commit -a -m 'Реструктуризировал проект'
|
git commit -a -m 'Сообщение'
|
||||||
git push -u origin main
|
git push -u origin main
|
||||||
exit
|
```
|
||||||
|
|||||||
112
config.py
112
config.py
@@ -1,112 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
35
config.yaml
35
config.yaml
@@ -1,35 +0,0 @@
|
|||||||
# Конфигурация ZFS Backup v1.1
|
|
||||||
# Запуск: python zfs_backup.py --config=config.yaml
|
|
||||||
|
|
||||||
# Параметры SSH по умолчанию (опционально)
|
|
||||||
ssh_defaults:
|
|
||||||
port: 22222
|
|
||||||
username: root
|
|
||||||
pkey_file: /root/.ssh/id_rsa
|
|
||||||
host_keys: "~/.ssh/known_hosts"
|
|
||||||
|
|
||||||
# Список серверов для бэкапа
|
|
||||||
servers:
|
|
||||||
- name: gwo2.mps.cln.su
|
|
||||||
pools:
|
|
||||||
- source_pool: zp2
|
|
||||||
datasets:
|
|
||||||
- containers/smb2
|
|
||||||
target_pool: fast-backup
|
|
||||||
- source_pool: zp0
|
|
||||||
datasets:
|
|
||||||
- containers/www
|
|
||||||
- containers/voip
|
|
||||||
target_pool: fast-backup
|
|
||||||
snapshot_name: dd-mm-yyyy
|
|
||||||
retention_days: 90
|
|
||||||
|
|
||||||
# Пример второго сервера (раскомментируйте при необходимости):
|
|
||||||
# - name: backup-server2.example.com
|
|
||||||
# pools:
|
|
||||||
# - source_pool: tank
|
|
||||||
# datasets:
|
|
||||||
# - data/vms
|
|
||||||
# target_pool: backup
|
|
||||||
# snapshot_name: dd-mm-yyyy
|
|
||||||
# retention_days: 30
|
|
||||||
81
config/__init__.py
Normal file
81
config/__init__.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Загрузка конфигурации из YAML.
|
||||||
|
Файлы: config/migration.yaml, config/zfs_backup.yaml
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
CONFIG_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
DEFAULT_MIGRATION_CONFIG = os.path.join(CONFIG_DIR, "migration.yaml")
|
||||||
|
DEFAULT_ZFS_CONFIG = os.path.join(CONFIG_DIR, "zfs_backup.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml(path: str) -> Dict[str, Any]:
|
||||||
|
"""Загрузить YAML-файл в словарь."""
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Загрузка конфигурации миграции 1С (SSH, PostgreSQL, 1C, migration).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Путь к migration.yaml. По умолчанию — config/migration.yaml.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Секции ssh, postgresql, c1, migration, logging.
|
||||||
|
"""
|
||||||
|
path = config_path or os.environ.get("MIGRATION_CONFIG", DEFAULT_MIGRATION_CONFIG)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
# Относительно корня проекта (родитель config/)
|
||||||
|
root = os.path.dirname(CONFIG_DIR)
|
||||||
|
path = os.path.join(root, path)
|
||||||
|
data = _load_yaml(path)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"ssh": data.get("ssh", {}),
|
||||||
|
"postgresql": data.get("postgresql", {}),
|
||||||
|
"c1": data.get("c1", {}),
|
||||||
|
"migration": dict(data.get("migration", {})),
|
||||||
|
"logging": data.get("logging", {"level": "INFO"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
mig = config["migration"]
|
||||||
|
archive = mig.get("archive_bases_name", [])
|
||||||
|
if mig.get("restore_bases_name") is None:
|
||||||
|
mig["restore_bases_name"] = list(archive)
|
||||||
|
if mig.get("bases") is None:
|
||||||
|
mig["bases"] = list(archive)
|
||||||
|
|
||||||
|
def _norm_on_off(v: Any) -> str:
|
||||||
|
if v in (True, "true", "on", "1"):
|
||||||
|
return "on"
|
||||||
|
if v in (False, "false", "off", "0"):
|
||||||
|
return "off"
|
||||||
|
raise ValueError(f"Ожидается on/off, получено: {v!r}")
|
||||||
|
|
||||||
|
scheduled = _norm_on_off(mig.get("scheduled_jobs_deny", "off"))
|
||||||
|
sessions = _norm_on_off(mig.get("sessions_deny", "off"))
|
||||||
|
mig["scheduled_jobs_deny"] = scheduled
|
||||||
|
mig["sessions_deny"] = sessions
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def load_zfs_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Загрузка конфигурации ZFS Backup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Путь к zfs_backup.yaml. По умолчанию — config/zfs_backup.yaml.
|
||||||
|
"""
|
||||||
|
path = config_path or os.environ.get("ZFS_CONFIG", DEFAULT_ZFS_CONFIG)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
root = os.path.dirname(CONFIG_DIR)
|
||||||
|
path = os.path.join(root, path)
|
||||||
|
return _load_yaml(path)
|
||||||
32
config/config_log.yaml
Normal file
32
config/config_log.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Конфигурация логирования (PRD v1.8)
|
||||||
|
# Используется: zfs_backup.py, modules/logger.py
|
||||||
|
|
||||||
|
# Основные настройки
|
||||||
|
log_file: "/var/log/zfs_backup.log"
|
||||||
|
log_level: "INFO"
|
||||||
|
|
||||||
|
# Telegram интеграция
|
||||||
|
telegram:
|
||||||
|
enabled: true
|
||||||
|
bot_token: "8294703499:AAGfSNSxe9NOrh9wD7FeQcWax9yq1bGBqBY"
|
||||||
|
chat_id: "-5101685781"
|
||||||
|
|
||||||
|
allowed_users:
|
||||||
|
- 86018113
|
||||||
|
|
||||||
|
log_level: "INFO"
|
||||||
|
allowed_levels:
|
||||||
|
- "ERROR"
|
||||||
|
- "INFO"
|
||||||
|
- "WARNING"
|
||||||
|
|
||||||
|
retry:
|
||||||
|
attempts: 3
|
||||||
|
delay: 5
|
||||||
|
|
||||||
|
options:
|
||||||
|
parse_mode: "HTML"
|
||||||
|
disable_notification: false
|
||||||
|
max_message_length: 4096
|
||||||
|
|
||||||
|
enable_commands: true
|
||||||
41
config/migration.yaml
Normal file
41
config/migration.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Конфигурация миграции 1С, PostgreSQL, SSH
|
||||||
|
# Используется: 1c-migration.py, example_postgresql.py, example_c1_cluster.py
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
hostname: g.it.cln.su
|
||||||
|
port: 22222
|
||||||
|
username: root
|
||||||
|
pkey_file: /root/.ssh/id_rsa
|
||||||
|
host_keys: "~/.ssh/known_hosts"
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
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
|
||||||
|
|
||||||
|
c1:
|
||||||
|
lxc_container_name: c1
|
||||||
|
cluster_user: neon
|
||||||
|
cluster_password: "Pre$tige310582"
|
||||||
|
db_server: postgres.it.cln.su
|
||||||
|
db_user: postgres
|
||||||
|
db_password: PrestigePostgres
|
||||||
|
infobase_user: neon
|
||||||
|
infobase_password: "$F%G^H&J*K"
|
||||||
|
|
||||||
|
migration:
|
||||||
|
archive_bases_name:
|
||||||
|
- salon
|
||||||
|
- lmotor-ut
|
||||||
|
- staretail
|
||||||
|
- uran-ut
|
||||||
|
restore_bases_name: null # если null — используется archive_bases_name
|
||||||
|
bases: null # для примеров; если null — archive_bases_name
|
||||||
|
scheduled_jobs_deny: on
|
||||||
|
sessions_deny: off
|
||||||
23
config/zfs_backup.yaml
Normal file
23
config/zfs_backup.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Конфигурация ZFS Backup v1.1
|
||||||
|
# Запуск: python zfs_backup.py --config=config/zfs_backup.yaml
|
||||||
|
|
||||||
|
ssh_defaults:
|
||||||
|
port: 22222
|
||||||
|
username: root
|
||||||
|
pkey_file: /root/.ssh/id_rsa
|
||||||
|
host_keys: "~/.ssh/known_hosts"
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- name: gwo2.mps.cln.su
|
||||||
|
pools:
|
||||||
|
- source_pool: zp2
|
||||||
|
datasets:
|
||||||
|
- containers/smb2
|
||||||
|
target_pool: fast-backup
|
||||||
|
- source_pool: zp0
|
||||||
|
datasets:
|
||||||
|
- containers/www
|
||||||
|
- containers/voip
|
||||||
|
target_pool: fast-backup
|
||||||
|
snapshot_name: dd-mm-yyyy
|
||||||
|
retention_days: 90
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Примеры использования модуля c1_cluster.py новым способом
|
Примеры использования модуля c1_cluster через единый SSHClient.
|
||||||
Демонстрирует прямое использование C1ClusterOperations без основного класса ssh
|
Паттерн: SSHClient + connect() + set_c1_config() + операции + close()
|
||||||
"""
|
"""
|
||||||
import config
|
import config
|
||||||
from modules.ssh_base import SSHBase
|
from modules import SSHClient
|
||||||
from modules.c1_cluster import C1ClusterOperations
|
|
||||||
|
|
||||||
|
|
||||||
def example_basic_usage():
|
def example_basic_usage():
|
||||||
@@ -17,37 +16,27 @@ def example_basic_usage():
|
|||||||
print("Пример 1: Базовое использование модуля c1_cluster")
|
print("Пример 1: Базовое использование модуля c1_cluster")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
# Загружаем конфигурацию
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
client = SSHClient(
|
||||||
# Создаем SSH подключение
|
|
||||||
ssh_client = SSHBase(
|
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file'],
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
host_keys=cfg['ssh']['host_keys']
|
host_keys=cfg['ssh']['host_keys'],
|
||||||
|
)
|
||||||
|
client.connect()
|
||||||
|
client.set_c1_config(
|
||||||
|
cfg['c1']['lxc_container_name'],
|
||||||
|
cfg['c1']['cluster_user'],
|
||||||
|
cfg['c1']['cluster_password'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Подключаемся
|
version = client.cluster_version()
|
||||||
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}")
|
print(f"Версия кластера 1С: {version}")
|
||||||
|
cluster_id = client.cluster_id()
|
||||||
# Получаем ID кластера
|
|
||||||
cluster_id = c1_cluster.cluster_id()
|
|
||||||
print(f"ID кластера 1С: {cluster_id}")
|
print(f"ID кластера 1С: {cluster_id}")
|
||||||
|
|
||||||
# Закрываем соединение
|
client.close()
|
||||||
ssh_client.close()
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -61,22 +50,21 @@ def example_get_base_list():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
|
)
|
||||||
|
client.connect()
|
||||||
|
client.set_c1_config(
|
||||||
|
cfg['c1']['lxc_container_name'],
|
||||||
|
cfg['c1']['cluster_user'],
|
||||||
|
cfg['c1']['cluster_password'],
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
|
||||||
|
|
||||||
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
bases = client.base_list()
|
||||||
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)}")
|
print(f"Найдено баз данных: {len(bases)}")
|
||||||
for base in bases:
|
for base in bases:
|
||||||
@@ -85,7 +73,7 @@ def example_get_base_list():
|
|||||||
base_id = base['id'][0] if base.get('id') and len(base['id']) > 0 else 'N/A'
|
base_id = base['id'][0] if base.get('id') and len(base['id']) > 0 else 'N/A'
|
||||||
print(f" - {base_name} (ID: {base_id})")
|
print(f" - {base_name} (ID: {base_id})")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -99,28 +87,27 @@ def example_get_base_info():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
|
)
|
||||||
|
client.connect()
|
||||||
|
client.set_c1_config(
|
||||||
|
cfg['c1']['lxc_container_name'],
|
||||||
|
cfg['c1']['cluster_user'],
|
||||||
|
cfg['c1']['cluster_password'],
|
||||||
)
|
)
|
||||||
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_user = cfg['c1']['infobase_user']
|
||||||
infobase_password = cfg['c1']['infobase_password']
|
infobase_password = cfg['c1']['infobase_password']
|
||||||
c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password)
|
|
||||||
|
|
||||||
# Получаем список баз из конфигурации
|
|
||||||
bases_to_process = cfg['migration'].get('bases', [])
|
bases_to_process = cfg['migration'].get('bases', [])
|
||||||
|
|
||||||
if not bases_to_process:
|
if not bases_to_process:
|
||||||
print("Список баз для обработки не указан в конфигурации (migration.bases)")
|
print("Список баз для обработки не указан в конфигурации (migration.bases)")
|
||||||
ssh_client.close()
|
client.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:")
|
print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:")
|
||||||
@@ -134,12 +121,12 @@ def example_get_base_info():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Получаем ID базы
|
# Получаем ID базы
|
||||||
base_id = c1_cluster.base_id(base_name)
|
base_id = client.base_id(base_name)
|
||||||
if base_id:
|
if base_id:
|
||||||
print(f"ID базы данных '{base_name}': {base_id}")
|
print(f"ID базы данных '{base_name}': {base_id}")
|
||||||
|
|
||||||
# Получаем полную информацию о базе
|
# Получаем полную информацию о базе
|
||||||
base_info = c1_cluster.base_info(base_name, infobase_user, infobase_password)
|
base_info = client.base_info(base_name, infobase_user, infobase_password)
|
||||||
|
|
||||||
if base_info:
|
if base_info:
|
||||||
print(f"\nИнформация о базе данных '{base_name}':")
|
print(f"\nИнформация о базе данных '{base_name}':")
|
||||||
@@ -154,7 +141,7 @@ def example_get_base_info():
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -168,13 +155,14 @@ def example_update_base_info():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
||||||
lxc_container = cfg['c1']['lxc_container_name']
|
lxc_container = cfg['c1']['lxc_container_name']
|
||||||
@@ -182,7 +170,7 @@ def example_update_base_info():
|
|||||||
cluster_password = cfg['c1']['cluster_password']
|
cluster_password = cfg['c1']['cluster_password']
|
||||||
infobase_user = cfg['c1']['infobase_user']
|
infobase_user = cfg['c1']['infobase_user']
|
||||||
infobase_password = cfg['c1']['infobase_password']
|
infobase_password = cfg['c1']['infobase_password']
|
||||||
c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password)
|
client.set_c1_config(lxc_container, cluster_user, cluster_password)
|
||||||
|
|
||||||
# Параметры базы данных PostgreSQL
|
# Параметры базы данных PostgreSQL
|
||||||
db_server = cfg['c1']['db_server']
|
db_server = cfg['c1']['db_server']
|
||||||
@@ -199,7 +187,7 @@ def example_update_base_info():
|
|||||||
|
|
||||||
if not bases_to_process:
|
if not bases_to_process:
|
||||||
print("Список баз для обработки не указан в конфигурации (migration.bases)")
|
print("Список баз для обработки не указан в конфигурации (migration.bases)")
|
||||||
ssh_client.close()
|
client.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:")
|
print(f"Обработка {len(bases_to_process)} баз данных из конфигурации:")
|
||||||
@@ -231,7 +219,7 @@ def example_update_base_info():
|
|||||||
print(f" Запрет сеансов: {sessions_deny}")
|
print(f" Запрет сеансов: {sessions_deny}")
|
||||||
|
|
||||||
# Обновляем информацию о базе
|
# Обновляем информацию о базе
|
||||||
updated_base_id = c1_cluster.base_info_update(
|
updated_base_id = client.base_info_update(
|
||||||
base_name, db_server, actual_db_name, db_user, db_password,
|
base_name, db_server, actual_db_name, db_user, db_password,
|
||||||
infobase_user, infobase_password,
|
infobase_user, infobase_password,
|
||||||
scheduled_jobs_deny, sessions_deny
|
scheduled_jobs_deny, sessions_deny
|
||||||
@@ -246,7 +234,7 @@ def example_update_base_info():
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -260,29 +248,30 @@ def example_workflow():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
||||||
lxc_container = cfg['c1']['lxc_container_name']
|
lxc_container = cfg['c1']['lxc_container_name']
|
||||||
cluster_user = cfg['c1']['cluster_user']
|
cluster_user = cfg['c1']['cluster_user']
|
||||||
cluster_password = cfg['c1']['cluster_password']
|
cluster_password = cfg['c1']['cluster_password']
|
||||||
c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password)
|
client.set_c1_config(lxc_container, cluster_user, cluster_password)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Шаг 1: Получаем версию кластера
|
# Шаг 1: Получаем версию кластера
|
||||||
print("Шаг 1: Получение версии кластера...")
|
print("Шаг 1: Получение версии кластера...")
|
||||||
version = c1_cluster.cluster_version()
|
version = client.cluster_version()
|
||||||
print(f" Версия: {version}")
|
print(f" Версия: {version}")
|
||||||
|
|
||||||
# Шаг 2: Запускаем демон кластера
|
# Шаг 2: Запускаем демон кластера
|
||||||
print("\nШаг 2: Запуск демона кластера...")
|
print("\nШаг 2: Запуск демона кластера...")
|
||||||
err = c1_cluster.cluster_daemon_start()
|
err = client.cluster_daemon_start()
|
||||||
if err:
|
if err:
|
||||||
print(f" Предупреждение: {err}")
|
print(f" Предупреждение: {err}")
|
||||||
else:
|
else:
|
||||||
@@ -290,12 +279,12 @@ def example_workflow():
|
|||||||
|
|
||||||
# Шаг 3: Получаем ID кластера
|
# Шаг 3: Получаем ID кластера
|
||||||
print("\nШаг 3: Получение ID кластера...")
|
print("\nШаг 3: Получение ID кластера...")
|
||||||
cluster_id = c1_cluster.cluster_id()
|
cluster_id = client.cluster_id()
|
||||||
print(f" ID кластера: {cluster_id}")
|
print(f" ID кластера: {cluster_id}")
|
||||||
|
|
||||||
# Шаг 4: Получаем список баз данных
|
# Шаг 4: Получаем список баз данных
|
||||||
print("\nШаг 4: Получение списка баз данных...")
|
print("\nШаг 4: Получение списка баз данных...")
|
||||||
bases = c1_cluster.base_list()
|
bases = client.base_list()
|
||||||
print(f" Найдено баз: {len(bases)}")
|
print(f" Найдено баз: {len(bases)}")
|
||||||
|
|
||||||
# Шаг 5: Для каждой базы получаем ID
|
# Шаг 5: Для каждой базы получаем ID
|
||||||
@@ -303,13 +292,13 @@ def example_workflow():
|
|||||||
for base in bases:
|
for base in bases:
|
||||||
if base.get('name') and len(base['name']) > 0:
|
if base.get('name') and len(base['name']) > 0:
|
||||||
base_name = base['name'][0]
|
base_name = base['name'][0]
|
||||||
base_id = c1_cluster.base_id(base_name)
|
base_id = client.base_id(base_name)
|
||||||
print(f" {base_name}: {base_id}")
|
print(f" {base_name}: {base_id}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка: {e}")
|
print(f"Ошибка: {e}")
|
||||||
finally:
|
finally:
|
||||||
ssh_client.close()
|
client.close()
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
@@ -323,28 +312,28 @@ def example_context_manager_style():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
ssh_client = None
|
client = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file']
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
# Создаем экземпляр модуля 1С кластера с параметрами из конфига
|
||||||
lxc_container = cfg['c1']['lxc_container_name']
|
lxc_container = cfg['c1']['lxc_container_name']
|
||||||
cluster_user = cfg['c1']['cluster_user']
|
cluster_user = cfg['c1']['cluster_user']
|
||||||
cluster_password = cfg['c1']['cluster_password']
|
cluster_password = cfg['c1']['cluster_password']
|
||||||
c1_cluster = C1ClusterOperations(ssh_client, lxc_container, cluster_user, cluster_password)
|
client.set_c1_config(lxc_container, cluster_user, cluster_password)
|
||||||
|
|
||||||
# Получаем версию и список баз
|
# Получаем версию и список баз
|
||||||
version = c1_cluster.cluster_version()
|
version = client.cluster_version()
|
||||||
print(f"Версия кластера: {version}")
|
print(f"Версия кластера: {version}")
|
||||||
|
|
||||||
bases = c1_cluster.base_list()
|
bases = client.base_list()
|
||||||
print(f"Список баз данных ({len(bases)} шт.):")
|
print(f"Список баз данных ({len(bases)} шт.):")
|
||||||
for base in bases[:3]: # Показываем только первые 3
|
for base in bases[:3]: # Показываем только первые 3
|
||||||
if base.get('name') and len(base['name']) > 0:
|
if base.get('name') and len(base['name']) > 0:
|
||||||
@@ -353,8 +342,8 @@ def example_context_manager_style():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Произошла ошибка: {e}")
|
print(f"Произошла ошибка: {e}")
|
||||||
finally:
|
finally:
|
||||||
if ssh_client:
|
if client:
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("Соединение закрыто")
|
print("Соединение закрыто")
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
@@ -362,7 +351,7 @@ def example_context_manager_style():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("Примеры использования модуля c1_cluster.py")
|
print("Примеры использования модуля client.py")
|
||||||
print("=" * 60 + "\n")
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
# Раскомментируйте нужные примеры для запуска
|
# Раскомментируйте нужные примеры для запуска
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Примеры использования модуля postgresql.py новым способом
|
Примеры использования модуля postgresql через единый SSHClient.
|
||||||
Демонстрирует прямое использование PostgreSQLOperations без основного класса ssh
|
Паттерн: SSHClient + connect() + операции + close()
|
||||||
"""
|
"""
|
||||||
import config
|
import config
|
||||||
from modules.ssh_base import SSHBase
|
from modules import SSHClient
|
||||||
from modules.postgresql import PostgreSQLOperations
|
|
||||||
|
|
||||||
|
|
||||||
def example_basic_usage():
|
def example_basic_usage():
|
||||||
@@ -17,35 +16,25 @@ def example_basic_usage():
|
|||||||
print("Пример 1: Базовое использование модуля postgresql")
|
print("Пример 1: Базовое использование модуля postgresql")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
# Загружаем конфигурацию
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
client = SSHClient(
|
||||||
# Создаем SSH подключение
|
|
||||||
ssh_client = SSHBase(
|
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file'],
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
host_keys=cfg['ssh']['host_keys']
|
host_keys=cfg['ssh']['host_keys'],
|
||||||
)
|
)
|
||||||
|
client.connect()
|
||||||
|
|
||||||
# Подключаемся
|
|
||||||
ssh_client.connect()
|
|
||||||
|
|
||||||
# Создаем экземпляр модуля PostgreSQL
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
# Получаем список баз данных
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
bases = pg.bases_list(srv_pgsql)
|
bases = client.bases_list(srv_pgsql)
|
||||||
|
|
||||||
print(f"Сервер PostgreSQL: {srv_pgsql}")
|
print(f"Сервер PostgreSQL: {srv_pgsql}")
|
||||||
print(f"Найдено баз данных: {len(bases)}")
|
print(f"Найдено баз данных: {len(bases)}")
|
||||||
for base in bases[:5]: # Показываем первые 5
|
for base in bases[:5]: # Показываем первые 5
|
||||||
print(f" - {base}")
|
print(f" - {base}")
|
||||||
|
|
||||||
# Закрываем соединение
|
client.close()
|
||||||
ssh_client.close()
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -58,28 +47,23 @@ def example_get_bases_list():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
client = SSHClient(
|
||||||
ssh_client = SSHBase(
|
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
bases = client.bases_list(srv_pgsql)
|
||||||
# Получаем список баз данных
|
|
||||||
bases = pg.bases_list(srv_pgsql)
|
|
||||||
|
|
||||||
print(f"Сервер: {srv_pgsql}")
|
print(f"Сервер: {srv_pgsql}")
|
||||||
print(f"Всего баз данных: {len(bases)}")
|
print(f"Всего баз данных: {len(bases)}")
|
||||||
print("\nСписок баз данных:")
|
print("\nСписок баз данных:")
|
||||||
pg.bases_list_print(srv_pgsql)
|
client.bases_list_print(srv_pgsql)
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -92,27 +76,24 @@ def example_get_bases_size():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
client = SSHClient(
|
||||||
ssh_client = SSHBase(
|
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
|
||||||
print(f"Сервер: {srv_pgsql}")
|
print(f"Сервер: {srv_pgsql}")
|
||||||
print("\nРазмеры баз данных:")
|
print("\nРазмеры баз данных:")
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
print(f"{'База данных':<30} | Размер")
|
print(f"{'База данных':<30} | Размер")
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
pg.bases_size_print(srv_pgsql)
|
client.bases_size_print(srv_pgsql)
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -125,28 +106,24 @@ def example_backup_single_base():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
client = SSHClient(
|
||||||
ssh_client = SSHBase(
|
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
bases = client.bases_list(srv_pgsql)
|
||||||
# Получаем список баз и берем первую для примера
|
|
||||||
bases = pg.bases_list(srv_pgsql)
|
|
||||||
if bases:
|
if bases:
|
||||||
base_name = bases[0]
|
base_name = bases[0]
|
||||||
print(f"Создание бэкапа базы данных: {base_name}")
|
print(f"Создание бэкапа базы данных: {base_name}")
|
||||||
print(f"Сервер: {srv_pgsql}")
|
print(f"Сервер: {srv_pgsql}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = pg.bases_backup(srv_pgsql, base_name)
|
results = client.bases_backup(srv_pgsql, base_name)
|
||||||
for result in results:
|
for result in results:
|
||||||
if result['success']:
|
if result['success']:
|
||||||
print(f"✓ Бэкап базы '{result['base']}' создан успешно")
|
print(f"✓ Бэкап базы '{result['base']}' создан успешно")
|
||||||
@@ -157,7 +134,7 @@ def example_backup_single_base():
|
|||||||
else:
|
else:
|
||||||
print("Базы данных не найдены")
|
print("Базы данных не найдены")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -171,18 +148,16 @@ def example_backup_multiple_bases():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
|
||||||
# Получаем список баз из конфигурации для примеров
|
# Получаем список баз из конфигурации для примеров
|
||||||
bases_to_backup = cfg['migration'].get('bases', [])
|
bases_to_backup = cfg['migration'].get('bases', [])
|
||||||
|
|
||||||
@@ -202,7 +177,7 @@ def example_backup_multiple_bases():
|
|||||||
if bases_to_backup:
|
if bases_to_backup:
|
||||||
for base_name in bases_to_backup:
|
for base_name in bases_to_backup:
|
||||||
print(f"\nСоздание бэкапа базы: {base_name}")
|
print(f"\nСоздание бэкапа базы: {base_name}")
|
||||||
results = pg.bases_backup(srv_pgsql, base_name)
|
results = client.bases_backup(srv_pgsql, base_name)
|
||||||
for result in results:
|
for result in results:
|
||||||
if result['success']:
|
if result['success']:
|
||||||
print(f" ✓ Бэкап базы '{result['base']}' создан успешно")
|
print(f" ✓ Бэкап базы '{result['base']}' создан успешно")
|
||||||
@@ -210,7 +185,7 @@ def example_backup_multiple_bases():
|
|||||||
print(f" ✗ Ошибка: {result['stderr']}")
|
print(f" ✗ Ошибка: {result['stderr']}")
|
||||||
else:
|
else:
|
||||||
# Бэкап всех баз
|
# Бэкап всех баз
|
||||||
results = pg.bases_backup(srv_pgsql, None)
|
results = client.bases_backup(srv_pgsql, None)
|
||||||
print(f"\nОбработано баз: {len(results)}")
|
print(f"\nОбработано баз: {len(results)}")
|
||||||
successful = sum(1 for r in results if r['success'])
|
successful = sum(1 for r in results if r['success'])
|
||||||
failed = len(results) - successful
|
failed = len(results) - successful
|
||||||
@@ -222,7 +197,7 @@ def example_backup_multiple_bases():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка: {e}")
|
print(f"Ошибка: {e}")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -236,28 +211,26 @@ def example_backup_all_bases():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
|
||||||
print(f"Сервер: {srv_pgsql}")
|
print(f"Сервер: {srv_pgsql}")
|
||||||
print("Создание бэкапа всех баз данных...")
|
print("Создание бэкапа всех баз данных...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = pg.bases_backup_all(srv_pgsql)
|
result = client.bases_backup_all(srv_pgsql)
|
||||||
print("✓ Бэкап всех баз данных завершен успешно")
|
print("✓ Бэкап всех баз данных завершен успешно")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Ошибка при создании бэкапа: {e}")
|
print(f"✗ Ошибка при создании бэкапа: {e}")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -271,15 +244,14 @@ def example_create_and_drop_base():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['restore_server']
|
srv_pgsql = cfg['postgresql']['restore_server']
|
||||||
test_base_name = "test_base_example"
|
test_base_name = "test_base_example"
|
||||||
@@ -290,28 +262,28 @@ def example_create_and_drop_base():
|
|||||||
try:
|
try:
|
||||||
# Создаем базу данных
|
# Создаем базу данных
|
||||||
print("\n1. Создание базы данных...")
|
print("\n1. Создание базы данных...")
|
||||||
pg.base_create(srv_pgsql, test_base_name)
|
client.base_create(srv_pgsql, test_base_name)
|
||||||
print(f"✓ База данных '{test_base_name}' создана")
|
print(f"✓ База данных '{test_base_name}' создана")
|
||||||
|
|
||||||
# Проверяем, что база создана
|
# Проверяем, что база создана
|
||||||
bases = pg.bases_list(srv_pgsql)
|
bases = client.bases_list(srv_pgsql)
|
||||||
if test_base_name in bases:
|
if test_base_name in bases:
|
||||||
print(f"✓ База '{test_base_name}' найдена в списке баз")
|
print(f"✓ База '{test_base_name}' найдена в списке баз")
|
||||||
|
|
||||||
# Удаляем базу данных
|
# Удаляем базу данных
|
||||||
print("\n2. Удаление базы данных...")
|
print("\n2. Удаление базы данных...")
|
||||||
pg.base_drop(srv_pgsql, test_base_name)
|
client.base_drop(srv_pgsql, test_base_name)
|
||||||
print(f"✓ База данных '{test_base_name}' удалена")
|
print(f"✓ База данных '{test_base_name}' удалена")
|
||||||
|
|
||||||
# Проверяем, что база удалена
|
# Проверяем, что база удалена
|
||||||
bases = pg.bases_list(srv_pgsql)
|
bases = client.bases_list(srv_pgsql)
|
||||||
if test_base_name not in bases:
|
if test_base_name not in bases:
|
||||||
print(f"✓ База '{test_base_name}' отсутствует в списке баз")
|
print(f"✓ База '{test_base_name}' отсутствует в списке баз")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Ошибка: {e}")
|
print(f"✗ Ошибка: {e}")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -325,15 +297,14 @@ def example_restore_base():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
archive_server = cfg['postgresql']['archive_server']
|
archive_server = cfg['postgresql']['archive_server']
|
||||||
restore_server = cfg['postgresql']['restore_server']
|
restore_server = cfg['postgresql']['restore_server']
|
||||||
@@ -358,19 +329,19 @@ def example_restore_base():
|
|||||||
# Сначала удаляем старую базу, если существует
|
# Сначала удаляем старую базу, если существует
|
||||||
print(f"\n1. Удаление старой базы '{restore_base}' (если существует)...")
|
print(f"\n1. Удаление старой базы '{restore_base}' (если существует)...")
|
||||||
try:
|
try:
|
||||||
pg.base_drop(restore_server, restore_base)
|
client.base_drop(restore_server, restore_base)
|
||||||
print(f" ✓ Старая база удалена")
|
print(f" ✓ Старая база удалена")
|
||||||
except Exception:
|
except Exception:
|
||||||
print(f" База не существует или уже удалена")
|
print(f" База не существует или уже удалена")
|
||||||
|
|
||||||
# Создаем новую базу
|
# Создаем новую базу
|
||||||
print(f"\n2. Создание новой базы '{restore_base}'...")
|
print(f"\n2. Создание новой базы '{restore_base}'...")
|
||||||
pg.base_create(restore_server, restore_base)
|
client.base_create(restore_server, restore_base)
|
||||||
print(f" ✓ База создана")
|
print(f" ✓ База создана")
|
||||||
|
|
||||||
# Восстанавливаем из бэкапа
|
# Восстанавливаем из бэкапа
|
||||||
print(f"\n3. Восстановление из бэкапа...")
|
print(f"\n3. Восстановление из бэкапа...")
|
||||||
pg.base_restore(
|
client.base_restore(
|
||||||
archive_server, restore_server, backup_date,
|
archive_server, restore_server, backup_date,
|
||||||
archive_base, restore_base, extra
|
archive_base, restore_base, extra
|
||||||
)
|
)
|
||||||
@@ -379,7 +350,7 @@ def example_restore_base():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ✗ Ошибка при восстановлении: {e}")
|
print(f" ✗ Ошибка при восстановлении: {e}")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -393,15 +364,14 @@ def example_manage_backups():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
backup_path = f'/backup/pgsql/{srv_pgsql}'
|
backup_path = f'/backup/pgsql/{srv_pgsql}'
|
||||||
@@ -413,7 +383,7 @@ def example_manage_backups():
|
|||||||
try:
|
try:
|
||||||
# Получаем список старых директорий
|
# Получаем список старых директорий
|
||||||
print("\n1. Поиск старых бэкапов...")
|
print("\n1. Поиск старых бэкапов...")
|
||||||
old_dirs = pg.file_list(backup_path, days_old)
|
old_dirs = client.file_list(backup_path, days_old)
|
||||||
if old_dirs.strip():
|
if old_dirs.strip():
|
||||||
dirs_list = [d.strip() for d in old_dirs.split('\n') if d.strip()]
|
dirs_list = [d.strip() for d in old_dirs.split('\n') if d.strip()]
|
||||||
print(f"Найдено директорий для удаления: {len(dirs_list)}")
|
print(f"Найдено директорий для удаления: {len(dirs_list)}")
|
||||||
@@ -424,13 +394,13 @@ def example_manage_backups():
|
|||||||
|
|
||||||
# Удаляем старые бэкапы
|
# Удаляем старые бэкапы
|
||||||
print(f"\n2. Удаление бэкапов старше {days_old} дней...")
|
print(f"\n2. Удаление бэкапов старше {days_old} дней...")
|
||||||
pg.delete_old_backups(backup_path, days_old)
|
client.delete_old_backups(backup_path, days_old)
|
||||||
print("✓ Удаление завершено")
|
print("✓ Удаление завершено")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"✗ Ошибка: {e}")
|
print(f"✗ Ошибка: {e}")
|
||||||
|
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -444,27 +414,25 @@ def example_full_workflow():
|
|||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
|
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file'],
|
||||||
|
host_keys=cfg['ssh'].get('host_keys', '~/.ssh/known_hosts'),
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Шаг 1: Получаем список баз
|
# Шаг 1: Получаем список баз
|
||||||
print("Шаг 1: Получение списка баз данных...")
|
print("Шаг 1: Получение списка баз данных...")
|
||||||
bases = pg.bases_list(srv_pgsql)
|
bases = client.bases_list(srv_pgsql)
|
||||||
print(f" Найдено баз: {len(bases)}")
|
print(f" Найдено баз: {len(bases)}")
|
||||||
|
|
||||||
# Шаг 2: Получаем размеры баз
|
# Шаг 2: Получаем размеры баз
|
||||||
print("\nШаг 2: Получение размеров баз данных...")
|
print("\nШаг 2: Получение размеров баз данных...")
|
||||||
sizes = pg.bases_size(srv_pgsql)
|
sizes = client.bases_size(srv_pgsql)
|
||||||
print(f" Получены размеры для {len(sizes)} баз")
|
print(f" Получены размеры для {len(sizes)} баз")
|
||||||
for size_info in sizes[:3]: # Показываем первые 3
|
for size_info in sizes[:3]: # Показываем первые 3
|
||||||
base_name = size_info[0]
|
base_name = size_info[0]
|
||||||
@@ -474,7 +442,7 @@ def example_full_workflow():
|
|||||||
# Шаг 3: Создаем бэкап первой базы (если есть)
|
# Шаг 3: Создаем бэкап первой базы (если есть)
|
||||||
if bases:
|
if bases:
|
||||||
print(f"\nШаг 3: Создание бэкапа базы '{bases[0]}'...")
|
print(f"\nШаг 3: Создание бэкапа базы '{bases[0]}'...")
|
||||||
results = pg.bases_backup(srv_pgsql, bases[0])
|
results = client.bases_backup(srv_pgsql, bases[0])
|
||||||
for result in results:
|
for result in results:
|
||||||
if result['success']:
|
if result['success']:
|
||||||
print(f" ✓ Бэкап создан успешно")
|
print(f" ✓ Бэкап создан успешно")
|
||||||
@@ -484,7 +452,7 @@ def example_full_workflow():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка: {e}")
|
print(f"Ошибка: {e}")
|
||||||
finally:
|
finally:
|
||||||
ssh_client.close()
|
client.close()
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
@@ -498,23 +466,21 @@ def example_context_manager_style():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
cfg = config.get_config()
|
cfg = config.get_config()
|
||||||
ssh_client = None
|
client = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ssh_client = SSHBase(
|
client = SSHClient(
|
||||||
hostname=cfg['ssh']['hostname'],
|
hostname=cfg['ssh']['hostname'],
|
||||||
port=cfg['ssh']['port'],
|
port=cfg['ssh']['port'],
|
||||||
username=cfg['ssh']['username'],
|
username=cfg['ssh']['username'],
|
||||||
pkey_file=cfg['ssh']['pkey_file']
|
pkey_file=cfg['ssh']['pkey_file']
|
||||||
)
|
)
|
||||||
ssh_client.connect()
|
client.connect()
|
||||||
|
|
||||||
pg = PostgreSQLOperations(ssh_client)
|
|
||||||
|
|
||||||
srv_pgsql = cfg['postgresql']['archive_server']
|
srv_pgsql = cfg['postgresql']['archive_server']
|
||||||
|
|
||||||
# Получаем список баз и их размеры
|
# Получаем список баз и их размеры
|
||||||
bases = pg.bases_list(srv_pgsql)
|
bases = client.bases_list(srv_pgsql)
|
||||||
print(f"Сервер: {srv_pgsql}")
|
print(f"Сервер: {srv_pgsql}")
|
||||||
print(f"Найдено баз данных: {len(bases)}")
|
print(f"Найдено баз данных: {len(bases)}")
|
||||||
|
|
||||||
@@ -526,8 +492,8 @@ def example_context_manager_style():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Произошла ошибка: {e}")
|
print(f"Произошла ошибка: {e}")
|
||||||
finally:
|
finally:
|
||||||
if ssh_client:
|
if client:
|
||||||
ssh_client.close()
|
client.close()
|
||||||
print("\nСоединение закрыто")
|
print("\nСоединение закрыто")
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|||||||
@@ -7,7 +7,16 @@
|
|||||||
from .ssh_base import SSHBase
|
from .ssh_base import SSHBase
|
||||||
from .postgresql import PostgreSQLOperations
|
from .postgresql import PostgreSQLOperations
|
||||||
from .c1_cluster import C1ClusterOperations
|
from .c1_cluster import C1ClusterOperations
|
||||||
from .ssh import ssh
|
from .ssh import SSHClient, ssh
|
||||||
|
from .protocols import SSHProtocol, SSHOperationsBase
|
||||||
|
|
||||||
__all__ = ['SSHBase', 'PostgreSQLOperations', 'C1ClusterOperations', 'ssh']
|
__all__ = [
|
||||||
|
'SSHBase',
|
||||||
|
'SSHClient',
|
||||||
|
'ssh',
|
||||||
|
'PostgreSQLOperations',
|
||||||
|
'C1ClusterOperations',
|
||||||
|
'SSHProtocol',
|
||||||
|
'SSHOperationsBase',
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -6,27 +6,30 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
from .logger import get_logger
|
||||||
|
from .protocols import SSHOperationsBase
|
||||||
|
|
||||||
|
logger = get_logger("c1_cluster")
|
||||||
|
|
||||||
|
|
||||||
class C1ClusterOperations:
|
class C1ClusterOperations(SSHOperationsBase):
|
||||||
"""
|
"""
|
||||||
Класс для операций с кластером 1С через SSH
|
Класс для операций с кластером 1С через SSH.
|
||||||
|
Реализует контракт операционного модуля (ssh: SSHProtocol).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ssh_client, srv_1c: str = "", c1_claster_user: str = "", c1_claster_pass: str = ""):
|
def __init__(self, ssh_client, srv_1c: str = "", c1_claster_user: str = "", c1_claster_pass: str = ""):
|
||||||
"""
|
"""
|
||||||
Инициализация модуля 1С кластера
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ssh_client: Экземпляр SSHBase для выполнения команд
|
ssh_client: Экземпляр, реализующий SSHProtocol (SSHBase, SSHClient).
|
||||||
srv_1c: Имя LXC контейнера с 1С
|
srv_1c: Имя LXC контейнера с 1С
|
||||||
c1_claster_user: Пользователь кластера 1С
|
c1_claster_user: Пользователь кластера 1С
|
||||||
c1_claster_pass: Пароль кластера 1С
|
c1_claster_pass: Пароль кластера 1С
|
||||||
"""
|
"""
|
||||||
self.ssh = ssh_client
|
super().__init__(ssh_client)
|
||||||
self.srv_1c = srv_1c
|
self.srv_1c: str = srv_1c
|
||||||
self.c1_claster_user = c1_claster_user
|
self.c1_claster_user: str = c1_claster_user
|
||||||
self.c1_claster_pass = c1_claster_pass
|
self.c1_claster_pass: str = c1_claster_pass
|
||||||
|
|
||||||
def set_srv_1c(self, srv_1c: str) -> None:
|
def set_srv_1c(self, srv_1c: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
283
modules/logger.py
Normal file
283
modules/logger.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Единый логгер для проекта SSH Client (PRD v1.8).
|
||||||
|
Поддержка файла, консоли и Telegram через TelegramHandler.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
# Имя корневого логгера проекта
|
||||||
|
ROOT_LOGGER_NAME = "ssh_client"
|
||||||
|
|
||||||
|
# Общий формат логов
|
||||||
|
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
# Кастомный уровень STATUS (между INFO и WARNING)
|
||||||
|
STATUS_LEVEL = 25
|
||||||
|
logging.addLevelName(STATUS_LEVEL, "STATUS")
|
||||||
|
|
||||||
|
|
||||||
|
def status(self, message: str, *args, **kwargs) -> None:
|
||||||
|
if self.isEnabledFor(STATUS_LEVEL):
|
||||||
|
self._log(STATUS_LEVEL, message, args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
logging.Logger.status = status # type: ignore
|
||||||
|
|
||||||
|
_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramHandler(logging.Handler):
|
||||||
|
"""
|
||||||
|
Отправляет логи в Telegram через очередь (асинхронно).
|
||||||
|
Retry при ошибках, разбивка длинных сообщений, /start для верификации.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._config = config
|
||||||
|
self._queue: queue.Queue = queue.Queue(maxsize=1000)
|
||||||
|
self._bot_token = config.get("bot_token", "")
|
||||||
|
self._chat_id = str(config.get("chat_id", ""))
|
||||||
|
self._allowed_users: List[Any] = config.get("allowed_users", [])
|
||||||
|
self._allowed_levels: List[str] = [str(x).upper() for x in config.get("allowed_levels", ["ERROR"])]
|
||||||
|
retry = config.get("retry", {})
|
||||||
|
self._retry_attempts = retry.get("attempts", 3)
|
||||||
|
self._retry_delay = retry.get("delay", 5)
|
||||||
|
opts = config.get("options", {})
|
||||||
|
self._max_length = opts.get("max_message_length", 4096)
|
||||||
|
self._parse_mode = opts.get("parse_mode", "HTML")
|
||||||
|
self._disable_notification = opts.get("disable_notification", False)
|
||||||
|
self._enable_commands = config.get("enable_commands", False)
|
||||||
|
self._stop = threading.Event()
|
||||||
|
self._worker = threading.Thread(target=self._process_queue, daemon=True)
|
||||||
|
self._worker.start()
|
||||||
|
if self._enable_commands:
|
||||||
|
self._poll_thread = threading.Thread(target=self._poll_commands, daemon=True)
|
||||||
|
self._poll_thread.start()
|
||||||
|
|
||||||
|
def _level_allowed(self, record: logging.LogRecord) -> bool:
|
||||||
|
levelname = getattr(record, "levelname", record.levelname)
|
||||||
|
return str(levelname).upper() in self._allowed_levels
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
if not self._bot_token or not self._chat_id:
|
||||||
|
return
|
||||||
|
if not self._level_allowed(record):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
msg = self.format(record)
|
||||||
|
# Экранируем HTML при parse_mode=HTML
|
||||||
|
if self._parse_mode == "HTML":
|
||||||
|
msg = msg.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
self._queue.put_nowait(msg)
|
||||||
|
except queue.Full:
|
||||||
|
sys.stderr.write(f"[TelegramHandler] Очередь переполнена, сообщение пропущено\n")
|
||||||
|
except Exception:
|
||||||
|
self.handleError(record)
|
||||||
|
|
||||||
|
def _process_queue(self) -> None:
|
||||||
|
while not self._stop.is_set():
|
||||||
|
try:
|
||||||
|
msg = self._queue.get(timeout=0.5)
|
||||||
|
if msg:
|
||||||
|
self._send_with_retry(msg)
|
||||||
|
except queue.Empty:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write(f"[TelegramHandler] Ошибка отправки: {e}\n")
|
||||||
|
|
||||||
|
def _split_message(self, text: str) -> List[str]:
|
||||||
|
if len(text) <= self._max_length:
|
||||||
|
return [text]
|
||||||
|
parts = []
|
||||||
|
while text:
|
||||||
|
chunk = text[: self._max_length]
|
||||||
|
idx = chunk.rfind("\n")
|
||||||
|
if idx > self._max_length // 2:
|
||||||
|
chunk, text = chunk[: idx + 1], text[idx + 1 :]
|
||||||
|
else:
|
||||||
|
text = text[self._max_length :]
|
||||||
|
parts.append(chunk)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
def _send_with_retry(self, text: str) -> None:
|
||||||
|
parts = self._split_message(text)
|
||||||
|
for part in parts:
|
||||||
|
for attempt in range(1, self._retry_attempts + 1):
|
||||||
|
try:
|
||||||
|
self._send_to_telegram(part)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if attempt == self._retry_attempts:
|
||||||
|
sys.stderr.write(f"[TelegramHandler] Не удалось отправить после {attempt} попыток: {e}\n")
|
||||||
|
return
|
||||||
|
time.sleep(self._retry_delay)
|
||||||
|
|
||||||
|
def _send_to_telegram(self, text: str) -> None:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{self._bot_token}/sendMessage"
|
||||||
|
data = {
|
||||||
|
"chat_id": self._chat_id,
|
||||||
|
"text": text,
|
||||||
|
"parse_mode": self._parse_mode,
|
||||||
|
"disable_notification": self._disable_notification,
|
||||||
|
}
|
||||||
|
resp = requests.post(url, data=data, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
j = resp.json()
|
||||||
|
if not j.get("ok"):
|
||||||
|
raise RuntimeError(j.get("description", "Unknown Telegram API error"))
|
||||||
|
|
||||||
|
def _poll_commands(self) -> None:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{self._bot_token}/getUpdates"
|
||||||
|
offset = 0
|
||||||
|
while not self._stop.is_set():
|
||||||
|
try:
|
||||||
|
r = requests.get(url, params={"offset": offset, "timeout": 30}, timeout=35)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if not data.get("ok"):
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
for upd in data.get("result", []):
|
||||||
|
offset = upd["update_id"] + 1
|
||||||
|
msg = upd.get("message", {})
|
||||||
|
text = msg.get("text", "").strip()
|
||||||
|
if text != "/start":
|
||||||
|
continue
|
||||||
|
user = msg.get("from", {})
|
||||||
|
user_id = user.get("id")
|
||||||
|
username = user.get("username", "")
|
||||||
|
chat_id = msg.get("chat", {}).get("id")
|
||||||
|
if self._authenticate_user(user_id, username):
|
||||||
|
reply = "✅ Доступ разрешён. Вы будете получать уведомления."
|
||||||
|
else:
|
||||||
|
reply = "❌ Доступ запрещён."
|
||||||
|
self._send_reply(chat_id, reply)
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write(f"[TelegramHandler] Polling error: {e}\n")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def _send_reply(self, chat_id: Any, text: str) -> None:
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = f"https://api.telegram.org/bot{self._bot_token}/sendMessage"
|
||||||
|
requests.post(url, data={"chat_id": chat_id, "text": text}, timeout=10)
|
||||||
|
|
||||||
|
def _authenticate_user(self, user_id: Optional[int], username: str) -> bool:
|
||||||
|
for u in self._allowed_users:
|
||||||
|
if isinstance(u, int) and u == user_id:
|
||||||
|
return True
|
||||||
|
if isinstance(u, str) and (u == username or str(u) == str(user_id)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._stop.set()
|
||||||
|
super().close()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_level(level: str) -> int:
|
||||||
|
m = {
|
||||||
|
"DEBUG": logging.DEBUG,
|
||||||
|
"INFO": logging.INFO,
|
||||||
|
"STATUS": STATUS_LEVEL,
|
||||||
|
"WARNING": logging.WARNING,
|
||||||
|
"ERROR": logging.ERROR,
|
||||||
|
"CRITICAL": logging.CRITICAL,
|
||||||
|
}
|
||||||
|
return m.get(str(level).upper(), logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_root_logger(
|
||||||
|
level: Union[int, str] = logging.INFO,
|
||||||
|
log_file: Optional[str] = None,
|
||||||
|
script_dir: Optional[str] = None,
|
||||||
|
telegram_config: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Настроить корневой логгер проекта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: Уровень логирования (int или "INFO", "DEBUG" и т.д.)
|
||||||
|
log_file: Путь к файлу логов (опционально)
|
||||||
|
script_dir: Директория скрипта для fallback (опционально)
|
||||||
|
telegram_config: Настройки Telegram (если None — Telegram отключён)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Настроенный логгер
|
||||||
|
"""
|
||||||
|
global _initialized
|
||||||
|
logger = logging.getLogger(ROOT_LOGGER_NAME)
|
||||||
|
formatter = logging.Formatter(LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
|
||||||
|
|
||||||
|
if isinstance(level, str):
|
||||||
|
level = _parse_level(level)
|
||||||
|
|
||||||
|
if not _initialized:
|
||||||
|
logger.setLevel(level)
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
logger.addHandler(ch)
|
||||||
|
_initialized = True
|
||||||
|
|
||||||
|
if log_file:
|
||||||
|
has_file = any(
|
||||||
|
isinstance(h, logging.FileHandler)
|
||||||
|
and getattr(h, "baseFilename", "").endswith(os.path.basename(log_file))
|
||||||
|
for h in logger.handlers
|
||||||
|
)
|
||||||
|
if not has_file:
|
||||||
|
try:
|
||||||
|
log_path = log_file
|
||||||
|
if script_dir and not os.path.isabs(log_path):
|
||||||
|
log_path = os.path.normpath(os.path.join(script_dir, log_file))
|
||||||
|
fh = logging.FileHandler(log_path, encoding="utf-8")
|
||||||
|
fh.setFormatter(formatter)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
except OSError:
|
||||||
|
if script_dir:
|
||||||
|
fallback = os.path.join(script_dir, "logs", "zfs_backup.log")
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(fallback), exist_ok=True)
|
||||||
|
fh = logging.FileHandler(fallback, encoding="utf-8")
|
||||||
|
fh.setFormatter(formatter)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
logger.warning("Не удалось писать в %s, используется %s", log_file, fallback)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if telegram_config and telegram_config.get("enabled"):
|
||||||
|
if not any(isinstance(h, TelegramHandler) for h in logger.handlers):
|
||||||
|
try:
|
||||||
|
th = TelegramHandler(telegram_config)
|
||||||
|
th.setLevel(_parse_level(telegram_config.get("log_level", "INFO")))
|
||||||
|
th.setFormatter(formatter)
|
||||||
|
logger.addHandler(th)
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write(f"[logger] Не удалось инициализировать TelegramHandler: {e}\n")
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(name: str) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Получить логгер для модуля.
|
||||||
|
"""
|
||||||
|
root = logging.getLogger(ROOT_LOGGER_NAME)
|
||||||
|
if not root.handlers:
|
||||||
|
setup_root_logger()
|
||||||
|
if name.startswith("ssh_client"):
|
||||||
|
return logging.getLogger(name)
|
||||||
|
return logging.getLogger(f"{ROOT_LOGGER_NAME}.{name}")
|
||||||
@@ -7,21 +7,24 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List, Dict, Union
|
from typing import Optional, List, Dict, Union
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
from .logger import get_logger
|
||||||
|
from .protocols import SSHOperationsBase
|
||||||
|
|
||||||
|
logger = get_logger("postgresql")
|
||||||
|
|
||||||
|
|
||||||
class PostgreSQLOperations:
|
class PostgreSQLOperations(SSHOperationsBase):
|
||||||
"""
|
"""
|
||||||
Класс для операций с PostgreSQL через SSH
|
Класс для операций с PostgreSQL через SSH.
|
||||||
|
Реализует контракт операционного модуля (ssh: SSHProtocol).
|
||||||
"""
|
"""
|
||||||
def __init__(self, ssh_client):
|
|
||||||
|
def __init__(self, ssh_client) -> None:
|
||||||
"""
|
"""
|
||||||
Инициализация модуля PostgreSQL
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ssh_client: Экземпляр SSHBase для выполнения команд
|
ssh_client: Экземпляр, реализующий SSHProtocol (SSHBase, SSHClient).
|
||||||
"""
|
"""
|
||||||
self.ssh = ssh_client
|
super().__init__(ssh_client)
|
||||||
|
|
||||||
def bases_list(self, srv_pgsql: str) -> List[str]:
|
def bases_list(self, srv_pgsql: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
52
modules/protocols.py
Normal file
52
modules/protocols.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Протоколы и интерфейсы для модулей SSH Client.
|
||||||
|
Обеспечивают единый контракт для SSH-операций и операционных модулей.
|
||||||
|
"""
|
||||||
|
from typing import List, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class SSHProtocol(Protocol):
|
||||||
|
"""
|
||||||
|
Протокол SSH-клиента: connect, cmd, close.
|
||||||
|
Реализуется SSHBase и SSHClient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
"""Подключение к удалённому серверу."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def cmd(
|
||||||
|
self,
|
||||||
|
command: str,
|
||||||
|
sleep: float = 0.1,
|
||||||
|
out_to_print: bool = False,
|
||||||
|
suppress_warnings: bool = False,
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
Выполнение команды на удалённом сервере.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[stdout, stderr]
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Закрытие соединения."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SSHOperationsBase:
|
||||||
|
"""
|
||||||
|
Базовый класс для операционных модулей (PostgreSQL, 1C, ZFS и т.д.).
|
||||||
|
Ожидает ssh_client, реализующий SSHProtocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ssh_client: SSHProtocol) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
ssh_client: Экземпляр, реализующий SSHProtocol (SSHBase, SSHClient).
|
||||||
|
"""
|
||||||
|
self.ssh: SSHProtocol = ssh_client
|
||||||
@@ -9,9 +9,10 @@ from .postgresql import PostgreSQLOperations
|
|||||||
from .c1_cluster import C1ClusterOperations
|
from .c1_cluster import C1ClusterOperations
|
||||||
|
|
||||||
|
|
||||||
class ssh(SSHBase, PostgreSQLOperations, C1ClusterOperations):
|
class SSHClient(SSHBase, PostgreSQLOperations, C1ClusterOperations):
|
||||||
"""
|
"""
|
||||||
Класс SSH клиента с поддержкой PostgreSQL и 1С кластера
|
SSH-клиент с поддержкой PostgreSQL и кластера 1С.
|
||||||
|
Рекомендуемый класс для использования (PEP8: PascalCase).
|
||||||
|
|
||||||
Наследует функциональность от:
|
Наследует функциональность от:
|
||||||
- SSHBase: базовые SSH операции (connect, cmd, close)
|
- SSHBase: базовые SSH операции (connect, cmd, close)
|
||||||
@@ -47,3 +48,7 @@ class ssh(SSHBase, PostgreSQLOperations, C1ClusterOperations):
|
|||||||
"""
|
"""
|
||||||
self.set_srv_1c(srv_1c)
|
self.set_srv_1c(srv_1c)
|
||||||
self.set_cluster_credentials(c1_claster_user, c1_claster_pass)
|
self.set_cluster_credentials(c1_claster_user, c1_claster_pass)
|
||||||
|
|
||||||
|
|
||||||
|
# Совместимость: alias для обратной совместимости (legacy)
|
||||||
|
ssh = SSHClient
|
||||||
|
|||||||
@@ -9,17 +9,9 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
# Настройка логирования
|
from .logger import get_logger
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
if not logger.handlers:
|
logger = get_logger("ssh_base")
|
||||||
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)
|
|
||||||
|
|
||||||
# Ожидаемые «ошибки» ZFS при бэкапе — не логируем WARNING при suppress_warnings=True
|
# Ожидаемые «ошибки» ZFS при бэкапе — не логируем WARNING при suppress_warnings=True
|
||||||
KNOWN_ZFS_ERRORS = (
|
KNOWN_ZFS_ERRORS = (
|
||||||
|
|||||||
226
modules/zfs_backup_ops.py
Normal file
226
modules/zfs_backup_ops.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Операционный модуль ZFS Backup.
|
||||||
|
Реализует логику снапшотов, репликации и очистки для удалённых ZFS-пулов.
|
||||||
|
Используется точкой входа zfs_backup.py (cron/CLI).
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from .logger import get_logger
|
||||||
|
from .protocols import SSHProtocol
|
||||||
|
from .ssh_base import SSHBase
|
||||||
|
|
||||||
|
logger = get_logger("zfs_backup")
|
||||||
|
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
SNAPSHOT_DATE_FMT = "%d-%m-%Y" # dd-mm-yyyy
|
||||||
|
|
||||||
|
|
||||||
|
def _execute(ssh: SSHProtocol, command: str, log: logging.Logger) -> Tuple[str, str]:
|
||||||
|
"""Выполнить команду по SSH. При ошибке — исключение."""
|
||||||
|
stdout, stderr = ssh.cmd(command)
|
||||||
|
if stderr and stderr.strip():
|
||||||
|
raise RuntimeError(f"Команда завершилась с ошибкой: {stderr.strip()}")
|
||||||
|
return stdout, stderr
|
||||||
|
|
||||||
|
|
||||||
|
def _create_snapshot(
|
||||||
|
ssh: SSHProtocol,
|
||||||
|
full_dataset: str,
|
||||||
|
date_str: str,
|
||||||
|
log: logging.Logger,
|
||||||
|
) -> bool:
|
||||||
|
"""Создать ZFS-снапшот. При 'dataset already exists' — успех (skip)."""
|
||||||
|
cmd = f"/usr/sbin/zfs snapshot {full_dataset}@{date_str}"
|
||||||
|
stdout, stderr = ssh.cmd(cmd, suppress_warnings=True)
|
||||||
|
stderr_lower = (stderr or "").lower()
|
||||||
|
|
||||||
|
if "dataset already exists" in stderr_lower:
|
||||||
|
pool_name = full_dataset.split("/")[0]
|
||||||
|
snapshot_name = "/".join(full_dataset.split("/")[1:])
|
||||||
|
log.info(
|
||||||
|
"В пуле %s снимок %s@%s уже существует, действие пропущено",
|
||||||
|
pool_name,
|
||||||
|
snapshot_name,
|
||||||
|
date_str,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if stderr and stderr.strip():
|
||||||
|
log.error("❌ Создание снимка failed: %s", stderr.strip())
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info("✅ Снимок %s@%s создан", full_dataset, date_str)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _replicate_snapshot(
|
||||||
|
ssh: SSHProtocol,
|
||||||
|
source_dataset: str,
|
||||||
|
target_dataset: str,
|
||||||
|
date_str: str,
|
||||||
|
prev_date: Optional[str],
|
||||||
|
log: logging.Logger,
|
||||||
|
) -> bool:
|
||||||
|
"""Репликация снапшота. При 'not an earlier snapshot' — успех (skip)."""
|
||||||
|
if prev_date:
|
||||||
|
send_cmd = f"/usr/sbin/zfs send -i @{prev_date} {source_dataset}@{date_str}"
|
||||||
|
else:
|
||||||
|
send_cmd = f"/usr/sbin/zfs send {source_dataset}@{date_str}"
|
||||||
|
full_cmd = f"{send_cmd} | /usr/sbin/zfs recv -F {target_dataset}"
|
||||||
|
stdout, stderr = ssh.cmd(full_cmd, suppress_warnings=True)
|
||||||
|
|
||||||
|
if stderr and "not an earlier snapshot from the same fs" in stderr:
|
||||||
|
pool_name = target_dataset.split("/")[0]
|
||||||
|
dataset_name = "/".join(target_dataset.split("/")[1:])
|
||||||
|
log.info("В пуле %s снимок %s@%s уже существует", pool_name, dataset_name, date_str)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if stderr and stderr.strip():
|
||||||
|
log.error("❌ Репликация failed: %s", stderr.strip())
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info("✅ Репликация %s → %s", source_dataset, target_dataset)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _get_previous_snapshot_date(ssh: SSHProtocol, target_dataset: str) -> Optional[str]:
|
||||||
|
"""Дата последнего снапшота target_dataset (dd-mm-yyyy) или None."""
|
||||||
|
cmd = f"/usr/sbin/zfs list -t snapshot -H -o name {target_dataset} 2>/dev/null || true"
|
||||||
|
stdout, _ = ssh.cmd(cmd)
|
||||||
|
lines = [s.strip() for s in stdout.splitlines() if s.strip()]
|
||||||
|
pattern = re.compile(r"@(\d{2}-\d{2}-\d{4})$")
|
||||||
|
dates: List[str] = []
|
||||||
|
for line in lines:
|
||||||
|
m = pattern.search(line)
|
||||||
|
if m:
|
||||||
|
dates.append(m.group(1))
|
||||||
|
if not dates:
|
||||||
|
return None
|
||||||
|
dates.sort(key=lambda s: datetime.strptime(s, "%d-%m-%Y"))
|
||||||
|
return dates[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_with_retry(
|
||||||
|
log: logging.Logger,
|
||||||
|
server_name: str,
|
||||||
|
operation_name: str,
|
||||||
|
func,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> Any:
|
||||||
|
"""Выполнить операцию до MAX_RETRIES раз при ошибке."""
|
||||||
|
last_exc = None
|
||||||
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
last_exc = e
|
||||||
|
log.warning("❌ %s: failed %s (retry %s/%s): %s", server_name, operation_name, attempt, MAX_RETRIES, e)
|
||||||
|
if attempt == MAX_RETRIES:
|
||||||
|
raise
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
|
|
||||||
|
def backup_server(
|
||||||
|
server: Dict[str, Any],
|
||||||
|
ssh_defaults: Dict[str, Any],
|
||||||
|
log: logging.Logger,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Выполнить бэкап для одного сервера: снапшоты, send/recv, очистка.
|
||||||
|
Конфиг v1.1: server['pools'] — список пулов с source_pool, datasets, target_pool.
|
||||||
|
"""
|
||||||
|
name = server["name"]
|
||||||
|
pools = server["pools"]
|
||||||
|
retention_days = int(server.get("retention_days", 30))
|
||||||
|
|
||||||
|
port = ssh_defaults.get("port", 22)
|
||||||
|
username = ssh_defaults.get("username", "root")
|
||||||
|
pkey_file = ssh_defaults.get("pkey_file", "/root/.ssh/id_rsa")
|
||||||
|
host_keys = ssh_defaults.get("host_keys", "~/.ssh/known_hosts")
|
||||||
|
|
||||||
|
date_str = datetime.now().strftime(SNAPSHOT_DATE_FMT)
|
||||||
|
log.info("Сервер %s: дата снапшотов %s", name, date_str)
|
||||||
|
|
||||||
|
ssh = SSHBase(hostname=name, port=port, username=username, pkey_file=pkey_file, host_keys=host_keys)
|
||||||
|
ssh.connect()
|
||||||
|
|
||||||
|
pool_counts: Dict[str, int] = {}
|
||||||
|
total_datasets = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for pool_config in pools:
|
||||||
|
source_pool = pool_config["source_pool"]
|
||||||
|
datasets = pool_config["datasets"]
|
||||||
|
target_pool = pool_config["target_pool"]
|
||||||
|
pool_counts[source_pool] = len(datasets)
|
||||||
|
total_datasets += len(datasets)
|
||||||
|
|
||||||
|
for dataset in datasets:
|
||||||
|
full_dataset = f"{source_pool}/{dataset}"
|
||||||
|
|
||||||
|
def do_snapshot(fd=full_dataset, d=date_str):
|
||||||
|
if not _create_snapshot(ssh, fd, d, log):
|
||||||
|
raise RuntimeError(f"Snapshot {fd}@{d} failed")
|
||||||
|
|
||||||
|
_run_with_retry(log, name, f"snapshot {full_dataset}", do_snapshot)
|
||||||
|
|
||||||
|
for pool_config in pools:
|
||||||
|
source_pool = pool_config["source_pool"]
|
||||||
|
datasets = pool_config["datasets"]
|
||||||
|
target_pool = pool_config["target_pool"]
|
||||||
|
|
||||||
|
for dataset in datasets:
|
||||||
|
full_dataset = f"{source_pool}/{dataset}"
|
||||||
|
target_dataset = f"{target_pool}/{dataset}"
|
||||||
|
|
||||||
|
def do_send_recv(fd=full_dataset, td=target_dataset, d=date_str):
|
||||||
|
prev_date = _get_previous_snapshot_date(ssh, td)
|
||||||
|
if prev_date:
|
||||||
|
log.info("Инкрементальная передача %s (от %s)", fd, prev_date)
|
||||||
|
else:
|
||||||
|
log.info("Полная передача %s", fd)
|
||||||
|
if not _replicate_snapshot(ssh, fd, td, d, prev_date, log):
|
||||||
|
raise RuntimeError(f"Replicate {fd}@{d} → {td} failed")
|
||||||
|
|
||||||
|
_run_with_retry(log, name, f"send/recv {full_dataset}", do_send_recv)
|
||||||
|
|
||||||
|
cutoff = datetime.now() - timedelta(days=retention_days)
|
||||||
|
for pool_config in pools:
|
||||||
|
source_pool = pool_config["source_pool"]
|
||||||
|
datasets = pool_config["datasets"]
|
||||||
|
target_pool = pool_config["target_pool"]
|
||||||
|
|
||||||
|
for dataset in datasets:
|
||||||
|
for pool, ds in [(source_pool, dataset), (target_pool, dataset)]:
|
||||||
|
full_ds = f"{pool}/{ds}"
|
||||||
|
cmd = f"/usr/sbin/zfs list -t snapshot -H -o name {full_ds} 2>/dev/null || true"
|
||||||
|
stdout, _ = ssh.cmd(cmd)
|
||||||
|
pattern = re.compile(r"@(\d{2}-\d{2}-\d{4})$")
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = pattern.search(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
snap_date = datetime.strptime(m.group(1), "%d-%m-%Y")
|
||||||
|
if snap_date < cutoff:
|
||||||
|
destroy_cmd = f"/usr/sbin/zfs destroy {line}"
|
||||||
|
_execute(ssh, destroy_cmd, log)
|
||||||
|
log.info("Удалён старый снапшот: %s", line)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = ", ".join(f"{p}:{c}" for p, c in sorted(pool_counts.items()))
|
||||||
|
log.info("✅ %s: %s datasets backed up (%s)", name, total_datasets, parts)
|
||||||
|
return True
|
||||||
|
|
||||||
|
finally:
|
||||||
|
ssh.close()
|
||||||
41
pyproject.toml
Normal file
41
pyproject.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "ssh-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "SSH клиент для миграции 1С, ZFS Backup и управления удалёнными серверами"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [{name = "SSH Client Team"}]
|
||||||
|
keywords = ["ssh", "1c", "postgresql", "zfs", "backup", "migration"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: System Administrators",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"paramiko>=2.12.0,<4.0.0",
|
||||||
|
"PyYAML>=6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest>=7.0", "ruff>=0.1.0"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
zfs-backup = "zfs_backup:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["modules*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-dir]
|
||||||
|
"" = "."
|
||||||
@@ -9,9 +9,12 @@
|
|||||||
# SSH клиент для подключения к удаленным серверам
|
# SSH клиент для подключения к удаленным серверам
|
||||||
paramiko>=2.12.0,<4.0.0
|
paramiko>=2.12.0,<4.0.0
|
||||||
|
|
||||||
# ZFS Backup: парсинг конфигурации
|
# Конфигурация
|
||||||
PyYAML>=6.0
|
PyYAML>=6.0
|
||||||
|
|
||||||
|
# Telegram интеграция (PRD v1.8)
|
||||||
|
requests>=2.25.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
343
zfs_backup.py
343
zfs_backup.py
@@ -1,70 +1,60 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Модуль ZFS Backup — централизованное управление ZFS-бэкапами удалённых серверов.
|
Точка входа ZFS Backup — cron/CLI (PRD v1.8).
|
||||||
Интегрирован с SSH-модулем. Cron: ежедневно 20:00.
|
Конфигурация: config/config_log.yaml (логирование), config/zfs_backup.yaml (бэкап).
|
||||||
|
|
||||||
Запуск: python zfs_backup.py --config=config.yaml
|
Запуск: python zfs_backup.py
|
||||||
|
или: zfs-backup (после pip install -e .)
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from typing import Any, Dict, Optional
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
# Добавляем корень проекта в path для импорта modules
|
|
||||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
if SCRIPT_DIR not in sys.path:
|
if SCRIPT_DIR not in sys.path:
|
||||||
sys.path.insert(0, SCRIPT_DIR)
|
sys.path.insert(0, SCRIPT_DIR)
|
||||||
|
|
||||||
from modules.ssh_base import SSHBase
|
from modules.logger import setup_root_logger, get_logger
|
||||||
|
from modules.zfs_backup_ops import backup_server, MAX_RETRIES
|
||||||
|
|
||||||
# Константы
|
|
||||||
DEFAULT_LOG_FILE = "/var/log/zfs_backup.log"
|
DEFAULT_LOG_FILE = "/var/log/zfs_backup.log"
|
||||||
FALLBACK_LOG_FILE = "zfs_backup.log"
|
CONFIG_LOG_PATH = os.path.join(SCRIPT_DIR, "config", "config_log.yaml")
|
||||||
MAX_RETRIES = 3
|
|
||||||
SNAPSHOT_DATE_FMT = "%d-%m-%Y" # dd-mm-yyyy
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(log_file: Optional[str] = None) -> logging.Logger:
|
def load_log_config(config_path: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""Настройка логирования в файл и консоль."""
|
"""Загрузка config_log.yaml. При отсутствии — возвращает defaults."""
|
||||||
logger = logging.getLogger("zfs_backup")
|
path = config_path or CONFIG_LOG_PATH
|
||||||
logger.setLevel(logging.INFO)
|
if not os.path.isfile(path):
|
||||||
if logger.handlers:
|
return {"log_file": DEFAULT_LOG_FILE, "log_level": "INFO"}
|
||||||
return logger
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
|
||||||
formatter = logging.Formatter(
|
|
||||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
def setup_logging(
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
log_file: Optional[str] = None,
|
||||||
|
log_config_path: Optional[str] = None,
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""Настройка логирования из config_log.yaml (файл, консоль, Telegram)."""
|
||||||
|
cfg = load_log_config(log_config_path)
|
||||||
|
path = log_file or cfg.get("log_file", DEFAULT_LOG_FILE)
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
path = os.path.normpath(os.path.join(SCRIPT_DIR, path))
|
||||||
|
setup_root_logger(
|
||||||
|
level=cfg.get("log_level", "INFO"),
|
||||||
|
log_file=path,
|
||||||
|
script_dir=SCRIPT_DIR,
|
||||||
|
telegram_config=cfg.get("telegram"),
|
||||||
)
|
)
|
||||||
|
return get_logger("zfs_backup")
|
||||||
# Файл
|
|
||||||
path = log_file or DEFAULT_LOG_FILE
|
|
||||||
try:
|
|
||||||
fh = logging.FileHandler(path, encoding="utf-8")
|
|
||||||
fh.setFormatter(formatter)
|
|
||||||
logger.addHandler(fh)
|
|
||||||
except OSError:
|
|
||||||
path = os.path.join(SCRIPT_DIR, FALLBACK_LOG_FILE)
|
|
||||||
fh = logging.FileHandler(path, encoding="utf-8")
|
|
||||||
fh.setFormatter(formatter)
|
|
||||||
logger.addHandler(fh)
|
|
||||||
logger.warning("Не удалось писать в %s, используется %s", DEFAULT_LOG_FILE, path)
|
|
||||||
|
|
||||||
# Консоль
|
|
||||||
ch = logging.StreamHandler(sys.stdout)
|
|
||||||
ch.setFormatter(formatter)
|
|
||||||
logger.addHandler(ch)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: str) -> Dict[str, Any]:
|
def load_config(config_path: str) -> Dict[str, Any]:
|
||||||
"""Загрузка конфигурации из YAML → словарь Python (формат v1.1: servers[].pools[])."""
|
"""Загрузка конфигурации из YAML (формат v1.1: servers[].pools[])."""
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
config = yaml.safe_load(f)
|
config = yaml.safe_load(f)
|
||||||
if not config or "servers" not in config:
|
if not config or "servers" not in config:
|
||||||
@@ -79,258 +69,30 @@ def load_config(config_path: str) -> Dict[str, Any]:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def execute(ssh: SSHBase, command: str, log: logging.Logger) -> Tuple[str, str]:
|
|
||||||
"""
|
|
||||||
Выполнение команды по SSH. При ошибке (stderr или ненулевой код) выбрасывает исключение.
|
|
||||||
Возвращает (stdout, stderr).
|
|
||||||
"""
|
|
||||||
stdout, stderr = ssh.cmd(command)
|
|
||||||
if stderr and stderr.strip():
|
|
||||||
raise RuntimeError(f"Команда завершилась с ошибкой: {stderr.strip()}")
|
|
||||||
return stdout, stderr
|
|
||||||
|
|
||||||
|
|
||||||
def create_snapshot(
|
|
||||||
ssh: SSHBase,
|
|
||||||
full_dataset: str,
|
|
||||||
date_str: str,
|
|
||||||
log: logging.Logger,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Создать ZFS-снапшот. При сообщении "dataset already exists" логируем и пропускаем (успех).
|
|
||||||
Возвращает True при успехе (создан или уже существует), False при ошибке (для retry).
|
|
||||||
"""
|
|
||||||
cmd = f"/usr/sbin/zfs snapshot {full_dataset}@{date_str}"
|
|
||||||
stdout, stderr = ssh.cmd(cmd, suppress_warnings=True)
|
|
||||||
stderr_lower = (stderr or "").lower()
|
|
||||||
|
|
||||||
if "dataset already exists" in stderr_lower:
|
|
||||||
pool_name = full_dataset.split("/")[0]
|
|
||||||
snapshot_name = "/".join(full_dataset.split("/")[1:])
|
|
||||||
log.info(
|
|
||||||
"В пуле %s снимок %s@%s уже существует, действие пропущено",
|
|
||||||
pool_name,
|
|
||||||
snapshot_name,
|
|
||||||
date_str,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if stderr and stderr.strip():
|
|
||||||
log.error("❌ Создание снимка failed: %s", stderr.strip())
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info("✅ Снимок %s@%s создан", full_dataset, date_str)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def replicate_snapshot(
|
|
||||||
ssh: SSHBase,
|
|
||||||
source_dataset: str,
|
|
||||||
target_dataset: str,
|
|
||||||
date_str: str,
|
|
||||||
prev_date: Optional[str],
|
|
||||||
log: logging.Logger,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Репликация снапшота: zfs send [ -i @prev ] source@date | zfs recv target.
|
|
||||||
При ошибке "not an earlier snapshot from the same fs" логируем и пропускаем (успех).
|
|
||||||
Возвращает True при успехе (репликация выполнена или снимок уже есть на target), False при ошибке (для retry).
|
|
||||||
"""
|
|
||||||
if prev_date:
|
|
||||||
send_cmd = f"/usr/sbin/zfs send -i @{prev_date} {source_dataset}@{date_str}"
|
|
||||||
else:
|
|
||||||
send_cmd = f"/usr/sbin/zfs send {source_dataset}@{date_str}"
|
|
||||||
full_cmd = f"{send_cmd} | /usr/sbin/zfs recv -F {target_dataset}"
|
|
||||||
stdout, stderr = ssh.cmd(full_cmd, suppress_warnings=True)
|
|
||||||
|
|
||||||
if stderr and "not an earlier snapshot from the same fs" in stderr:
|
|
||||||
pool_name = target_dataset.split("/")[0]
|
|
||||||
dataset_name = "/".join(target_dataset.split("/")[1:])
|
|
||||||
log.info(
|
|
||||||
"В пуле %s снимок %s@%s уже существует",
|
|
||||||
pool_name,
|
|
||||||
dataset_name,
|
|
||||||
date_str,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if stderr and stderr.strip():
|
|
||||||
log.error("❌ Репликация failed: %s", stderr.strip())
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info("✅ Репликация %s → %s", source_dataset, target_dataset)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def get_previous_snapshot_date(
|
|
||||||
ssh: SSHBase,
|
|
||||||
target_dataset: str,
|
|
||||||
log: logging.Logger,
|
|
||||||
) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Получить дату последнего снапшота для target_dataset (формат dd-mm-yyyy).
|
|
||||||
Если снапшотов нет — возвращает None.
|
|
||||||
"""
|
|
||||||
cmd = f"/usr/sbin/zfs list -t snapshot -H -o name {target_dataset} 2>/dev/null || true"
|
|
||||||
stdout, stderr = ssh.cmd(cmd)
|
|
||||||
lines = [s.strip() for s in stdout.splitlines() if s.strip()]
|
|
||||||
pattern = re.compile(r"@(\d{2}-\d{2}-\d{4})$")
|
|
||||||
dates: List[str] = []
|
|
||||||
for line in lines:
|
|
||||||
m = pattern.search(line)
|
|
||||||
if m:
|
|
||||||
dates.append(m.group(1))
|
|
||||||
|
|
||||||
if not dates:
|
|
||||||
return None
|
|
||||||
def parse_d(s: str) -> datetime:
|
|
||||||
return datetime.strptime(s, "%d-%m-%Y")
|
|
||||||
dates.sort(key=parse_d)
|
|
||||||
return dates[-1]
|
|
||||||
|
|
||||||
|
|
||||||
def run_with_retry(
|
|
||||||
log: logging.Logger,
|
|
||||||
server_name: str,
|
|
||||||
operation_name: str,
|
|
||||||
func,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
) -> Any:
|
|
||||||
"""Выполнить операцию до MAX_RETRIES раз при ошибке."""
|
|
||||||
last_exc = None
|
|
||||||
for attempt in range(1, MAX_RETRIES + 1):
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
last_exc = e
|
|
||||||
log.warning(
|
|
||||||
"❌ %s: failed %s (retry %s/%s): %s",
|
|
||||||
server_name,
|
|
||||||
operation_name,
|
|
||||||
attempt,
|
|
||||||
MAX_RETRIES,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
if attempt == MAX_RETRIES:
|
|
||||||
raise
|
|
||||||
raise last_exc
|
|
||||||
|
|
||||||
|
|
||||||
def backup_server(
|
|
||||||
server: Dict[str, Any],
|
|
||||||
ssh_defaults: Dict[str, Any],
|
|
||||||
log: logging.Logger,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Выполнить бэкап для одного сервера: снапшоты по пулам/datasets, send/recv, очистка.
|
|
||||||
Конфиг v1.1: server['pools'] — список пулов с source_pool, datasets, target_pool.
|
|
||||||
"""
|
|
||||||
name = server["name"]
|
|
||||||
pools = server["pools"]
|
|
||||||
retention_days = int(server.get("retention_days", 30))
|
|
||||||
|
|
||||||
port = ssh_defaults.get("port", 22)
|
|
||||||
username = ssh_defaults.get("username", "root")
|
|
||||||
pkey_file = ssh_defaults.get("pkey_file", "/root/.ssh/id_rsa")
|
|
||||||
host_keys = ssh_defaults.get("host_keys", "~/.ssh/known_hosts")
|
|
||||||
|
|
||||||
date_str = datetime.now().strftime(SNAPSHOT_DATE_FMT)
|
|
||||||
log.info("Сервер %s: дата снапшотов %s", name, date_str)
|
|
||||||
|
|
||||||
ssh = SSHBase(hostname=name, port=port, username=username, pkey_file=pkey_file, host_keys=host_keys)
|
|
||||||
ssh.connect()
|
|
||||||
|
|
||||||
pool_counts: Dict[str, int] = {}
|
|
||||||
total_datasets = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 1. Фаза снапшотов
|
|
||||||
for pool_config in pools:
|
|
||||||
source_pool = pool_config["source_pool"]
|
|
||||||
datasets = pool_config["datasets"]
|
|
||||||
target_pool = pool_config["target_pool"]
|
|
||||||
pool_counts[source_pool] = len(datasets)
|
|
||||||
total_datasets += len(datasets)
|
|
||||||
|
|
||||||
for dataset in datasets:
|
|
||||||
full_dataset = f"{source_pool}/{dataset}"
|
|
||||||
|
|
||||||
def do_snapshot(fd=full_dataset, d=date_str):
|
|
||||||
if not create_snapshot(ssh, fd, d, log):
|
|
||||||
raise RuntimeError(f"Snapshot {fd}@{d} failed")
|
|
||||||
|
|
||||||
run_with_retry(log, name, f"snapshot {full_dataset}", do_snapshot)
|
|
||||||
|
|
||||||
# 2. Фаза send/recv
|
|
||||||
for pool_config in pools:
|
|
||||||
source_pool = pool_config["source_pool"]
|
|
||||||
datasets = pool_config["datasets"]
|
|
||||||
target_pool = pool_config["target_pool"]
|
|
||||||
|
|
||||||
for dataset in datasets:
|
|
||||||
full_dataset = f"{source_pool}/{dataset}"
|
|
||||||
target_dataset = f"{target_pool}/{dataset}"
|
|
||||||
|
|
||||||
def do_send_recv(fd=full_dataset, td=target_dataset, d=date_str):
|
|
||||||
prev_date = get_previous_snapshot_date(ssh, td, log)
|
|
||||||
if prev_date:
|
|
||||||
log.info("Инкрементальная передача %s (от %s)", fd, prev_date)
|
|
||||||
else:
|
|
||||||
log.info("Полная передача %s", fd)
|
|
||||||
if not replicate_snapshot(ssh, fd, td, d, prev_date, log):
|
|
||||||
raise RuntimeError(f"Replicate {fd}@{d} → {td} failed")
|
|
||||||
|
|
||||||
run_with_retry(log, name, f"send/recv {full_dataset}", do_send_recv)
|
|
||||||
|
|
||||||
# 3. Cleanup
|
|
||||||
cutoff = datetime.now() - timedelta(days=retention_days)
|
|
||||||
for pool_config in pools:
|
|
||||||
source_pool = pool_config["source_pool"]
|
|
||||||
datasets = pool_config["datasets"]
|
|
||||||
target_pool = pool_config["target_pool"]
|
|
||||||
|
|
||||||
for dataset in datasets:
|
|
||||||
for pool, ds in [(source_pool, dataset), (target_pool, dataset)]:
|
|
||||||
full_ds = f"{pool}/{ds}"
|
|
||||||
cmd = f"/usr/sbin/zfs list -t snapshot -H -o name {full_ds} 2>/dev/null || true"
|
|
||||||
stdout, _ = ssh.cmd(cmd)
|
|
||||||
pattern = re.compile(r"@(\d{2}-\d{2}-\d{4})$")
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
m = pattern.search(line)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
snap_date = datetime.strptime(m.group(1), "%d-%m-%Y")
|
|
||||||
if snap_date < cutoff:
|
|
||||||
destroy_cmd = f"/usr/sbin/zfs destroy {line}"
|
|
||||||
execute(ssh, destroy_cmd, log)
|
|
||||||
log.info("Удалён старый снапшот: %s", line)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = ", ".join(f"{p}:{c}" for p, c in sorted(pool_counts.items()))
|
|
||||||
log.info("✅ %s: %s datasets backed up (%s)", name, total_datasets, parts)
|
|
||||||
return True
|
|
||||||
|
|
||||||
finally:
|
|
||||||
ssh.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description="ZFS Backup — бэкап ZFS датасетов по config.yaml")
|
parser = argparse.ArgumentParser(description="ZFS Backup — бэкап ZFS датасетов по config.yaml")
|
||||||
parser.add_argument("--config", default="config.yaml", help="Путь к config.yaml")
|
parser.add_argument(
|
||||||
parser.add_argument("--log-file", default=None, help="Путь к лог-файлу (по умолчанию /var/log/zfs_backup.log)")
|
"--config",
|
||||||
|
default="config/zfs_backup.yaml",
|
||||||
|
help="Путь к config (по умолчанию config/zfs_backup.yaml)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-file",
|
||||||
|
default=None,
|
||||||
|
help="Путь к лог-файлу (переопределяет config/config_log.yaml)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-config",
|
||||||
|
default=None,
|
||||||
|
help="Путь к config_log.yaml (по умолчанию config/config_log.yaml)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
log = setup_logging(args.log_file)
|
log = setup_logging(log_file=args.log_file, log_config_path=args.log_config)
|
||||||
|
|
||||||
config_path = args.config
|
config_path = args.config
|
||||||
if not os.path.isabs(config_path):
|
if not os.path.isabs(config_path):
|
||||||
config_path = os.path.join(SCRIPT_DIR, config_path)
|
config_path = os.path.normpath(os.path.join(SCRIPT_DIR, config_path))
|
||||||
if not os.path.isfile(config_path):
|
if not os.path.isfile(config_path):
|
||||||
log.error("Файл конфигурации не найден: %s", config_path)
|
log.error("Файл конфигурации не найден: %s", config_path)
|
||||||
return 1
|
return 1
|
||||||
@@ -348,7 +110,12 @@ def main() -> int:
|
|||||||
try:
|
try:
|
||||||
backup_server(server, ssh_defaults, log)
|
backup_server(server, ssh_defaults, log)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("❌ %s: бэкап завершился ошибкой после %s попыток: %s", server["name"], MAX_RETRIES, e)
|
log.exception(
|
||||||
|
"❌ %s: бэкап завершился ошибкой после %s попыток: %s",
|
||||||
|
server["name"],
|
||||||
|
MAX_RETRIES,
|
||||||
|
e,
|
||||||
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user