From 37f9e3abfb5b8941cedd6686a4ddcb6e825627b2 Mon Sep 17 00:00:00 2001 From: "s.galanskiy" Date: Mon, 23 Jun 2025 12:47:05 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3.=20docstring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SRV/api/v1/auth.py | 113 +++++++++++---- SRV/api/v1/ip.py | 76 +++++++--- SRV/api/v1/network.py | 126 +++++++++++----- SRV/api/v1/subnet.py | 167 ++++++++++++++++++++-- SRV/api/v1/websocket.py | 79 ++++++++-- SRV/services/arp_service.py | 57 ++++++-- SRV/services/auth_service.py | 1 - SRV/services/connect_service.py | 61 +++++++- SRV/services/ip_service.py | 60 ++++++-- SRV/services/network_service.py | 245 ++++++++++++++++++++++---------- SRV/services/subnet_service.py | 172 +++++++++++++++++++++- SRV/services/wifi_service.py | 128 ++++++++++++++--- SRV/storage/json_storage.py | 88 ++++++++++-- SRV/storage/session_storage.py | 104 ++++++++++++-- 14 files changed, 1234 insertions(+), 243 deletions(-) diff --git a/SRV/api/v1/auth.py b/SRV/api/v1/auth.py index ffb5d6d..a3fbe2a 100644 --- a/SRV/api/v1/auth.py +++ b/SRV/api/v1/auth.py @@ -1,4 +1,3 @@ -# api/v1/auth.py from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.security import OAuth2PasswordBearer from services.auth_service import AuthService @@ -17,7 +16,21 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") @router.post("/register", response_model=UserResponse) async def register(user: UserCreate): - """Регистрирует нового пользователя""" + """Регистрирует нового пользователя. + + Создаёт нового пользователя на основе предоставленных данных и возвращает + информацию о зарегистрированном пользователе. + + Args: + user (UserCreate): Объект с данными нового пользователя (username, password, is_admin). + + Returns: + UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin). + + Raises: + HTTPException: Если пользователь уже существует (status_code=400) или произошла + внутренняя ошибка (status_code=500). + """ try: db_user = auth_service.register(user) logger.info(f"Пользователь {user.username} успешно зарегистрирован") @@ -26,13 +39,27 @@ async def register(user: UserCreate): raise e except Exception as e: logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}") - raise HTTPException(status_code=500, - detail="Не удалось зарегистрировать пользователя") + raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя") @router.post("/login", response_model=Token) async def login(user: UserLogin, request: Request): - """Авторизует пользователя и создаёт сессию""" + """Авторизует пользователя и создаёт сессию. + + Проверяет учетные данные пользователя и возвращает токен и session_id для + последующей авторизации. + + Args: + user (UserLogin): Объект с данными для входа (username, password). + request (Request): Объект запроса для получения client_id (IP-адрес клиента). + + Returns: + Token: Объект с access_token и session_id для авторизации. + + Raises: + HTTPException: Если учетные данные неверны (status_code=401) или произошла + внутренняя ошибка (status_code=500). + """ try: client_id = request.client.host logger.debug(f"Попытка авторизации для client_id {client_id}") @@ -45,13 +72,27 @@ async def login(user: UserLogin, request: Request): raise e except Exception as e: logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}") - raise HTTPException(status_code=500, - detail="Не удалось авторизовать пользователя") + raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя") @router.get("/me") async def get_current_user(request: Request): - """Возвращает данные текущего пользователя и токен""" + """Возвращает данные текущего пользователя и токен. + + Проверяет активную сессию по client_id и возвращает информацию о пользователе + и токен для дальнейшего использования. + + Args: + request (Request): Объект запроса для получения client_id (IP-адрес клиента). + + Returns: + dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и + "token_type" (str, всегда "bearer"). + + Raises: + HTTPException: Если сессия не найдена или истекла (status_code=401) или + произошла внутренняя ошибка (status_code=500). + """ try: client_id = request.client.host logger.debug(f"Запрос /me для client_id {client_id}") @@ -66,13 +107,25 @@ async def get_current_user(request: Request): except Exception as e: logger.error( f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}") - raise HTTPException(status_code=500, - detail="Не удалось получить данные пользователя") + raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя") @router.get("/logout") async def logout(request: Request): - """Выход из системы, удаление сессии""" + """Выход из системы, удаление сессии. + + Удаляет активную сессию пользователя по client_id. + + Args: + request (Request): Объект запроса для получения client_id (IP-адрес клиента). + + Returns: + dict: Пустой словарь или сообщение об успешном выходе. + + Raises: + HTTPException: Если сессия не найдена (status_code=401) или произошла + внутренняя ошибка (status_code=500). + """ try: client_id = request.client.host logger.debug(f"Запрос logout для client_id {client_id}") @@ -81,13 +134,24 @@ async def logout(request: Request): raise e except Exception as e: logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}") - raise HTTPException(status_code=500, - detail="Не удалось выйти из системы") + raise HTTPException(status_code=500, detail="Не удалось выйти из системы") @router.get("/check_json") async def check_users_file(): - """Проверяет, существует ли файл users.json и содержит ли он пользователей""" + """Проверяет, существует ли файл users.json и содержит ли он пользователей. + + Возвращает информацию о состоянии файла и наличии пользователей. + + Returns: + dict: Словарь с ключами: + - "exists" (bool): Существует ли файл. + - "has_users" (bool): Содержит ли файл пользователей. + - "message" (str): Сообщение с описанием результата. + + Raises: + HTTPException: Если произошла ошибка при проверке файла (status_code=500). + """ try: file_path = "/etc/gonets/users.json" storage = JsonStorage(file_path=file_path) @@ -104,27 +168,24 @@ async def check_users_file(): has_users = len(data.get("users", [])) > 0 if has_users: logger.info( - f"Файл {file_path} существует и содержит пользователей") + f"Файл {file_path} существует и содержит пользователей" + ) return { "exists": True, "has_users": True, - "message": - "Файл users.json существует и содержит пользователей", + "message": "Файл users.json существует и содержит пользователей", } else: logger.info( - f"Файл {file_path} существует, но не содержит пользователей") + f"Файл {file_path} существует, но не содержит пользователей" + ) return { - "exists": - True, - "has_users": - False, - "message": - "Файл users.json существует, но не содержит пользователей", + "exists": True, + "has_users": False, + "message": "Файл users.json существует, но не содержит пользователей", } except Exception as e: logger.error( f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}" ) - raise HTTPException(status_code=500, - detail="Ошибка при проверке файла") + raise HTTPException(status_code=500, detail="Ошибка при проверке файла") \ No newline at end of file diff --git a/SRV/api/v1/ip.py b/SRV/api/v1/ip.py index b5702d3..387ed54 100644 --- a/SRV/api/v1/ip.py +++ b/SRV/api/v1/ip.py @@ -8,7 +8,23 @@ ip_service = IPService() @router.post("/check_ip") async def check_ip_config(data: Dict[str, Any] = Body(...)): - """Проверяет доступность IP-адреса""" + """Проверяет доступность IP-адреса перед его применением. + + Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес, + и возвращает результат проверки. + + Args: + data (Dict[str, Any]): Словарь с данными конфигурации, включая + "ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса. + + Returns: + Dict[str, Any]: Словарь с ключами: + - "available" (bool): Доступен ли IP-адрес. + - "message" (str): Сообщение о результате проверки. + + Raises: + HTTPException: Если произошла внутренняя ошибка (status_code=500). + """ try: result = await ip_service.check_ip_config(data) return result @@ -20,37 +36,63 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)): @router.post("/apply_ip") async def apply_ip_config(data: Dict[str, Any] = Body(...)): - """Применяет проверенные IP-настройки""" + """Применяет проверенные IP-настройки к указанному сетевому профилю. + + Обновляет настройки соединения через NetworkManager, отключает и + переподключает устройство к сети. + + Args: + data (Dict[str, Any]): Словарь с данными конфигурации, включая + "connection_id" и "ipv4_updates" с обновлениями IP-настроек. + + Returns: + dict: Словарь с ключами: + - "success" (bool): Успешность операции. + - "message" (str): Сообщение об успешном применении настроек. + + Raises: + HTTPException: Если подключение не найдено (status_code=404) или + произошла внутренняя ошибка (status_code=500). + """ try: await ip_service.apply_ip_config(data) return { - "success": - True, - "message": - f"Настройки сетевого профиля для {data['connection_id']} применены", + "success": True, + "message": f"Настройки сетевого профиля для {data['connection_id']} применены", } except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Ошибка проверки IP конфигурации: {str(e)}") + raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}") @router.post("/save_ip") async def save_ip_config(data: Dict[str, Any] = Body(...)): - """Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно)""" + """Применяет проверенные IP-настройки для отката сетевого профиля. + + Используется для сохранения и отката настроек сетевого профиля к предыдущему + состоянию через NetworkManager. + + Args: + data (Dict[str, Any]): Словарь с данными конфигурации, включая + "connection_id" и "ipv4_updates" с обновлениями IP-настроек. + + Returns: + dict: Словарь с ключами: + - "success" (bool): Успешность операции. + - "message" (str): Сообщение об успешном применении настроек. + + Raises: + HTTPException: Если подключение не найдено (status_code=404) или + произошла внутренняя ошибка (status_code=500). + """ try: await ip_service.apply_ip_config(data) return { - "success": - True, - "message": - f"Настройки сетевого профиля для {data['connection_id']} применены", + "success": True, + "message": f"Настройки сетевого профиля для {data['connection_id']} применены", } except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: - raise HTTPException( - status_code=500, - detail=f"Ошибка проверки IP конфигурации: {str(e)}") + raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}") \ No newline at end of file diff --git a/SRV/api/v1/network.py b/SRV/api/v1/network.py index 78c0cae..904a14d 100644 --- a/SRV/api/v1/network.py +++ b/SRV/api/v1/network.py @@ -19,11 +19,18 @@ network_service = NetworkService() class ContainerPositionManager: - """ - Класс для управления позициями и состоянием контейнеров + """Класс для управления позициями и состоянием контейнеров. + + Хранит и обновляет координаты (x, y) и состояние (isOpen) для контейнеров + активации и деактивации. """ def __init__(self): + """Инициализирует менеджер позиций контейнеров. + + Создаёт начальные позиции и состояния для контейнеров "activation" и + "deactivation" с координатами (0, 0) и закрытым состоянием (False). + """ self.positions: Dict[str, Dict[str, Union[int, bool]]] = { "activation": { "x": 0, @@ -37,15 +44,35 @@ class ContainerPositionManager: }, } - def get_position_and_state(self, - container_name: str) -> Tuple[int, int, bool]: + def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]: + """Получает текущие координаты и состояние указанного контейнера. + + Args: + container_name (str): Имя контейнера ("activation" или "deactivation"). + + Returns: + Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen). + + Raises: + ValueError: Если указан неверный идентификатор контейнера. + """ container = self.positions.get(container_name) if not container: raise ValueError(f"Неверное имя контейнера: {container_name}") return container["x"], container["y"], container["isOpen"] - def update_position(self, container_id: int, x: int, y: int, - isOpen: bool) -> None: + def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None: + """Обновляет позицию и состояние контейнера по его идентификатору. + + Args: + container_id (int): Идентификатор контейнера (1 для "activation", 2 для "deactivation"). + x (int): Новая координата X. + y (int): Новая координата Y. + isOpen (bool): Новое состояние контейнера (открыт/закрыт). + + Raises: + ValueError: Если container_id не соответствует ни одному контейнеру. + """ container_name = "activation" if container_id == 1 else "deactivation" self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen} @@ -54,20 +81,27 @@ container_manager = ContainerPositionManager() async def get_network_data() -> Dict[str, Dict]: - """ - Возвращает текущие сетевые данные в виде словаря + """Возвращает текущие сетевые данные в виде словаря. + + Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные, + и возвращает их в формате Activation и Deactivation с учётом позиций и состояний + контейнеров. + + Returns: + Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими + данные о соответствующих контейнерах. + + Raises: + HTTPException: Если произошла ошибка при получении данных (status_code=500). """ logger.debug("Вызов get_network_data()") try: network_data = await network_service.get_network_interfaces_container() - activation_interfaces, deactivation_interfaces = _split_interfaces( - network_data.interfaces) + activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces) - activation_x, activation_y, activation_is_open = ( - container_manager.get_position_and_state("activation")) - deactivation_x, deactivation_y, deactivation_is_open = ( - container_manager.get_position_and_state("deactivation")) + activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation")) + deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation")) activation = Activation( id=1, @@ -90,16 +124,20 @@ async def get_network_data() -> Dict[str, Dict]: } except Exception as e: logger.error(f"Ошибка в get_network_data: {e}") - raise HTTPException(status_code=500, - detail="Не удалось получить сетевые данные") + raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные") def _split_interfaces( interfaces: List[Union[NetworkInterface, WiFiDevice]], -) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[ - NetworkInterface, WiFiDevice]]]: - """ - Разделяет интерфейсы на активные и неактивные +) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]: + """Разделяет интерфейсы на активные и неактивные. + + Args: + interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список сетевых интерфейсов. + + Returns: + Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]: + Кортеж из списков активных и неактивных интерфейсов. """ activation_interfaces = [] deactivation_interfaces = [] @@ -112,13 +150,29 @@ def _split_interfaces( @router.post("/update_container_position") -async def update_container_position(data: Dict[str, Union[int, - bool]] = Body(...)): - """ - Обновляет позицию и состояние контейнера +async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)): + """Обновляет позицию и состояние контейнера. + + Принимает данные о новом положении и состоянии контейнера и обновляет их + в менеджере позиций. + + Args: + data (Dict[str, Union[int, bool]]): Словарь с ключами: + - "container_id" (int): Идентификатор контейнера (1 или 2). + - "container_x" (int): Новая координата X. + - "container_y" (int): Новая координата Y. + - "isOpen" (bool): Новое состояние контейнера (открыт/закрыт). + + Returns: + dict: Словарь с ключами: + - "success" (bool): Успешность операции. + - "message" (str): Сообщение об успешном обновлении. + + Raises: + HTTPException: Если отсутствуют обязательные поля (status_code=400) или + типы данных неверны. """ try: - required_keys = [ "container_id", "container_x", "container_y", "isOpen" ] @@ -132,33 +186,37 @@ async def update_container_position(data: Dict[str, Union[int, container_y = data["container_y"] is_open = data["isOpen"] - # Проверка типов if (not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y, int) or not isinstance(is_open, bool)): raise ValueError("Неверный тип данных в запросе") - container_manager.update_position(container_id, container_x, - container_y, is_open) + container_manager.update_position(container_id, container_x, container_y, is_open) return { "success": True, "message": "Позиция и состояние контейнера обновлены" } except Exception as e: logger.error(f"Ошибка обновления позиции контейнера: {e}") - raise HTTPException( - status_code=400, - detail=f"Не удалось обновить положение контейнера: {str(e)}", - ) + raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}") @router.get("/get_hostname") async def get_hostname(): + """Возвращает имя хоста системы. + + Выполняет запрос к сервису для получения имени хоста. + + Returns: + dict: Словарь с ключом "hostname" и значением имени хоста. + + Raises: + HTTPException: Если произошла ошибка при получении имени хоста (status_code=500). + """ try: hostname_info = await network_service.get_hostname() return hostname_info except Exception as e: logger.error(f"Ошибка в get_hostname: {e}") - raise HTTPException(status_code=500, - detail="Не удалось получить имя хоста") + raise HTTPException(status_code=500, detail="Не удалось получить имя хоста") \ No newline at end of file diff --git a/SRV/api/v1/subnet.py b/SRV/api/v1/subnet.py index e500010..43e4d1c 100644 --- a/SRV/api/v1/subnet.py +++ b/SRV/api/v1/subnet.py @@ -11,7 +11,22 @@ router = APIRouter() @router.get("/devices", response_model=SubnetScanResult) def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None): - """Сканирует локальную подсеть и возвращает данные об устройствах""" + """Сканирует локальную подсеть и возвращает данные об устройствах. + + Выполняет сканирование указанной подсети для обнаружения активных устройств + с возможностью проверки указанных портов. + + Args: + subnet (str, optional): Адрес подсети в формате CIDR (например, "192.168.1.0/24"). + По умолчанию "192.168.1.0/24". + ports (List[int], optional): Список портов для сканирования. По умолчанию None. + + Returns: + SubnetScanResult: Объект с результатами сканирования (устройства и их данные). + + Raises: + HTTPException: Если произошла ошибка сканирования (status_code=500). + """ try: subnet_service = SubnetService() result = subnet_service.scan_subnet(subnet, ports) @@ -21,7 +36,19 @@ def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None): @router.get("/wifi", response_model=WifiScanResult) def scan_wifi(timeout: int = 30): - """Сканирует Wi-Fi точки доступа и возвращает данные о них""" + """Сканирует Wi-Fi точки доступа и возвращает данные о них. + + Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом. + + Args: + timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30. + + Returns: + WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей). + + Raises: + HTTPException: Если произошла ошибка сканирования Wi-Fi (status_code=500). + """ try: wifi_service = WifiService() result = wifi_service.scan_wifi(timeout=timeout) @@ -31,7 +58,21 @@ def scan_wifi(timeout: int = 30): @router.post("/devices/{mac}/note") def add_note(mac: str, request: NoteRequest): - """Добавляет заметку для устройства по MAC-адресу""" + """Добавляет заметку для устройства по MAC-адресу. + + Привязывает заметку к устройству, идентифицируемому по MAC-адресу. + + Args: + mac (str): MAC-адрес устройства. + request (NoteRequest): Объект с полем "note" для текста заметки. + + Returns: + dict: Словарь с сообщением об успешном добавлении заметки. + + Raises: + HTTPException: Если устройство не найдено (status_code=404), заметка уже + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: subnet_service = SubnetService() success, error_message = subnet_service.add_note(mac, request.note) @@ -48,7 +89,21 @@ def add_note(mac: str, request: NoteRequest): @router.put("/devices/{mac}/note") def update_note(mac: str, request: NoteRequest): - """Обновляет заметку для устройства по MAC-адресу""" + """Обновляет заметку для устройства по MAC-адресу. + + Изменяет существующую заметку, привязанную к устройству. + + Args: + mac (str): MAC-адрес устройства. + request (NoteRequest): Объект с полем "note" для нового текста заметки. + + Returns: + dict: Словарь с сообщением об успешном обновлении заметки. + + Raises: + HTTPException: Если устройство не найдено (status_code=404), заметка не + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: subnet_service = SubnetService() success, error_message = subnet_service.update_note(mac, request.note) @@ -65,7 +120,20 @@ def update_note(mac: str, request: NoteRequest): @router.delete("/devices/{mac}/note") def delete_note(mac: str): - """Удаляет заметку для устройства по MAC-адресу""" + """Удаляет заметку для устройства по MAC-адресу. + + Удаляет привязанную заметку для указанного устройства. + + Args: + mac (str): MAC-адрес устройства. + + Returns: + dict: Словарь с сообщением об успешном удалении заметки. + + Raises: + HTTPException: Если устройство не найдено (status_code=404), заметка не + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: subnet_service = SubnetService() success, error_message = subnet_service.delete_note(mac) @@ -82,7 +150,21 @@ def delete_note(mac: str): @router.post("/wifi/{ssid}/note") def add_wifi_note(ssid: str, request: NoteRequest): - """Добавляет заметку для сети Wi-Fi по SSID""" + """Добавляет заметку для сети Wi-Fi по SSID. + + Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID. + + Args: + ssid (str): Идентификатор Wi-Fi сети. + request (NoteRequest): Объект с полем "note" для текста заметки. + + Returns: + dict: Словарь с сообщением об успешном добавлении заметки. + + Raises: + HTTPException: Если сеть не найдена (status_code=404), заметка уже + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: wifi_service = WifiService() success, error_message = wifi_service.add_note(ssid, request.note) @@ -99,7 +181,21 @@ def add_wifi_note(ssid: str, request: NoteRequest): @router.put("/wifi/{ssid}/note") def update_wifi_note(ssid: str, request: NoteRequest): - """Обновляет заметку для сети Wi-Fi по SSID""" + """Обновляет заметку для сети Wi-Fi по SSID. + + Изменяет существующую заметку, привязанную к Wi-Fi сети. + + Args: + ssid (str): Идентификатор Wi-Fi сети. + request (NoteRequest): Объект с полем "note" для нового текста заметки. + + Returns: + dict: Словарь с сообщением об успешном обновлении заметки. + + Raises: + HTTPException: Если сеть не найдена (status_code=404), заметка не + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: wifi_service = WifiService() success, error_message = wifi_service.update_note(ssid, request.note) @@ -116,7 +212,20 @@ def update_wifi_note(ssid: str, request: NoteRequest): @router.delete("/wifi/{ssid}/note") def delete_wifi_note(ssid: str): - """Удаляет заметку для сети Wi-Fi по SSID""" + """Удаляет заметку для сети Wi-Fi по SSID. + + Удаляет привязанную заметку для указанной Wi-Fi сети. + + Args: + ssid (str): Идентификатор Wi-Fi сети. + + Returns: + dict: Словарь с сообщением об успешном удалении заметки. + + Raises: + HTTPException: Если сеть не найдена (status_code=404), заметка не + существует (status_code=400), или произошла внутренняя ошибка (status_code=500). + """ try: wifi_service = WifiService() success, error_message = wifi_service.delete_note(ssid) @@ -133,7 +242,20 @@ def delete_wifi_note(ssid: str): @router.post("/wifi/connect") def connect_to_wifi(request: WifiConnectRequest): - """Подключается к Wi-Fi сети по SSID и паролю.""" + """Подключается к Wi-Fi сети по SSID и паролю. + + Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных + учетных данных. + + Args: + request (WifiConnectRequest): Объект с полями "ssid" и "password". + + Returns: + dict: Словарь с сообщением об успешном подключении. + + Raises: + HTTPException: Если подключение не удалось (status_code=400). + """ wifi_service = WifiConnectionService() success, message = wifi_service.connect(request.ssid, request.password) if not success: @@ -142,7 +264,19 @@ def connect_to_wifi(request: WifiConnectRequest): @router.post("/wifi/disconnect") def disconnect_from_wifi(request: WifiDisconnectRequest): - """Отключается от Wi-Fi сети по указанному SSID.""" + """Отключается от Wi-Fi сети по указанному SSID. + + Выполняет отключение от указанной Wi-Fi сети. + + Args: + request (WifiDisconnectRequest): Объект с полем "ssid". + + Returns: + dict: Словарь с сообщением об успешном отключении. + + Raises: + HTTPException: Если отключение не удалось (status_code=400). + """ wifi_service = WifiConnectionService() success, message = wifi_service.disconnect(request.ssid) if not success: @@ -151,7 +285,18 @@ def disconnect_from_wifi(request: WifiDisconnectRequest): @router.get("/wifi/status") def get_wifi_status(): - """Проверяет статус текущего подключения к Wi-Fi.""" + """Проверяет статус текущего подключения к Wi-Fi. + + Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно. + + Returns: + dict: Словарь с ключами: + - "status" (str): Состояние ("connected" или другое). + - "ssid" (str, optional): SSID текущей сети, если подключено. + + Raises: + HTTPException: Если проверка статуса не удалась (status_code=400). + """ wifi_service = WifiConnectionService() success, message = wifi_service.get_status() if not success: diff --git a/SRV/api/v1/websocket.py b/SRV/api/v1/websocket.py index 8001103..271b898 100644 --- a/SRV/api/v1/websocket.py +++ b/SRV/api/v1/websocket.py @@ -12,27 +12,57 @@ router = APIRouter() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) - class WebSocketManager: - """ - Класс для управления подключениями к WebSocket-серверу + """Класс для управления подключениями к WebSocket-серверу. + + Обрабатывает подключение, отключение и трансляцию сообщений для клиентов, + подключённых через WebSocket. """ def __init__(self): - """Инициализация менеджера подключений к WebSocket-серверу""" + """Инициализирует менеджер подключений к WebSocket-серверу. + + Создаёт пустое множество для хранения активных соединений. + """ self.active_connections: Set[WebSocket] = set() async def connect(self, websocket: WebSocket): - """Подключение клиента к WebSocket-серверу""" + """Подключает клиента к WebSocket-серверу. + + Принимает подключение и добавляет WebSocket в список активных соединений. + + Args: + websocket (WebSocket): Объект WebSocket для подключения. + + Raises: + Exception: Если произошла ошибка при принятии подключения. + """ await websocket.accept() self.active_connections.add(websocket) def disconnect(self, websocket: WebSocket): - """Отключение клиента от WebSocket-серверу""" + """Отключает клиента от WebSocket-серверу. + + Удаляет WebSocket из списка активных соединений. + + Args: + websocket (WebSocket): Объект WebSocket для отключения. + """ self.active_connections.remove(websocket) async def broadcast(self, message: dict): - """Трансляция сообщений всем подключенным клиентам""" + """Транслирует сообщения всем подключённым клиентам. + + Отправляет JSON-данные всем активным соединениям, обрабатывая + отключения клиентов. + + Args: + message (dict): Сообщение для трансляции в формате JSON. + + Raises: + WebSocketDisconnect: Если клиент неожиданно отключился. + Exception: Если произошла другая ошибка при отправке. + """ for connection in list(self.active_connections): try: if connection.client_state == WebSocketState.CONNECTED: @@ -49,15 +79,25 @@ manager = WebSocketManager() @router.websocket("/api/v1") async def websocket_endpoint(websocket: WebSocket): - """Обработчик WebSocket-соединения""" + """Обработчик WebSocket-соединения. + + Управляет соединением клиента, принимает данные для обновления позиций + контейнеров и транслирует сетевые данные. + + Args: + websocket (WebSocket): Объект WebSocket для обработки соединения. + + Raises: + WebSocketDisconnect: Если клиент отключился. + Exception: Если произошла ошибка при обработке данных. + """ await manager.connect(websocket) try: while True: data = await websocket.receive_json() logger.info(f"Полученные данные: {data}") - if all(key in data - for key in ["container_id", "container_x", "container_y"]): + if all(key in data for key in ["container_id", "container_x", "container_y"]): await update_container_position(data) network_data = await get_network_data() @@ -77,12 +117,25 @@ async def websocket_endpoint(websocket: WebSocket): @router.on_event("startup") async def startup_event(): - """Запуск таска для трансляции сетевых данных""" + """Запуск таска для трансляции сетевых данных при старте приложения. + + Создаёт асинхронную задачу для периодической трансляции данных. + + Raises: + Exception: Если произошла ошибка при запуске таска. + """ asyncio.create_task(broadcast_data()) async def broadcast_data(): - """Таск для трансляции сетевых данных""" + """Таск для периодической трансляции сетевых данных. + + Выполняет сканирование сети и отправляет данные всем подключённым клиентам + с интервалом в 2 секунды. + + Raises: + Exception: Если произошла ошибка при получении данных или трансляции. + """ while True: try: network_data = await get_network_data() @@ -96,4 +149,4 @@ async def broadcast_data(): await manager.broadcast(network_data) except Exception as e: logger.error(f"Ошибка в broadcast_data:{e}") - await asyncio.sleep(2) + await asyncio.sleep(2) \ No newline at end of file diff --git a/SRV/services/arp_service.py b/SRV/services/arp_service.py index 97ef2eb..d68464d 100644 --- a/SRV/services/arp_service.py +++ b/SRV/services/arp_service.py @@ -5,10 +5,21 @@ from netaddr import IPNetwork class NetworkScanner: - """Иницилазицая сетевого сканера""" + """Инициализация сетевого сканера. + + Предоставляет методы для сканирования сети с использованием ARP-запросов и + вывода информации о клиентах. + """ def __init__(self, interface=None): - """Инициализация параметров интерфейсов""" + """Инициализация параметров сетевого сканера. + + Args: + interface (str, optional): Название сетевого интерфейса. По умолчанию + используется интерфейс по умолчанию из netifaces.gateways(). + + Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес. + """ self.interface = ( interface or netifaces.gateways()["default"][netifaces.AF_INET][1]) self.ip_info = netifaces.ifaddresses(self.interface).get( @@ -20,13 +31,34 @@ class NetworkScanner: self.interface)[netifaces.AF_LINK][0]["addr"] def _create_arp_packet(self, target_ip): - """Создание пакета ARP для отправки""" + """Создаёт ARP-пакет для отправки на указанный IP-адрес. + + Args: + target_ip (str): Целевой IP-адрес или подсеть для сканирования. + + Returns: + scapy.packet.Packet: Сформированный ARP-пакет с Ethernet-заголовком. + """ arp = ARP(pdst=target_ip) ether = Ether(dst="ff:ff:ff:ff:ff:ff") return ether / arp def scan_network(self, attempts=3, timeout=2): - """Подготовка к сканированию сети""" + """Сканирует сеть для обнаружения активных клиентов. + + Args: + attempts (int, optional): Количество попыток отправки ARP-запросов. + По умолчанию 3. + timeout (int, optional): Таймаут ожидания ответа в секундах. + По умолчанию 2. + + Returns: + List[dict]: Список словарей с ключами "ip" и "mac", представляющих + обнаруженных клиентов, включая локальные IP-адреса. + + Note: + Учитывает локальные IP-адреса интерфейса и удаляет дубликаты. + """ all_clients = [] for network in self.networks: @@ -41,30 +73,35 @@ class NetworkScanner: verbose=0)[0] for _, received in result: client = {"ip": received.psrc, "mac": received.hwsrc} - if client not in clients: # Избегаем дублирования клиентов + if client not in clients: clients.append(client) all_clients.extend(clients) - # Добавляет все локальные IP-адреса интерфейса как клиенты for ip_info in self.ip_info: local_client = {"ip": ip_info["addr"], "mac": self.local_mac} if local_client not in all_clients: all_clients.append(local_client) - # Удаление дубликатов unique_clients = {tuple(client.items()) for client in all_clients} return [dict(client) for client in unique_clients] def print_clients(self, clients): - """Лист адресов клиентов""" + """Выводит список адресов клиентов в формате таблицы. + + Args: + clients (List[dict]): Список словарей с ключами "ip" и "mac", + представляющих клиентов. + + Выводит IP-адреса и MAC-адреса в отсортированном по IP порядке. + """ print("Доступные устройства в сети:") print("IP" + " " * 18 + "MAC") for client in sorted(clients, key=lambda x: x["ip"]): print(f"{client['ip']:<16} {client['mac']}") - +# Оставил нативный запуск сервиса для тестов if __name__ == "__main__": scanner = NetworkScanner() clients = scanner.scan_network() - scanner.print_clients(clients) + scanner.print_clients(clients) \ No newline at end of file diff --git a/SRV/services/auth_service.py b/SRV/services/auth_service.py index 8ed1435..8675f1b 100644 --- a/SRV/services/auth_service.py +++ b/SRV/services/auth_service.py @@ -1,4 +1,3 @@ -# services/auth_service.py from jose import JWTError, jwt from datetime import datetime, timedelta from fastapi import HTTPException, Depends, Request diff --git a/SRV/services/connect_service.py b/SRV/services/connect_service.py index d33f970..984bfab 100644 --- a/SRV/services/connect_service.py +++ b/SRV/services/connect_service.py @@ -6,11 +6,28 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(level logger = logging.getLogger(__name__) class WifiConnectionService: + """Сервис для управления подключением к Wi-Fi сетям с использованием nmcli. + + Предоставляет методы для подключения, отключения и проверки статуса Wi-Fi соединений. + """ + def __init__(self): + """Инициализирует сервис Wi-Fi. + + Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя. + """ self.interface = self._get_wifi_interface() def _get_wifi_interface(self) -> str: - """Автоматически определяет Wi-Fi-интерфейс.""" + """Автоматически определяет Wi-Fi-интерфейс. + + Returns: + str: Название Wi-Fi-интерфейса (например, "wlan0") или пустая строка, + если интерфейс не найден или Wi-Fi отключён. + + Raises: + Exception: Если произошла ошибка при выполнении команд nmcli. + """ try: result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True) if "enabled" in result.stdout.lower(): @@ -29,7 +46,21 @@ class WifiConnectionService: return "" def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]: - """Подключается к Wi-Fi сети по SSID и паролю.""" + """Подключается к Wi-Fi сети по SSID и паролю. + + Args: + ssid (str): Идентификатор сети Wi-Fi. + password (str): Пароль для подключения. + + Returns: + Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке). + - Успех: (True, None). + - Ошибка: (False, сообщение об ошибке). + + Raises: + subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой. + Exception: Если произошла неизвестная ошибка при выполнении. + """ try: if not self.interface: return False, "Wi-Fi-интерфейс не доступен" @@ -63,7 +94,20 @@ class WifiConnectionService: return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}" def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]: - """Отключается от Wi-Fi сети по указанному SSID.""" + """Отключается от Wi-Fi сети по указанному SSID. + + Args: + ssid (str): Идентификатор сети Wi-Fi для отключения. + + Returns: + Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке). + - Успех: (True, None или сообщение, если подключение не найдено). + - Ошибка: (False, сообщение об ошибке). + + Raises: + subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой. + Exception: Если произошла неизвестная ошибка при выполнении. + """ try: if not self.interface: return False, "Wi-Fi-интерфейс не доступен" @@ -91,7 +135,16 @@ class WifiConnectionService: return False, f"Ошибка отключения от {ssid}: {str(e)}" def get_status(self) -> Tuple[bool, Optional[str]]: - """Проверяет статус текущего подключения к Wi-Fi.""" + """Проверяет статус текущего подключения к Wi-Fi. + + Returns: + Tuple[bool, Optional[str]]: Кортеж (успех, SSID или сообщение об ошибке). + - Успех: (True, SSID текущей сети). + - Ошибка или отсутствие подключения: (False, сообщение об ошибке). + + Raises: + Exception: Если произошла ошибка при выполнении команды nmcli. + """ try: if not self.interface: return False, "Wi-Fi-интерфейс не доступен" diff --git a/SRV/services/ip_service.py b/SRV/services/ip_service.py index 99e3bea..5115343 100644 --- a/SRV/services/ip_service.py +++ b/SRV/services/ip_service.py @@ -1,4 +1,3 @@ -import asyncio from typing import Dict, Any from sdbus_async.networkmanager import ( NetworkManager, @@ -12,17 +11,28 @@ from services.arp_service import NetworkScanner class IPService: - """ - Сервис проверки доступности IP-адреса и применения настроек сети + """Сервис проверки доступности 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""" + """Перезапускает соединение с системной шиной 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: @@ -30,7 +40,23 @@ class IPService: self.bus = new_bus async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]: - """Проверяет доступность IP-адреса перед его применением""" + """Проверяет доступность IP-адреса перед его применением. + + Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес. + Перезапускает шину D-Bus перед выполнением. + + Args: + args (Dict[str, Any]): Словарь с данными, включая "ipv4_updates" + с полем "ipv4.address-data" для нового IP-адреса. + + Returns: + Dict[str, Any]: Словарь с ключами: + - "available" (bool): Доступен ли IP-адрес. + - "message" (str): Сообщение о результате проверки. + + Raises: + Exception: Если произошла ошибка при сканировании ARP. + """ self._reset_bus() clients = self.arp_scanner.scan_network() new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"] @@ -45,7 +71,23 @@ class IPService: } async def apply_ip_config(self, args: Dict[str, Any]) -> None: - """Применяет проверенные настройки IP""" + """Применяет проверенные настройки IP к указанному соединению. + + Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager, + отключает и переподключает устройство к сети. + + Args: + args (Dict[str, Any]): Словарь с данными, включая: + - "connection_id" (str): Идентификатор соединения. + - "ipv4_updates" (Dict): Обновления настроек IPv4. + + Raises: + ValueError: Если соединение с указанным ID не найдено. + RuntimeError: Если устройство для интерфейса не найдено. + + Note: + Требуются права для изменения настроек через NetworkManager. + """ self._reset_bus() nm = NetworkManager() settings = NetworkManagerSettings() @@ -77,10 +119,8 @@ class IPService: if await device.interface == interface_name: await device.disconnect() print(f"Отключён от {interface_name}") - await nm.activate_connection(connection=connection_paths[0], - device=device_path) + await nm.activate_connection(connection=connection_paths[0],device=device_path) print(f"Переподключён к {args['connection_id']}") return - raise RuntimeError( - f"Устройство для интерфейса {interface_name} не найдено") + raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено") \ No newline at end of file diff --git a/SRV/services/network_service.py b/SRV/services/network_service.py index 3d60ff9..b047808 100644 --- a/SRV/services/network_service.py +++ b/SRV/services/network_service.py @@ -23,16 +23,35 @@ import subprocess class NetworkService: - """Сервис для взаимодействия с NetworkManager через D-Bus""" + """Сервис для взаимодействия с NetworkManager через D-Bus. + + Предоставляет асинхронные методы для получения информации о сетевых интерфейсах, + их конфигурациях IP, Wi-Fi и состояниях. + """ def __init__(self): + """Инициализирует сервис NetworkManager. + + Устанавливает системный D-Bus и создаёт объекты для взаимодействия с + NetworkManager и его настройками. + """ self.system_bus = sd_bus_open_system() self.nm = NetworkManager(self.system_bus) self.nm_settings = NetworkManagerSettings(self.system_bus) - async def get_ip_method(self, device: NetworkDeviceGeneric, - ip_type: str) -> str: - """Получает метод IP для устройства""" + async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str: + """Получает метод IP (например, "auto", "manual") для указанного устройства. + + Args: + device (NetworkDeviceGeneric): Объект устройства NetworkManager. + ip_type (str): Тип IP (например, "ipv4" или "ipv6"). + + Returns: + str: Метод IP или "unknown" в случае ошибки. + + Raises: + Exception: Если произошла ошибка при получении настроек соединения. + """ try: config = await device.get_applied_connection() settings = config[0].get(ip_type, {}) @@ -41,64 +60,80 @@ class NetworkService: return "unknown" async def get_connection_name(self, device: NetworkDeviceGeneric) -> str: - """Извлекает имя соединения (GENERAL.CONNECTION) для устройства""" + """Извлекает имя соединения (GENERAL.CONNECTION) для устройства. + + Args: + device (NetworkDeviceGeneric): Объект устройства NetworkManager. + + Returns: + str: Имя соединения или "unknown" в случае ошибки. + + Raises: + Exception: Если произошла ошибка при получении настроек соединения. + """ try: applied_connection = await device.get_applied_connection() connection_settings = applied_connection[0] connection_info = connection_settings.get("connection", {}) return str(connection_info.get("id", ("s", "unknown"))[1]) except Exception as e: - # print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}") - # срёт ошибки дл отключенных интерфейсов - # добавить потом условия для отключенных интерфейсов return "unknown" - async def get_ip_config(self, device: NetworkDeviceGeneric, - ip_version: str) -> List[IndividualIPConfig]: - """Получает конфигурацию IP для устройства""" + async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]: + """Получает конфигурацию IP для устройства. + + Args: + device (NetworkDeviceGeneric): Объект устройства NetworkManager. + ip_version (str): Версия IP ("4" или "6"). + + Returns: + List[IndividualIPConfig]: Список конфигураций IP, включая адреса и маршруты. + + Raises: + Exception: Если произошла ошибка при получении данных IP. + """ configs = [] device_ip = await getattr(device, f"ip{ip_version}_config") if device_ip != "/": - ip_config = globals()[f"IPv{ip_version}Config"](device_ip, - self.system_bus) + ip_config = globals()[f"IPv{ip_version}Config"](device_ip, self.system_bus) address_data = await ip_config.address_data for address in address_data: config = IndividualIPConfig( address=str(address["address"][1]), routes=[ globals()[f"IPv{ip_version}Route"]( - destination=route.get( - "dest", - ("s", - "0.0.0.0" if ip_version == "4" else "::"))[1], + destination=route.get("dest", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1], prefix=route.get("prefix", ("u", 0))[1], - next_hop=route.get( - "next-hop", - ("s", - "0.0.0.0" if ip_version == "4" else "::"), - )[1], + next_hop=route.get("next-hop", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1], metric=route.get("metric", ("u", 0))[1], ) for route in await ip_config.route_data ], - method=await self.get_ip_method(device, - f"ipv{ip_version}"), + method=await self.get_ip_method(device, f"ipv{ip_version}"), gateway=str(await ip_config.gateway), dns=[ str(ns["address"][1]) if ip_version == "4" else str(ns) for ns in await getattr( ip_config, - "nameserver_data" if ip_version == - "4" else "nameservers", + "nameserver_data" if ip_version == "4" else "nameservers", ) ], ) configs.append(config) return configs - async def get_wifi_info( - self, wifi_device: NetworkDeviceWireless - ) -> Dict[str, Optional[Union[str, int]]]: - """Получает информацию о Wi-Fi устройстве""" + async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]: + """Получает информацию о Wi-Fi устройстве. + + Args: + wifi_device (NetworkDeviceWireless): Объект Wi-Fi устройства NetworkManager. + + Returns: + Dict[str, Optional[Union[str, int]]]: Словарь с данными Wi-Fi (SSID, BSSID, + bitrate, strength, frequency, mode), где значения могут быть None. + + Raises: + Exception: Если произошла ошибка при получении данных точки доступа. + """ active_ap = await wifi_device.active_access_point wifi_info = { "ssid": None, @@ -123,7 +158,18 @@ class NetworkService: self, initial_positions: Dict[str, Tuple[int, int]] = None ) -> List[Union[NetworkInterface, WiFiDevice]]: - """Получает список сетевых интерфейсов""" + """Получает список сетевых интерфейсов. + + Args: + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None (используются (100, 100)). + + Returns: + List[Union[NetworkInterface, WiFiDevice]]: Список объектов интерфейсов, + включая Wi-Fi устройства. + + Исключает интерфейсы типа LOOPBACK и неактивные состояния (state == "10"). + """ devices_paths = await self.nm.get_devices() network_interfaces = [] initial_positions = initial_positions or {} @@ -141,43 +187,37 @@ class NetworkService: continue common_data = { - "name": - interface_name, - "type": - device_type, - "device_state": - device_state, - "active_connection": - await self.get_connection_name(generic_device), - "mac_address": - str(await generic_device.hw_address), - "device_mtu": - str(await generic_device.mtu), - "ipv4": - IPConfig(configurations=await self.get_ip_config( - generic_device, "4")), - "ipv6": - IPConfig(configurations=await self.get_ip_config( - generic_device, "6")), - "x": - initial_positions.get(interface_name, (100, 100))[0], - "y": - initial_positions.get(interface_name, (100, 100))[1], + "name": interface_name, + "type": device_type, + "device_state": device_state, + "active_connection": await self.get_connection_name(generic_device), + "mac_address": str(await generic_device.hw_address), + "device_mtu": str(await generic_device.mtu), + "ipv4": IPConfig(configurations=await self.get_ip_config(generic_device, "4")), + "ipv6": IPConfig(configurations=await self.get_ip_config(generic_device, "6")), + "x": initial_positions.get(interface_name, (100, 100))[0], + "y": initial_positions.get(interface_name, (100, 100))[1], } if device_type == "WIFI": - wifi_device = NetworkDeviceWireless(device_path, - self.system_bus) + wifi_device = NetworkDeviceWireless(device_path, self.system_bus) wifi_info = await self.get_wifi_info(wifi_device) - network_interfaces.append( - WiFiDevice(**common_data, **wifi_info)) + network_interfaces.append(WiFiDevice(**common_data, **wifi_info)) else: network_interfaces.append(NetworkInterface(**common_data)) return network_interfaces async def get_hostname(self) -> Dict[str, str]: - """Возвращает имя хоста из команды hostname в формате JSON""" + """Возвращает имя хоста из команды hostname в формате JSON. + + Returns: + Dict[str, str]: Словарь с ключом "hostname" и значением имени хоста. + + Raises: + RuntimeError: Если произошла ошибка при выполнении команды hostname + или обработке вывода. + """ try: result = subprocess.run(["hostname"], capture_output=True, @@ -195,8 +235,15 @@ class NetworkService: state: str, initial_positions: Dict[str, Tuple[int, int]] = None ) -> List[NetworkInterface]: - """ - Получает список интерфейсов по состоянию + """Получает список интерфейсов по указанному состоянию. + + Args: + state (str): Состояние интерфейса (например, "100" для активных). + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + + Returns: + List[NetworkInterface]: Список интерфейсов с указанным состоянием. """ all_interfaces = await self.get_network_interfaces(initial_positions) return [ @@ -211,10 +258,21 @@ class NetworkService: container_x: int = 0, container_y: int = 0, ) -> NetworkInterfacesContainer: - """Получает объект, содержащий список интерфейсов и координаты контейнера""" + """Получает объект, содержащий список интерфейсов и координаты контейнера. + + Args: + state (Optional[str], optional): Состояние интерфейсов (например, "100"). + По умолчанию None (все интерфейсы). + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + container_x (int, optional): Координата X контейнера. По умолчанию 0. + container_y (int, optional): Координата Y контейнера. По умолчанию 0. + + Returns: + NetworkInterfacesContainer: Объект с интерфейсами и координатами контейнера. + """ if state: - interfaces = await self.get_interfaces_by_state( - state, initial_positions) + interfaces = await self.get_interfaces_by_state(state, initial_positions) else: interfaces = await self.get_network_interfaces(initial_positions) return NetworkInterfacesContainer(interfaces=interfaces, @@ -225,7 +283,15 @@ class NetworkService: self, initial_positions: Optional[Dict[str, Tuple[int, int]]] = None, ) -> NetworkInterfacesContainer: - """Получает контейнер с сетевыми интерфейсами и координатами контейнера""" + """Получает контейнер с сетевыми интерфейсами и координатами контейнера. + + Args: + initial_positions (Optional[Dict[str, Tuple[int, int]]], optional): + Словарь начальных координат (x, y) для интерфейсов. По умолчанию None. + + Returns: + NetworkInterfacesContainer: Объект с интерфейсами и координатами (0, 0). + """ interfaces = await self.get_network_interfaces(initial_positions) return NetworkInterfacesContainer(interfaces=interfaces, container_x=0, @@ -235,14 +301,30 @@ class NetworkService: self, initial_positions: Dict[str, Tuple[int, int]] = None ) -> List[NetworkInterface]: - """Получает список активных сетевых интерфейсов""" + """Получает список активных сетевых интерфейсов. + + Args: + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + + Returns: + List[NetworkInterface]: Список активных интерфейсов (state == "100"). + """ return await self.get_interfaces_by_state("100", initial_positions) async def get_deactivated_interfaces( self, initial_positions: Dict[str, Tuple[int, int]] = None ) -> List[NetworkInterface]: - """Получает список неактивных сетевых интерфейсов""" + """Получает список неактивных сетевых интерфейсов. + + Args: + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + + Returns: + List[NetworkInterface]: Список интерфейсов с state != "100". + """ all_interfaces = await self.get_network_interfaces(initial_positions) return [ interface for interface in all_interfaces @@ -255,9 +337,18 @@ class NetworkService: container_x: int = 0, container_y: int = 0, ) -> NetworkInterfacesContainer: - """Получает объект, содержащий список активных интерфейсов и координаты контейнера""" - return await self.get_interfaces_container("100", initial_positions, - container_x, container_y) + """Получает объект, содержащий список активных интерфейсов и координаты контейнера. + + Args: + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + container_x (int, optional): Координата X контейнера. По умолчанию 0. + container_y (int, optional): Координата Y контейнера. По умолчанию 0. + + Returns: + NetworkInterfacesContainer: Объект с активными интерфейсами и координатами. + """ + return await self.get_interfaces_container("100", initial_positions, container_x, container_y) async def get_deactivated_interfaces_container( self, @@ -265,8 +356,16 @@ class NetworkService: container_x: int = 0, container_y: int = 0, ) -> NetworkInterfacesContainer: - """Получает объект, содержащий список неактивных интерфейсов и координаты контейнера""" + """Получает объект, содержащий список неактивных интерфейсов и координаты контейнера. + + Args: + initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных + координат (x, y) для интерфейсов. По умолчанию None. + container_x (int, optional): Координата X контейнера. По умолчанию 0. + container_y (int, optional): Координата Y контейнера. По умолчанию 0. + + Returns: + NetworkInterfacesContainer: Объект с неактивными интерфейсами и координатами. + """ interfaces = await self.get_deactivated_interfaces(initial_positions) - return NetworkInterfacesContainer(interfaces=interfaces, - container_x=container_x, - container_y=container_y) + return NetworkInterfacesContainer(interfaces=interfaces, container_x=container_x, container_y=container_y) \ No newline at end of file diff --git a/SRV/services/subnet_service.py b/SRV/services/subnet_service.py index e25fcb6..1733533 100644 --- a/SRV/services/subnet_service.py +++ b/SRV/services/subnet_service.py @@ -11,7 +11,6 @@ from functools import lru_cache import json from pathlib import Path from datetime import datetime, timedelta -from zoneinfo import ZoneInfo import subprocess from models.subnet import SubnetScanResult, Device @@ -26,11 +25,33 @@ logging.basicConfig( logger = logging.getLogger(__name__) class NetworkScanner: + """Класс для сканирования подсети с использованием ARP-запросов. + + Выполняет обнаружение устройств в указанной подсети через ARP-пакеты. + """ + def __init__(self, interface: str): + """Инициализирует сканер сети с указанным сетевым интерфейсом. + + Args: + interface (str): Название сетевого интерфейса (например, "eth0"). + """ self.interface = interface conf.iface = interface def scan(self, subnet: str) -> List[Tuple[str, str]]: + """Сканирует подсеть с использованием ARP-запросов. + + Args: + subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24"). + + Returns: + List[Tuple[str, str]]: Список кортежей (IP-адрес, MAC-адрес) обнаруженных + устройств. + + Raises: + Exception: Если произошла ошибка при отправке ARP-пакетов. + """ try: arp = ARP(pdst=subnet) ether = Ether(dst="ff:ff:ff:ff:ff:ff") @@ -46,13 +67,42 @@ class NetworkScanner: return [] class PortScanner: + """Класс для сканирования открытых портов на устройствах. + + Выполняет сканирование портов с использованием TCP SYN-запросов. + """ + def __init__(self, interface: str): + """Инициализирует сканер портов с указанным сетевым интерфейсом. + + Args: + interface (str): Название сетевого интерфейса (например, "eth0"). + """ self.interface = interface def scan(self, ip: str) -> List[str]: + """Сканирует открытые порты на указанном IP-адресе. + + Args: + ip (str): IP-адрес устройства для сканирования. + + Returns: + List[str]: Список имён открытых портов (например, "HTTP", "SSH"). + + Raises: + Exception: Если произошла ошибка при сканировании портов. + """ open_ports = [] def check_port(port: int) -> Optional[str]: + """Проверяет доступность конкретного порта. + + Args: + port (int): Номер порта для проверки. + + Returns: + Optional[str]: Имя порта, если открыт, иначе None. + """ try: pkt = sr1( IP(dst=ip) / TCP(dport=port, flags="S"), @@ -80,13 +130,33 @@ class PortScanner: return [] class DeviceInfoCollector: + """Класс для сбора информации об устройствах, включая вендоров и хостнеймы. + + Управляет кэшированием данных NetBIOS и DNS. + """ + def __init__(self): + """Инициализирует коллектор информации об устройствах. + + Создаёт объект MacLookup для определения вендоров и инициализирует кэши. + """ self.mac_lookup = MacLookup() self.nbtscan_cache = {} self.nbtscan_results = {} @lru_cache(maxsize=1000) def get_vendor(self, mac: str) -> str: + """Определяет вендора по MAC-адресу с использованием кэширования. + + Args: + mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX. + + Returns: + str: Название вендора или "Unknown", если вендор не определён. + + Raises: + Exception: Если произошла ошибка при запросе к базе вендоров. + """ try: return self.mac_lookup.lookup(mac) except Exception as e: @@ -95,6 +165,13 @@ class DeviceInfoCollector: return "Unknown" def scan_nbtscan(self, subnet: str) -> None: + """Выполняет сканирование NetBIOS для указанной подсети. + + Args: + subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24"). + + Сохраняет результаты в кэш, игнорируя хостнеймы "". + """ if subnet not in self.nbtscan_results: try: logger.debug(f"Запуск nbtscan для подсети {subnet}") @@ -121,6 +198,16 @@ class DeviceInfoCollector: logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}") def get_hostname(self, ip: str) -> Optional[str]: + """Определяет хостнейм для указанного IP-адреса. + + Args: + ip (str): IP-адрес устройства. + + Returns: + Optional[str]: Хостнейм, если найден, иначе None. + + Сначала пытается использовать DNS, затем NetBIOS через nbtscan. + """ try: socket.setdefaulttimeout(2) return socket.gethostbyaddr(ip)[0] @@ -138,12 +225,33 @@ class DeviceInfoCollector: return None class DeviceStorage: + """Класс для хранения и управления данными об устройствах. + + Хранит устройства в JSON-файле и удаляет устаревшие записи. + """ + def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.json"): + """Инициализирует хранилище устройств с указанным путём. + + Args: + storage_path (str, optional): Путь к файлу хранения. + По умолчанию "etc/gonets/SRV/storage/devices/devices.json". + + Создаёт родительские директории, если они не существуют. + """ self.storage_file = Path(storage_path) self.storage_file.parent.mkdir(parents=True, exist_ok=True) self.offline_timeout = timedelta(days=7) def load_devices(self) -> Dict[str, Device]: + """Загружает устройства из файла хранения. + + Returns: + Dict[str, Device]: Словарь устройств, где ключ — MAC-адрес. + + Удаляет устройства с неверным форматом last_seen и возвращает пустой словарь + при ошибке. + """ try: if self.storage_file.exists(): with open(self.storage_file, "r", encoding="utf-8") as f: @@ -160,6 +268,15 @@ class DeviceStorage: return {} def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]: + """Очищает устаревшие устройства из словаря. + + Args: + devices (Dict[str, Device]): Словарь устройств с MAC-адресами как ключами. + + Returns: + Dict[str, Device]: Отфильтрованный словарь, исключающий устройства + offline более 7 дней. + """ now = datetime.now() filtered = {} for mac, device in devices.items(): @@ -177,6 +294,14 @@ class DeviceStorage: return filtered def save_devices(self, devices: List[Device]) -> None: + """Сохраняет список устройств в файл. + + Args: + devices (List[Device]): Список объектов Device для сохранения. + + Raises: + Exception: Если произошла ошибка при записи в файл. + """ try: data = SubnetScanResult(devices=devices).model_dump() with open(self.storage_file, "w", encoding="utf-8") as f: @@ -186,7 +311,16 @@ class DeviceStorage: logger.error(f"Ошибка при сохранении устройств: {str(e)}") class SubnetService: + """Сервис для сканирования подсетей и управления устройствами. + + Объединяет функционал сканирования сети, портов и хранения данных об устройствах. + """ + def __init__(self): + """Инициализирует сервис подсети с настройкой всех компонентов. + + Автоматически определяет интерфейс и инициализирует сканеры и хранилище. + """ self.interface = self._get_default_interface() self.network_scanner = NetworkScanner(self.interface) self.port_scanner = PortScanner(self.interface) @@ -194,6 +328,13 @@ class SubnetService: self.storage = DeviceStorage() def _get_default_interface(self) -> str: + """Определяет имя сетевого интерфейса по умолчанию. + + Returns: + str: Название интерфейса (например, "eth0"). + + Использует информацию о шлюзе или первый доступный интерфейс, отличный от "lo". + """ try: gateways = netifaces.gateways() default_gateway = gateways.get("default", {}).get(netifaces.AF_INET) @@ -209,6 +350,19 @@ class SubnetService: return "eth0" def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult: + """Сканирует указанную подсеть и возвращает результаты. + + Args: + subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24"). + ports (List[int], optional): Список портов для сканирования. + По умолчанию None (используются порты из config.ports). + + Returns: + SubnetScanResult: Объект с результатами сканирования, содержащий список + устройств. + + Сохраняет данные в хранилище и обновляет статус устройств. + """ start_time = time.time() devices = [] now = datetime.now().astimezone() @@ -228,6 +382,14 @@ class SubnetService: self.device_info.scan_nbtscan(subnet) def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]: + """Обрабатывает устройство из пары IP и MAC. + + Args: + ip_mac (Tuple[str, str]): Кортеж с IP-адресом и MAC-адресом. + + Returns: + Optional[Device]: Объект Device или None, если устройство локальное. + """ ip, mac = ip_mac if local_device and ip == local_device.ip: return None @@ -286,6 +448,14 @@ class SubnetService: return SubnetScanResult(devices=devices) def _get_local_device(self, subnet: str) -> Optional[Device]: + """Определяет локальное устройство в указанной подсети. + + Args: + subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24"). + + Returns: + Optional[Device]: Объект локального устройства или None, если не найдено. + """ now = datetime.now().astimezone() formatted_now = now.strftime("%d-%m-%Y, %H:%M") try: diff --git a/SRV/services/wifi_service.py b/SRV/services/wifi_service.py index 30cc458..9745b82 100644 --- a/SRV/services/wifi_service.py +++ b/SRV/services/wifi_service.py @@ -6,11 +6,9 @@ from functools import lru_cache from typing import List, Optional, Dict, Tuple from mac_vendor_lookup import MacLookup import pywifi -from pywifi import Profile from models.wifi import WifiAccessPoint, WifiScanResult from pathlib import Path import json -from datetime import datetime logging.basicConfig( level=logging.DEBUG, @@ -18,14 +16,34 @@ logging.basicConfig( logger = logging.getLogger(__name__) class WifiStorage: - """Класс для хранения данных о Wi-Fi сетях и их заметках по SSID.""" + """Класс для хранения данных о Wi-Fi сетях и их заметках по SSID. + + Предоставляет методы для загрузки и сохранения заметок, связанных с SSID сетей, + в JSON-файле на диске. + """ def __init__(self, storage_path: str = "etc/gonets/SRV/storage/wifi/wifi.json"): + """Инициализирует объект хранилища Wi-Fi с указанным путём к файлу. + + Args: + storage_path (str, optional): Путь к файлу для хранения заметок. + По умолчанию "etc/gonets/SRV/storage/wifi/wifi.json". + + Создаёт родительские директории, если они не существуют. + """ self.storage_file = Path(storage_path) self.storage_file.parent.mkdir(parents=True, exist_ok=True) def load_notes(self) -> Dict[str, Optional[str]]: - """Загружает заметки по SSID из файла хранения.""" + """Загружает заметки по SSID из файла хранения. + + Returns: + Dict[str, Optional[str]]: Словарь, где ключ — SSID, значение — заметка + или None, если заметки нет. + + Raises: + Exception: Если произошла ошибка при чтении или парсинге файла. + """ try: if self.storage_file.exists(): with open(self.storage_file, "r", encoding="utf-8") as f: @@ -37,7 +55,15 @@ class WifiStorage: return {} def save_notes(self, notes: Dict[str, Optional[str]]) -> None: - """Сохраняет заметки по SSID в файл.""" + """Сохраняет заметки по SSID в файл. + + Args: + notes (Dict[str, Optional[str]]): Словарь с SSID как ключами и заметками + как значениями. + + Raises: + Exception: Если произошла ошибка при записи в файл. + """ try: data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]} with open(self.storage_file, "w", encoding="utf-8") as f: @@ -47,16 +73,33 @@ class WifiStorage: logger.error(f"Ошибка при сохранении заметок: {str(e)}") class WifiService: - """Сервис для сканирования текущих Wi-Fi точек доступа""" + """Сервис для сканирования текущих Wi-Fi точек доступа. + + Предоставляет функционал для обнаружения Wi-Fi сетей с использованием pywifi и iw, + определения характеристик точек доступа (сигнал, шифрование, канал и т.д.), + а также управления заметками. + """ def __init__(self): + """Инициализирует сервис Wi-Fi с настройкой интерфейса и хранилища. + + Инициализирует объект MacLookup для определения вендоров по MAC-адресам, + PyWiFi для работы с Wi-Fi-интерфейсами и объект хранилища заметок. + """ self.mac_lookup = MacLookup() self.wifi = pywifi.PyWiFi() self.interface = self._get_wifi_interface() self.storage = WifiStorage() def _get_wifi_interface(self) -> Optional[object]: - """Автоматически определяет активный Wi-Fi-интерфейс""" + """Автоматически определяет активный Wi-Fi-интерфейс. + + Returns: + Optional[object]: Объект интерфейса PyWiFi или None, если интерфейс не найден. + + Проверяет доступные интерфейсы, выполняет тестовое сканирование и возвращает + первый активный интерфейс, исключая P2P-интерфейсы. + """ try: interfaces = self.wifi.interfaces() if not interfaces: @@ -93,6 +136,17 @@ class WifiService: @lru_cache(maxsize=1000) def get_vendor(self, mac: str) -> str: + """Определяет вендора по MAC-адресу с использованием кэширования. + + Args: + mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX. + + Returns: + str: Название вендора или "Unknown", если вендор не определён. + + Raises: + Exception: Если произошла ошибка при запросе к базе вендоров. + """ try: return self.mac_lookup.lookup(mac) except Exception as e: @@ -101,7 +155,14 @@ class WifiService: return "Unknown" def _get_encryption_from_iw(self, ap_info: Dict) -> str: - """Определяет шифрование из данных iw""" + """Определяет тип шифрования из данных, полученных через iw. + + Args: + ap_info (Dict): Словарь с данными точки доступа, включая поле "security". + + Returns: + str: Тип шифрования ("WPA3", "WPA2", "WPA", "WEP" или "Open"). + """ security = ap_info.get("security", "") logger.debug( f"Шифрование для BSSID {ap_info.get('bssid')}: {security}") @@ -116,7 +177,14 @@ class WifiService: return "Open" def _get_channel(self, frequency: Optional[int]) -> Optional[int]: - """Преобразует частоту в канал.""" + """Преобразует частоту в номер канала. + + Args: + frequency (Optional[int]): Частота в МГц или None. + + Returns: + Optional[int]: Номер канала или None, если частота вне диапазона. + """ if frequency is None: return None try: @@ -133,7 +201,14 @@ class WifiService: return None def _get_frequency(self, frequency: Optional[int]) -> Optional[str]: - """Определяет частотный диапазон.""" + """Определяет частотный диапазон на основе частоты. + + Args: + frequency (Optional[int]): Частота в МГц или None. + + Returns: + Optional[str]: Диапазон ("2.4 GHz", "5 GHz" или None). + """ if frequency is None: return None try: @@ -150,7 +225,17 @@ class WifiService: return None def _get_iw_scan_data(self) -> List[Dict]: - """Получает данные сканирования через iw.""" + """Получает данные сканирования Wi-Fi через команду iw. + + Returns: + List[Dict]: Список словарей с данными точек доступа (BSSID, SSID, частота, + сигнал, шифрование). + + Raises: + subprocess.CalledProcessError: Если команда iw завершилась с ошибкой. + subprocess.TimeoutExpired: Если превышен таймаут выполнения. + Exception: Если произошла ошибка при парсинге вывода. + """ try: if not self.interface: logger.error("Интерфейс не определён") @@ -225,7 +310,18 @@ class WifiService: return [] def scan_wifi(self, timeout: int = 30) -> WifiScanResult: - """Сканирует текущие Wi-Fi точки доступа""" + """Сканирует текущие Wi-Fi точки доступа. + + Args: + timeout (int, optional): Таймаут ожидания в секундах. По умолчанию 30. + + Returns: + WifiScanResult: Объект с результатами сканирования, содержащий список точек + доступа. + + Сканирует Wi-Fi с использованием pywifi и iw, объединяет данные, определяет + лучшие точки по сигналу и сохраняет заметки. + """ start_time = time.time() aps = [] @@ -282,12 +378,10 @@ class WifiService: if ssid not in best_aps_by_ssid or ( current_ap.signal and best_aps_by_ssid[ssid].signal and current_ap.signal > best_aps_by_ssid[ssid].signal): - # При обновлении или добавлении точки сохраняем заметку из saved_notes current_ap.note = saved_notes.get(ssid) best_aps_by_ssid[ssid] = current_ap aps = list(best_aps_by_ssid.values()) - # Сохраняем заметки для всех найденных SSID notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid} self.storage.save_notes(notes_to_save) @@ -313,7 +407,7 @@ class WifiService: - Ошибка: (False, сообщение об ошибке). Raises: - Exception: Если произошла ошибка при сохранении. + Exception: Если произошла ошибка при сохранении заметки. """ try: saved_notes = self.storage.load_notes() @@ -343,7 +437,7 @@ class WifiService: - Ошибка: (False, сообщение об ошибке). Raises: - Exception: Если произошла ошибка при сохранении. + Exception: Если произошла ошибка при сохранении заметки. """ try: saved_notes = self.storage.load_notes() @@ -373,7 +467,7 @@ class WifiService: - Ошибка: (False, сообщение об ошибке). Raises: - Exception: Если произошла ошибка при сохранении. + Exception: Если произошла ошибка при сохранении изменений. """ try: saved_notes = self.storage.load_notes() diff --git a/SRV/storage/json_storage.py b/SRV/storage/json_storage.py index 8a13d89..1823706 100644 --- a/SRV/storage/json_storage.py +++ b/SRV/storage/json_storage.py @@ -1,4 +1,3 @@ -# storage/json_storage.py import json import os import logging @@ -9,22 +8,40 @@ from models.auth import UserCreate, UserResponse logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) - class JsonStorage: - """Хранилище пользователей в JSON-файле""" + """Хранилище пользователей в JSON-файле. + + Предоставляет методы для создания, чтения и проверки данных пользователей, + используя шифрование паролей с помощью bcrypt. + """ def __init__(self, file_path: str = "/etc/gonets/users.json"): + """Инициализирует хранилище пользователей. + + Args: + file_path (str, optional): Путь к JSON-файлу для хранения данных. + По умолчанию "/etc/gonets/users.json". + + Создаёт объект CryptContext для хеширования паролей и проверяет/создаёт + файл, если он отсутствует. + """ self.file_path = file_path self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") self._ensure_file_exists() def _ensure_file_exists(self): - """Создаёт JSON-файл, если он не существует""" + """Создаёт JSON-файл, если он не существует. + + Создаёт родительские директории и файл с начальной структурой данных, + устанавливая права доступа 600. + + Raises: + Exception: Если произошла ошибка при создании файла или директории. + """ try: logger.info(f"Проверка существования файла {self.file_path}") if not os.path.exists(self.file_path): - logger.info( - f"Создание директории {os.path.dirname(self.file_path)}") + logger.info(f"Создание директории {os.path.dirname(self.file_path)}") os.makedirs(os.path.dirname(self.file_path), exist_ok=True) logger.info(f"Создание файла {self.file_path}") with open(self.file_path, "w") as f: @@ -40,7 +57,15 @@ class JsonStorage: raise def _read_data(self) -> dict: - """Читает данные из JSON-файла""" + """Читает данные из JSON-файла. + + Returns: + dict: Словарь с данными пользователей и следующим ID. + + Raises: + FileNotFoundError: Если файл отсутствует (возвращает пустую структуру). + Exception: Если произошла ошибка при чтении файла. + """ try: with open(self.file_path, "r") as f: return json.load(f) @@ -56,7 +81,14 @@ class JsonStorage: raise def _write_data(self, data: dict): - """Записывает данные в JSON файл""" + """Записывает данные в JSON-файл. + + Args: + data (dict): Словарь с данными для записи (пользователи и next_id). + + Raises: + Exception: Если произошла ошибка при записи в файл. + """ try: with open(self.file_path, "w") as f: json.dump(data, f, indent=2) @@ -67,7 +99,18 @@ class JsonStorage: raise def create_user(self, user: UserCreate) -> UserResponse: - """Создаёт нового пользователя""" + """Создаёт нового пользователя с хешированием пароля. + + Args: + user (UserCreate): Объект с данными нового пользователя (username, password, is_admin). + + Returns: + UserResponse: Объект с данными созданного пользователя (id, username, is_admin). + + Raises: + ValueError: Если пользователь с таким именем уже существует. + Exception: Если произошла ошибка при записи данных. + """ try: data = self._read_data() users = data["users"] @@ -99,7 +142,17 @@ class JsonStorage: raise def get_user_by_username(self, username: str) -> Optional[dict]: - """Получает пользователя по имени""" + """Получает данные пользователя по имени. + + Args: + username (str): Имя пользователя для поиска. + + Returns: + Optional[dict]: Словарь с данными пользователя или None, если пользователь не найден. + + Raises: + Exception: Если произошла ошибка при чтении файла. + """ try: if not os.path.exists(self.file_path): logger.info( @@ -117,7 +170,14 @@ class JsonStorage: ) raise - def verify_password(self, plain_password: str, - hashed_password: str) -> bool: - """Проверяет пароль""" - return self.pwd_context.verify(plain_password, hashed_password) + def verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Проверяет соответствие введённого пароля хэшированному. + + Args: + plain_password (str): Введённый пользователем пароль. + hashed_password (str): Хэшированный пароль из хранилища. + + Returns: + bool: True, если пароль верен, False в противном случае. + """ + return self.pwd_context.verify(plain_password, hashed_password) \ No newline at end of file diff --git a/SRV/storage/session_storage.py b/SRV/storage/session_storage.py index 83aee33..fac024f 100644 --- a/SRV/storage/session_storage.py +++ b/SRV/storage/session_storage.py @@ -1,4 +1,3 @@ -# storage/session_storage.py import json import os import uuid @@ -9,19 +8,38 @@ from datetime import datetime, timedelta logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) - class SessionStorage: + """Хранилище сессий пользователей в JSON-файле. + + Предоставляет методы для создания, получения, удаления и очистки сессий с + учётом срока действия. + """ def __init__(self, file_path: str = "/etc/gonets/sessions.json"): + """Инициализирует хранилище сессий. + + Args: + file_path (str, optional): Путь к JSON-файлу для хранения сессий. + По умолчанию "/etc/gonets/sessions.json". + + Создаёт файл и директории, если они отсутствуют, с правами доступа 600. + """ self.file_path = file_path self._ensure_file_exists() def _ensure_file_exists(self): + """Создаёт JSON-файл, если он не существует. + + Создаёт родительские директории и файл с начальной структурой данных, + устанавливая права доступа 600. + + Raises: + Exception: Если произошла ошибка при создании файла или директории. + """ try: logger.info(f"Проверка существования файла {self.file_path}") if not os.path.exists(self.file_path): - logger.info( - f"Создание директории {os.path.dirname(self.file_path)}") + logger.info(f"Создание директории {os.path.dirname(self.file_path)}") os.makedirs(os.path.dirname(self.file_path), exist_ok=True) logger.info(f"Создание файла {self.file_path}") with open(self.file_path, "w") as f: @@ -37,6 +55,14 @@ class SessionStorage: raise def _read_data(self) -> Dict: + """Читает данные из JSON-файла. + + Returns: + Dict: Словарь с данными сессий. + + Raises: + Exception: Если произошла ошибка при чтении файла. + """ try: with open(self.file_path, "r") as f: return json.load(f) @@ -47,6 +73,14 @@ class SessionStorage: raise def _write_data(self, data: Dict): + """Записывает данные в JSON-файл. + + Args: + data (Dict): Словарь с данными сессий для записи. + + Raises: + Exception: Если произошла ошибка при записи в файл. + """ try: with open(self.file_path, "w") as f: json.dump(data, f, indent=2) @@ -57,7 +91,23 @@ class SessionStorage: raise def create_session(self, username: str, token: str, client_id: str) -> str: - """Создаёт новую сессию и возвращает session_id""" + """Создаёт новую сессию и возвращает уникальный session_id. + + Args: + username (str): Имя пользователя, для которого создаётся сессия. + token (str): Токен аутентификации. + client_id (str): Идентификатор клиента. + + Returns: + str: Уникальный идентификатор сессии (session_id). + + Raises: + Exception: Если произошла ошибка при создании или записи сессии. + + Note: + Удаляет истекшие сессии перед созданием новой и устанавливает срок + действия 30 минут. + """ try: self.clean_expired_sessions() data = self._read_data() @@ -83,7 +133,22 @@ class SessionStorage: raise def get_latest_session(self, client_id: str) -> Optional[Dict]: - """Получает последнюю активную сессию для client_id""" + """Получает последнюю активную сессию для указанного client_id. + + Args: + client_id (str): Идентификатор клиента. + + Returns: + Optional[Dict]: Словарь с данными последней активной сессии, включая + "session_id", или None, если сессия не найдена. + + Raises: + Exception: Если произошла ошибка при чтении или обработке данных. + + Note: + Очищает истекшие сессии перед поиском и возвращает сессию с самым + поздним временем создания. + """ try: self.clean_expired_sessions() data = self._read_data() @@ -114,7 +179,17 @@ class SessionStorage: raise def delete_session(self, session_id: str): - """Удаляет сессию по session_id""" + """Удаляет сессию по указанному session_id. + + Args: + session_id (str): Уникальный идентификатор сессии. + + Raises: + Exception: Если произошла ошибка при чтении или записи данных. + + Note: + Очищает истекшие сессии перед удалением. + """ try: self.clean_expired_sessions() data = self._read_data() @@ -129,25 +204,30 @@ class SessionStorage: raise def clean_expired_sessions(self): - """Удаляет все истекшие сессии из хранилища""" + """Удаляет все истекшие сессии из хранилища. + + Raises: + Exception: Если произошла ошибка при чтении или записи данных. + + Note: + Проверяет срок действия каждой сессии и удаляет те, что истекли, + записывая обновлённые данные. + """ try: data = self._read_data() expired_sessions = [] current_time = datetime.utcnow() - # Находим истекшие сессии for session_id, session in list(data["sessions"].items()): expiry = datetime.fromisoformat(session["expiry"]) if current_time > expiry: expired_sessions.append(session_id) - # Удаляем истекшие сессии for session_id in expired_sessions: del data["sessions"][session_id] logger.info( f"Удалена истекшая сессия с session_id {session_id}") - # Записываем обновлённые данные if expired_sessions: self._write_data(data) logger.info(f"Удалено {len(expired_sessions)} истекших сессий") @@ -158,4 +238,4 @@ class SessionStorage: logger.error( f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}" ) - raise + raise \ No newline at end of file