448 lines
17 KiB
Python
448 lines
17 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
|
||
"""
|
||
|
||
import requests
|
||
import json
|
||
import logging
|
||
from typing import Dict, Any, Optional, List, Tuple
|
||
import socket
|
||
import struct
|
||
from time import sleep
|
||
import urllib3
|
||
from synology_dsm import SynologyDSM
|
||
|
||
from src.config.config import (
|
||
SYNOLOGY_HOST,
|
||
SYNOLOGY_PORT,
|
||
SYNOLOGY_USERNAME,
|
||
SYNOLOGY_PASSWORD,
|
||
SYNOLOGY_SECURE,
|
||
SYNOLOGY_TIMEOUT,
|
||
SYNOLOGY_MAC,
|
||
WOL_PORT
|
||
)
|
||
|
||
# Отключение предупреждений о небезопасных SSL-соединениях
|
||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class SynologyAPI:
|
||
"""Класс для взаимодействия с API Synology NAS с использованием python-synology"""
|
||
|
||
def __init__(self):
|
||
"""Инициализация класса SynologyAPI"""
|
||
self.protocol = "https" if SYNOLOGY_SECURE else "http"
|
||
self.dsm = None
|
||
|
||
def login(self) -> bool:
|
||
"""Авторизация в API Synology NAS используя python-synology"""
|
||
try:
|
||
# Создаем экземпляр SynologyDSM
|
||
self.dsm = SynologyDSM(
|
||
SYNOLOGY_HOST,
|
||
port=SYNOLOGY_PORT,
|
||
username=SYNOLOGY_USERNAME,
|
||
password=SYNOLOGY_PASSWORD,
|
||
secure=SYNOLOGY_SECURE,
|
||
timeout=SYNOLOGY_TIMEOUT,
|
||
verify_ssl=False
|
||
)
|
||
|
||
# Авторизация
|
||
self.dsm.login()
|
||
logger.info("Successfully logged in to Synology NAS")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to log in to Synology NAS: {str(e)}")
|
||
self.dsm = None
|
||
return False
|
||
|
||
def logout(self) -> bool:
|
||
"""Выход из API Synology NAS"""
|
||
if not self.dsm:
|
||
return True
|
||
|
||
try:
|
||
self.dsm.logout()
|
||
self.dsm = None
|
||
logger.info("Successfully logged out from Synology NAS")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error during logout: {str(e)}")
|
||
return False
|
||
|
||
def get_system_status(self) -> Optional[Dict[str, Any]]:
|
||
"""Получение расширенного статуса системы"""
|
||
if not self.dsm and not self.login():
|
||
return None
|
||
|
||
try:
|
||
result = {
|
||
"model": self.dsm.information.model,
|
||
"version": self.dsm.information.version_string,
|
||
"uptime": self.dsm.information.uptime,
|
||
"serial": self.dsm.information.serial,
|
||
"temperature": self.dsm.information.temperature,
|
||
"temperature_unit": "C",
|
||
"cpu_usage": self.dsm.utilisation.cpu_total_load,
|
||
"memory": {
|
||
"total_mb": self.dsm.utilisation.memory_size_mb,
|
||
"available_mb": self.dsm.utilisation.memory_available_real_mb,
|
||
"usage_percent": self.dsm.utilisation.memory_real_usage
|
||
},
|
||
"network": self._get_network_info(),
|
||
"volumes": self._get_volumes_info()
|
||
}
|
||
|
||
logger.info("Successfully fetched extended system status")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting system status: {str(e)}")
|
||
return None
|
||
|
||
def _get_network_info(self) -> List[Dict[str, Any]]:
|
||
"""Получение информации о сетевых интерфейсах"""
|
||
try:
|
||
result = []
|
||
|
||
# Получение информации о сети
|
||
for device in self.dsm.network.interfaces:
|
||
net_info = {
|
||
"device": device,
|
||
"ip": self.dsm.network.get_ip(device),
|
||
"mask": self.dsm.network.get_mask(device),
|
||
"mac": self.dsm.network.get_mac(device),
|
||
"type": self.dsm.network.get_type(device),
|
||
"rx_bytes": self.dsm.network.get_rx(device),
|
||
"tx_bytes": self.dsm.network.get_tx(device)
|
||
}
|
||
result.append(net_info)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting network info: {str(e)}")
|
||
return []
|
||
|
||
def _get_volumes_info(self) -> List[Dict[str, Any]]:
|
||
"""Получение информации о томах хранения"""
|
||
try:
|
||
result = []
|
||
|
||
# Получение информации о томах
|
||
for volume in self.dsm.storage.volumes:
|
||
vol_info = {
|
||
"name": volume,
|
||
"status": self.dsm.storage.volume_status(volume),
|
||
"device_type": self.dsm.storage.volume_device_type(volume),
|
||
"total_size": self.dsm.storage.volume_size_total(volume),
|
||
"used_size": self.dsm.storage.volume_size_used(volume),
|
||
"percent_used": self.dsm.storage.volume_percentage_used(volume)
|
||
}
|
||
result.append(vol_info)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting volumes info: {str(e)}")
|
||
return []
|
||
|
||
def get_shared_folders(self) -> List[Dict[str, Any]]:
|
||
"""Получение списка общих папок"""
|
||
if not self.dsm and not self.login():
|
||
return []
|
||
|
||
try:
|
||
result = []
|
||
|
||
# Получение информации о общих папках
|
||
for folder in self.dsm.share.shares:
|
||
share_info = {
|
||
"name": folder,
|
||
"path": self.dsm.share.get_info(folder).get("path", ""),
|
||
"desc": self.dsm.share.get_info(folder).get("desc", "")
|
||
}
|
||
result.append(share_info)
|
||
|
||
logger.info(f"Successfully retrieved {len(result)} shared folders")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting shared folders: {str(e)}")
|
||
return []
|
||
|
||
def get_system_info(self) -> Dict[str, Any]:
|
||
"""Получение основной информации о системе"""
|
||
if not self.dsm and not self.login():
|
||
return {}
|
||
|
||
try:
|
||
result = {
|
||
"model": self.dsm.information.model,
|
||
"serial": self.dsm.information.serial,
|
||
"version": self.dsm.information.version_string,
|
||
"uptime": self.dsm.information.uptime
|
||
}
|
||
|
||
logger.info("Successfully fetched system info")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting system info: {str(e)}")
|
||
return {}
|
||
|
||
def shutdown_system(self) -> bool:
|
||
"""Выключение системы"""
|
||
if not self.dsm and not self.login():
|
||
return False
|
||
|
||
try:
|
||
# Используем низкоуровневый API для отправки команды выключения
|
||
endpoint = "SYNO.DSM.System"
|
||
api_path = "entry.cgi"
|
||
req_param = {"version": 1, "method": "shutdown"}
|
||
|
||
self.dsm.post(endpoint, api_path, req_param)
|
||
logger.info("Successfully initiated system shutdown")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error shutting down system: {str(e)}")
|
||
return False
|
||
|
||
def reboot_system(self) -> bool:
|
||
"""Перезагрузка системы"""
|
||
if not self.dsm and not self.login():
|
||
return False
|
||
|
||
try:
|
||
# Используем низкоуровневый API для отправки команды перезагрузки
|
||
endpoint = "SYNO.DSM.System"
|
||
api_path = "entry.cgi"
|
||
req_param = {"version": 1, "method": "reboot"}
|
||
|
||
self.dsm.post(endpoint, api_path, req_param)
|
||
logger.info("Successfully initiated system reboot")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error rebooting system: {str(e)}")
|
||
return False
|
||
|
||
def is_online(self) -> bool:
|
||
"""Проверка онлайн-статуса Synology NAS"""
|
||
try:
|
||
socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
socket_obj.settimeout(SYNOLOGY_TIMEOUT)
|
||
result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
|
||
socket_obj.close()
|
||
|
||
return result == 0
|
||
except socket.error:
|
||
return False
|
||
|
||
def wake_on_lan(self) -> bool:
|
||
"""Отправка Wake-on-LAN пакета для включения Synology NAS"""
|
||
if not SYNOLOGY_MAC:
|
||
logger.error("MAC address not configured")
|
||
return False
|
||
|
||
try:
|
||
# Преобразование MAC-адреса в байты
|
||
mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
|
||
mac_bytes = bytes.fromhex(mac_address)
|
||
|
||
# Создание Magic Packet
|
||
magic_packet = b'\xff' * 6 + mac_bytes * 16
|
||
|
||
# Отправка пакета
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||
sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
|
||
sock.close()
|
||
|
||
logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
|
||
return False
|
||
|
||
def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
|
||
"""Ожидание загрузки Synology NAS"""
|
||
logger.info("Waiting for Synology NAS to boot...")
|
||
|
||
for attempt in range(max_attempts):
|
||
if self.is_online():
|
||
logger.info(f"Synology NAS is online after {attempt + 1} attempts")
|
||
# Дадим дополнительное время для полной загрузки всех сервисов
|
||
sleep(delay)
|
||
return True
|
||
|
||
sleep(delay)
|
||
logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
|
||
|
||
logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
|
||
return False
|
||
|
||
def power_on(self) -> bool:
|
||
"""Включение Synology NAS"""
|
||
if self.is_online():
|
||
logger.info("Synology NAS is already online")
|
||
return True
|
||
|
||
# Отправка WoL пакета
|
||
if not self.wake_on_lan():
|
||
return False
|
||
|
||
# Ожидание загрузки
|
||
return self.wait_for_boot()
|
||
|
||
def power_off(self) -> bool:
|
||
"""Выключение Synology NAS"""
|
||
if not self.is_online():
|
||
logger.info("Synology NAS is already offline")
|
||
return True
|
||
|
||
return self.shutdown_system()
|
||
|
||
def get_system_load(self) -> Dict[str, Any]:
|
||
"""Получение информации о загрузке системы"""
|
||
if not self.dsm and not self.login():
|
||
return {}
|
||
|
||
try:
|
||
result = {
|
||
"cpu_load": self.dsm.utilisation.cpu_total_load,
|
||
"memory": {
|
||
"total_mb": self.dsm.utilisation.memory_size_mb,
|
||
"available_mb": self.dsm.utilisation.memory_available_real_mb,
|
||
"cached_mb": self.dsm.utilisation.memory_cached_mb,
|
||
"usage_percent": self.dsm.utilisation.memory_real_usage
|
||
},
|
||
"network": {}
|
||
}
|
||
|
||
# Добавляем данные по сети
|
||
for device in self.dsm.network.interfaces:
|
||
result["network"][device] = {
|
||
"rx_bytes": self.dsm.network.get_rx(device),
|
||
"tx_bytes": self.dsm.network.get_tx(device)
|
||
}
|
||
|
||
logger.info("Successfully fetched system load")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting system load: {str(e)}")
|
||
return {}
|
||
|
||
def get_storage_status(self) -> Dict[str, Any]:
|
||
"""Получение подробной информации о хранилище"""
|
||
if not self.dsm and not self.login():
|
||
return {}
|
||
|
||
try:
|
||
result = {
|
||
"volumes": self._get_volumes_info(),
|
||
"disks": self._get_disks_info(),
|
||
"total_size": 0,
|
||
"total_used": 0
|
||
}
|
||
|
||
# Суммируем общий размер и использование
|
||
for volume in result["volumes"]:
|
||
result["total_size"] += volume["total_size"]
|
||
result["total_used"] += volume["used_size"]
|
||
|
||
logger.info("Successfully fetched storage status")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting storage status: {str(e)}")
|
||
return {}
|
||
|
||
def _get_disks_info(self) -> List[Dict[str, Any]]:
|
||
"""Получение информации о дисках"""
|
||
try:
|
||
result = []
|
||
|
||
# Получение информации о дисках
|
||
for disk in self.dsm.storage.disks:
|
||
disk_info = {
|
||
"name": disk,
|
||
"model": self.dsm.storage.disk_model(disk),
|
||
"type": self.dsm.storage.disk_type(disk),
|
||
"status": self.dsm.storage.disk_status(disk),
|
||
"temp": self.dsm.storage.disk_temp(disk)
|
||
}
|
||
result.append(disk_info)
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting disks info: {str(e)}")
|
||
return []
|
||
|
||
def get_security_status(self) -> Dict[str, bool]:
|
||
"""Получение информации о состоянии безопасности"""
|
||
if not self.dsm and not self.login():
|
||
return {"success": False}
|
||
|
||
try:
|
||
# Используем низкоуровневый API для получения информации о безопасности
|
||
endpoint = "SYNO.Core.Security.DSM"
|
||
api_path = "entry.cgi"
|
||
req_param = {"version": 1, "method": "status"}
|
||
|
||
response = self.dsm.get(endpoint, api_path, req_param)
|
||
|
||
if response and "data" in response:
|
||
return {
|
||
"success": True,
|
||
"status": response["data"].get("status", "unknown"),
|
||
"last_check": response["data"].get("last_check", None),
|
||
"is_secure": response["data"].get("is_secure", False)
|
||
}
|
||
else:
|
||
return {"success": False}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting security status: {str(e)}")
|
||
return {"success": False}
|
||
|
||
def get_users(self) -> List[str]:
|
||
"""Получение списка пользователей"""
|
||
if not self.dsm and not self.login():
|
||
return []
|
||
|
||
try:
|
||
users = []
|
||
|
||
# Используем низкоуровневый API для получения списка пользователей
|
||
endpoint = "SYNO.Core.User"
|
||
api_path = "entry.cgi"
|
||
req_param = {"version": 1, "method": "list", "additional": ["email"]}
|
||
|
||
response = self.dsm.get(endpoint, api_path, req_param)
|
||
|
||
if response and "data" in response and "users" in response["data"]:
|
||
for user in response["data"]["users"]:
|
||
if "name" in user:
|
||
users.append(user["name"])
|
||
|
||
logger.info(f"Successfully retrieved {len(users)} users")
|
||
return users
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting users: {str(e)}")
|
||
return []
|