#!/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 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 []