""" Сервис проверки и применения IP-настроек через NetworkManager (D-Bus). Назначение: - Перед применением конфигурации проверяет занятость IP адреса, сканируя сегмент сети средствами ARP (через `NetworkScanner`). - Применяет изменения профиля подключения через `sdbus_async.networkmanager` и выполняет переподключение интерфейса. Требования/ограничения: - Доступ к системной шине D-Bus и права на изменения сетевых профилей. - Структура входных данных должна соответствовать схеме `ipv4_updates` для корректного обновления вложенных полей у профиля. """ import asyncio import logging from typing import Dict, Any, Optional import subprocess from sdbus_async.networkmanager import ( NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ) import sdbus from sdbus_async.networkmanager.settings import ConnectionProfile from services.arp_service import NetworkScanner from utils.i18n import _load_translations from babel.support import Translations from fastapi import Depends logger = logging.getLogger(__name__) class IPService: """Сервис проверки доступности IP-адреса и применения настроек сети. Предоставляет асинхронные методы для проверки занятости IP-адреса с использованием ARP-сканирования и применения настроек через NetworkManager. """ def __init__(self): """Инициализирует сервис IP. Создаёт объект ARP-сканера и устанавливает системную шину D-Bus как по умолчанию. """ self.arp_scanner = NetworkScanner() self.bus = sdbus.sd_bus_open_system() sdbus.set_default_bus(self.bus) def _reset_bus(self): """Перезапускает соединение с системной шиной D-Bus. Закрывает текущее соединение и создаёт новое, устанавливая его как по умолчанию. """ new_bus = sdbus.sd_bus_open_system() sdbus.set_default_bus(new_bus) if hasattr(self, "bus") and self.bus is not None: self.bus = None self.bus = new_bus async def check_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> Dict[str, Any]: """Проверяет доступность IP-адреса перед его применением. Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес. Перезапускает шину D-Bus перед выполнением. Args: args (Dict[str, Any]): Словарь с данными, включая "ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса. translations (Translations): Объект переводов. Returns: Dict[str, Any]: Словарь с ключами: - "available" (bool): Доступен ли IP-адрес. - "message" (str): Сообщение о результате проверки. Raises: Exception: Если произошла ошибка при сканировании ARP. """ translations = translations or _load_translations("ru") self._reset_bus() new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"] logger.info("check_ip_config: start ARP scan for %s", new_ip) try: clients = await asyncio.wait_for( asyncio.to_thread(self.arp_scanner.scan_network), timeout=10, ) except asyncio.TimeoutError as exc: logger.error("check_ip_config: ARP scan timed out for %s", new_ip) return self._fallback_ping(new_ip, translations) except Exception as exc: logger.exception("check_ip_config: ARP scan failed for %s", new_ip) return self._fallback_ping(new_ip, translations, error=str(exc)) if any(client["ip"] == new_ip for client in clients): return { "available": False, "message": translations.gettext("IP-адрес {ip} уже занят.").format(ip=new_ip) } return { "available": True, "message": translations.gettext("Настройки сетевого профиля доступны для использования.") } def _fallback_ping(self, ip: str, translations: Translations, error: Optional[str] = None) -> Dict[str, Any]: """Резервная проверка через ping, если ARP-скан недоступен. Не блокирует применение: если ping не отвечает, считаем IP свободным, но возвращаем предупреждение. """ try: logger.info("check_ip_config: fallback ping for %s after ARP failure (%s)", ip, error or "timeout") result = subprocess.run( ["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if result.returncode == 0: return { "available": False, "message": translations.gettext("IP-адрес {ip} отвечает на ping").format(ip=ip), } except Exception as exc: logger.warning("check_ip_config: ping fallback failed for %s: %s", ip, exc) return { "available": True, "message": translations.gettext("Не удалось проверить ARP, IP {ip} считается свободным").format(ip=ip), } async def apply_ip_config(self, args: Dict[str, Any], translations: Translations = None) -> None: """Применяет проверенные настройки IP к указанному соединению. Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager, отключает и переподключает устройство к сети. Args: args (Dict[str, Any]): Словарь с данными, включая: - "connection_id" (str): Идентификатор соединения. - "ipv4_updates" (Dict): Обновления настроек IPv4. translations (Translations): Объект переводов. Raises: ValueError: Если соединение с указанным ID не найдено. RuntimeError: Если устройство для интерфейса не найдено. Note: Требуются права для изменения настроек через NetworkManager. """ translations = translations or _load_translations("ru") self._reset_bus() nm = NetworkManager() settings = NetworkManagerSettings() logger.info("apply_ip_config: start for connection_id=%s", args.get("connection_id")) try: connection_paths = await asyncio.wait_for( settings.get_connections_by_id(args["connection_id"]), timeout=5, ) except asyncio.TimeoutError as exc: logger.error("apply_ip_config: timeout while fetching connection %s", args.get("connection_id")) raise RuntimeError( translations.gettext("NetworkManager не ответил при поиске профиля") ) from exc if not connection_paths: raise ValueError(translations.gettext("Подключение {connection_id} не найдено").format(connection_id=args["connection_id"])) connection = NetworkConnectionSettings(connection_paths[0]) try: profile = await asyncio.wait_for(connection.get_profile(), timeout=5) except asyncio.TimeoutError as exc: logger.error("apply_ip_config: timeout while reading profile %s", args.get("connection_id")) raise RuntimeError( translations.gettext("NetworkManager не ответил при чтении профиля") ) from exc settings_dict = profile.to_settings_dict() for key_path, value in args["ipv4_updates"].items(): section, *props = key_path.split(".") current = settings_dict[section] for prop in props[:-1]: current = current.setdefault(prop, {}) current[props[-1]] = value new_profile = ConnectionProfile.from_settings_dict(settings_dict) try: await asyncio.wait_for(connection.update(new_profile.to_dbus()), timeout=5) except asyncio.TimeoutError as exc: logger.error("apply_ip_config: timeout while updating profile %s", args.get("connection_id")) raise RuntimeError( translations.gettext("NetworkManager не ответил при обновлении профиля") ) from exc logger.info("apply_ip_config: settings updated for %s", args.get("connection_id")) interface_name: Optional[str] = settings_dict["connection"].get("interface-name") devices = await asyncio.wait_for(nm.get_devices(), timeout=5) # Если interface-name не задан, пытаемся найти устройство по активному подключению if not interface_name: for device_path in devices: device = NetworkDeviceGeneric(device_path) try: applied = await asyncio.wait_for(device.get_applied_connection(), timeout=3) connection_info = applied[0].get("connection", {}) active_id = connection_info.get("id", ("s", None))[1] if active_id == args["connection_id"]: interface_name = await asyncio.wait_for(device.interface, timeout=3) break except Exception as exc: logger.debug("apply_ip_config: unable to derive interface from device %s: %s", device_path, exc) continue if not interface_name: raise RuntimeError( translations.gettext("Не удалось определить интерфейс для профиля {connection_id}").format( connection_id=args["connection_id"] ) ) for device_path in devices: device = NetworkDeviceGeneric(device_path) if await asyncio.wait_for(device.interface, timeout=3) == interface_name: await asyncio.wait_for(device.disconnect(), timeout=5) logger.info("apply_ip_config: disconnected from %s", interface_name) await asyncio.wait_for( nm.activate_connection(connection=connection_paths[0], device=device_path), timeout=10, ) logger.info("apply_ip_config: reconnected to %s", args.get("connection_id")) return raise RuntimeError(translations.gettext("Устройство для интерфейса {interface} не найдено").format(interface=interface_name))