Рефакторинг. docstring

This commit is contained in:
parent 060f4de175
commit 37f9e3abfb
14 changed files with 1234 additions and 243 deletions

View file

@ -1,4 +1,3 @@
# api/v1/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from services.auth_service import AuthService from services.auth_service import AuthService
@ -17,7 +16,21 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse) @router.post("/register", response_model=UserResponse)
async def register(user: UserCreate): 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: try:
db_user = auth_service.register(user) db_user = auth_service.register(user)
logger.info(f"Пользователь {user.username} успешно зарегистрирован") logger.info(f"Пользователь {user.username} успешно зарегистрирован")
@ -26,13 +39,27 @@ async def register(user: UserCreate):
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
detail="Не удалось зарегистрировать пользователя")
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request): 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: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Попытка авторизации для client_id {client_id}") logger.debug(f"Попытка авторизации для client_id {client_id}")
@ -45,13 +72,27 @@ async def login(user: UserLogin, request: Request):
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
detail="Не удалось авторизовать пользователя")
@router.get("/me") @router.get("/me")
async def get_current_user(request: Request): 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: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос /me для client_id {client_id}") logger.debug(f"Запрос /me для client_id {client_id}")
@ -66,13 +107,25 @@ async def get_current_user(request: Request):
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}") f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
detail="Не удалось получить данные пользователя")
@router.get("/logout") @router.get("/logout")
async def logout(request: Request): async def logout(request: Request):
"""Выход из системы, удаление сессии""" """Выход из системы, удаление сессии.
Удаляет активную сессию пользователя по client_id.
Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
Returns:
dict: Пустой словарь или сообщение об успешном выходе.
Raises:
HTTPException: Если сессия не найдена (status_code=401) или произошла
внутренняя ошибка (status_code=500).
"""
try: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос logout для client_id {client_id}") logger.debug(f"Запрос logout для client_id {client_id}")
@ -81,13 +134,24 @@ async def logout(request: Request):
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
detail="Не удалось выйти из системы")
@router.get("/check_json") @router.get("/check_json")
async def check_users_file(): async def check_users_file():
"""Проверяет, существует ли файл users.json и содержит ли он пользователей""" """Проверяет, существует ли файл users.json и содержит ли он пользователей.
Возвращает информацию о состоянии файла и наличии пользователей.
Returns:
dict: Словарь с ключами:
- "exists" (bool): Существует ли файл.
- "has_users" (bool): Содержит ли файл пользователей.
- "message" (str): Сообщение с описанием результата.
Raises:
HTTPException: Если произошла ошибка при проверке файла (status_code=500).
"""
try: try:
file_path = "/etc/gonets/users.json" file_path = "/etc/gonets/users.json"
storage = JsonStorage(file_path=file_path) storage = JsonStorage(file_path=file_path)
@ -104,27 +168,24 @@ async def check_users_file():
has_users = len(data.get("users", [])) > 0 has_users = len(data.get("users", [])) > 0
if has_users: if has_users:
logger.info( logger.info(
f"Файл {file_path} существует и содержит пользователей") f"Файл {file_path} существует и содержит пользователей"
)
return { return {
"exists": True, "exists": True,
"has_users": True, "has_users": True,
"message": "message": "Файл users.json существует и содержит пользователей",
"Файл users.json существует и содержит пользователей",
} }
else: else:
logger.info( logger.info(
f"Файл {file_path} существует, но не содержит пользователей") f"Файл {file_path} существует, но не содержит пользователей"
)
return { return {
"exists": "exists": True,
True, "has_users": False,
"has_users": "message": "Файл users.json существует, но не содержит пользователей",
False,
"message":
"Файл users.json существует, но не содержит пользователей",
} }
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}" f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}"
) )
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Ошибка при проверке файла")
detail="Ошибка при проверке файла")

View file

@ -8,7 +8,23 @@ ip_service = IPService()
@router.post("/check_ip") @router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...)): 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: try:
result = await ip_service.check_ip_config(data) result = await ip_service.check_ip_config(data)
return result return result
@ -20,37 +36,63 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
@router.post("/apply_ip") @router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...)): 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: try:
await ip_service.apply_ip_config(data) await ip_service.apply_ip_config(data)
return { return {
"success": "success": True,
True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены",
"message":
f"Настройки сетевого профиля для {data['connection_id']} применены",
} }
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
status_code=500,
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/save_ip") @router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...)): 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: try:
await ip_service.apply_ip_config(data) await ip_service.apply_ip_config(data)
return { return {
"success": "success": True,
True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены",
"message":
f"Настройки сетевого профиля для {data['connection_id']} применены",
} }
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
status_code=500,
detail=f"Ошибка проверки IP конфигурации: {str(e)}")

View file

@ -19,11 +19,18 @@ network_service = NetworkService()
class ContainerPositionManager: class ContainerPositionManager:
""" """Класс для управления позициями и состоянием контейнеров.
Класс для управления позициями и состоянием контейнеров
Хранит и обновляет координаты (x, y) и состояние (isOpen) для контейнеров
активации и деактивации.
""" """
def __init__(self): def __init__(self):
"""Инициализирует менеджер позиций контейнеров.
Создаёт начальные позиции и состояния для контейнеров "activation" и
"deactivation" с координатами (0, 0) и закрытым состоянием (False).
"""
self.positions: Dict[str, Dict[str, Union[int, bool]]] = { self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
"activation": { "activation": {
"x": 0, "x": 0,
@ -37,15 +44,35 @@ class ContainerPositionManager:
}, },
} }
def get_position_and_state(self, def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
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) container = self.positions.get(container_name)
if not container: if not container:
raise ValueError(f"Неверное имя контейнера: {container_name}") raise ValueError(f"Неверное имя контейнера: {container_name}")
return container["x"], container["y"], container["isOpen"] return container["x"], container["y"], container["isOpen"]
def update_position(self, container_id: int, x: int, y: int, def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
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" container_name = "activation" if container_id == 1 else "deactivation"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen} 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]: 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()") logger.debug("Вызов get_network_data()")
try: try:
network_data = await network_service.get_network_interfaces_container() network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces( activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
network_data.interfaces)
activation_x, activation_y, activation_is_open = ( activation_x, activation_y, activation_is_open = (container_manager.get_position_and_state("activation"))
container_manager.get_position_and_state("activation")) deactivation_x, deactivation_y, deactivation_is_open = (container_manager.get_position_and_state("deactivation"))
deactivation_x, deactivation_y, deactivation_is_open = (
container_manager.get_position_and_state("deactivation"))
activation = Activation( activation = Activation(
id=1, id=1,
@ -90,16 +124,20 @@ async def get_network_data() -> Dict[str, Dict]:
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка в get_network_data: {e}") logger.error(f"Ошибка в get_network_data: {e}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
detail="Не удалось получить сетевые данные")
def _split_interfaces( def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]], interfaces: List[Union[NetworkInterface, WiFiDevice]],
) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[ ) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
NetworkInterface, WiFiDevice]]]: """Разделяет интерфейсы на активные и неактивные.
"""
Разделяет интерфейсы на активные и неактивные Args:
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список сетевых интерфейсов.
Returns:
Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
Кортеж из списков активных и неактивных интерфейсов.
""" """
activation_interfaces = [] activation_interfaces = []
deactivation_interfaces = [] deactivation_interfaces = []
@ -112,13 +150,29 @@ def _split_interfaces(
@router.post("/update_container_position") @router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
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: try:
required_keys = [ required_keys = [
"container_id", "container_x", "container_y", "isOpen" "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"] container_y = data["container_y"]
is_open = data["isOpen"] is_open = data["isOpen"]
# Проверка типов
if (not isinstance(container_id, int) if (not isinstance(container_id, int)
or not isinstance(container_x, int) or not isinstance(container_x, int)
or not isinstance(container_y, int) or not isinstance(container_y, int)
or not isinstance(is_open, bool)): or not isinstance(is_open, bool)):
raise ValueError("Неверный тип данных в запросе") raise ValueError("Неверный тип данных в запросе")
container_manager.update_position(container_id, container_x, container_manager.update_position(container_id, container_x, container_y, is_open)
container_y, is_open)
return { return {
"success": True, "success": True,
"message": "Позиция и состояние контейнера обновлены" "message": "Позиция и состояние контейнера обновлены"
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка обновления позиции контейнера: {e}") logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException( raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
status_code=400,
detail=f"Не удалось обновить положение контейнера: {str(e)}",
)
@router.get("/get_hostname") @router.get("/get_hostname")
async def get_hostname(): async def get_hostname():
"""Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста.
Returns:
dict: Словарь с ключом "hostname" и значением имени хоста.
Raises:
HTTPException: Если произошла ошибка при получении имени хоста (status_code=500).
"""
try: try:
hostname_info = await network_service.get_hostname() hostname_info = await network_service.get_hostname()
return hostname_info return hostname_info
except Exception as e: except Exception as e:
logger.error(f"Ошибка в get_hostname: {e}") logger.error(f"Ошибка в get_hostname: {e}")
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")
detail="Не удалось получить имя хоста")

View file

@ -11,7 +11,22 @@ router = APIRouter()
@router.get("/devices", response_model=SubnetScanResult) @router.get("/devices", response_model=SubnetScanResult)
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None): 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: try:
subnet_service = SubnetService() subnet_service = SubnetService()
result = subnet_service.scan_subnet(subnet, ports) 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) @router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30): 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: try:
wifi_service = WifiService() wifi_service = WifiService()
result = wifi_service.scan_wifi(timeout=timeout) result = wifi_service.scan_wifi(timeout=timeout)
@ -31,7 +58,21 @@ def scan_wifi(timeout: int = 30):
@router.post("/devices/{mac}/note") @router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest): 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: try:
subnet_service = SubnetService() subnet_service = SubnetService()
success, error_message = subnet_service.add_note(mac, request.note) 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") @router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest): 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: try:
subnet_service = SubnetService() subnet_service = SubnetService()
success, error_message = subnet_service.update_note(mac, request.note) 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") @router.delete("/devices/{mac}/note")
def delete_note(mac: str): 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: try:
subnet_service = SubnetService() subnet_service = SubnetService()
success, error_message = subnet_service.delete_note(mac) success, error_message = subnet_service.delete_note(mac)
@ -82,7 +150,21 @@ def delete_note(mac: str):
@router.post("/wifi/{ssid}/note") @router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest): 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: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.add_note(ssid, request.note) 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") @router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest): 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: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.update_note(ssid, request.note) 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") @router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str): 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: try:
wifi_service = WifiService() wifi_service = WifiService()
success, error_message = wifi_service.delete_note(ssid) success, error_message = wifi_service.delete_note(ssid)
@ -133,7 +242,20 @@ def delete_wifi_note(ssid: str):
@router.post("/wifi/connect") @router.post("/wifi/connect")
def connect_to_wifi(request: WifiConnectRequest): 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() wifi_service = WifiConnectionService()
success, message = wifi_service.connect(request.ssid, request.password) success, message = wifi_service.connect(request.ssid, request.password)
if not success: if not success:
@ -142,7 +264,19 @@ def connect_to_wifi(request: WifiConnectRequest):
@router.post("/wifi/disconnect") @router.post("/wifi/disconnect")
def disconnect_from_wifi(request: WifiDisconnectRequest): 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() wifi_service = WifiConnectionService()
success, message = wifi_service.disconnect(request.ssid) success, message = wifi_service.disconnect(request.ssid)
if not success: if not success:
@ -151,7 +285,18 @@ def disconnect_from_wifi(request: WifiDisconnectRequest):
@router.get("/wifi/status") @router.get("/wifi/status")
def 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() wifi_service = WifiConnectionService()
success, message = wifi_service.get_status() success, message = wifi_service.get_status()
if not success: if not success:

View file

@ -12,27 +12,57 @@ router = APIRouter()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WebSocketManager: class WebSocketManager:
""" """Класс для управления подключениями к WebSocket-серверу.
Класс для управления подключениями к WebSocket-серверу
Обрабатывает подключение, отключение и трансляцию сообщений для клиентов,
подключённых через WebSocket.
""" """
def __init__(self): def __init__(self):
"""Инициализация менеджера подключений к WebSocket-серверу""" """Инициализирует менеджер подключений к WebSocket-серверу.
Создаёт пустое множество для хранения активных соединений.
"""
self.active_connections: Set[WebSocket] = set() self.active_connections: Set[WebSocket] = set()
async def connect(self, websocket: WebSocket): async def connect(self, websocket: WebSocket):
"""Подключение клиента к WebSocket-серверу""" """Подключает клиента к WebSocket-серверу.
Принимает подключение и добавляет WebSocket в список активных соединений.
Args:
websocket (WebSocket): Объект WebSocket для подключения.
Raises:
Exception: Если произошла ошибка при принятии подключения.
"""
await websocket.accept() await websocket.accept()
self.active_connections.add(websocket) self.active_connections.add(websocket)
def disconnect(self, websocket: WebSocket): def disconnect(self, websocket: WebSocket):
"""Отключение клиента от WebSocket-серверу""" """Отключает клиента от WebSocket-серверу.
Удаляет WebSocket из списка активных соединений.
Args:
websocket (WebSocket): Объект WebSocket для отключения.
"""
self.active_connections.remove(websocket) self.active_connections.remove(websocket)
async def broadcast(self, message: dict): async def broadcast(self, message: dict):
"""Трансляция сообщений всем подключенным клиентам""" """Транслирует сообщения всем подключённым клиентам.
Отправляет JSON-данные всем активным соединениям, обрабатывая
отключения клиентов.
Args:
message (dict): Сообщение для трансляции в формате JSON.
Raises:
WebSocketDisconnect: Если клиент неожиданно отключился.
Exception: Если произошла другая ошибка при отправке.
"""
for connection in list(self.active_connections): for connection in list(self.active_connections):
try: try:
if connection.client_state == WebSocketState.CONNECTED: if connection.client_state == WebSocketState.CONNECTED:
@ -49,15 +79,25 @@ manager = WebSocketManager()
@router.websocket("/api/v1") @router.websocket("/api/v1")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket):
"""Обработчик WebSocket-соединения""" """Обработчик WebSocket-соединения.
Управляет соединением клиента, принимает данные для обновления позиций
контейнеров и транслирует сетевые данные.
Args:
websocket (WebSocket): Объект WebSocket для обработки соединения.
Raises:
WebSocketDisconnect: Если клиент отключился.
Exception: Если произошла ошибка при обработке данных.
"""
await manager.connect(websocket) await manager.connect(websocket)
try: try:
while True: while True:
data = await websocket.receive_json() data = await websocket.receive_json()
logger.info(f"Полученные данные: {data}") logger.info(f"Полученные данные: {data}")
if all(key in data if all(key in data for key in ["container_id", "container_x", "container_y"]):
for key in ["container_id", "container_x", "container_y"]):
await update_container_position(data) await update_container_position(data)
network_data = await get_network_data() network_data = await get_network_data()
@ -77,12 +117,25 @@ async def websocket_endpoint(websocket: WebSocket):
@router.on_event("startup") @router.on_event("startup")
async def startup_event(): async def startup_event():
"""Запуск таска для трансляции сетевых данных""" """Запуск таска для трансляции сетевых данных при старте приложения.
Создаёт асинхронную задачу для периодической трансляции данных.
Raises:
Exception: Если произошла ошибка при запуске таска.
"""
asyncio.create_task(broadcast_data()) asyncio.create_task(broadcast_data())
async def broadcast_data(): async def broadcast_data():
"""Таск для трансляции сетевых данных""" """Таск для периодической трансляции сетевых данных.
Выполняет сканирование сети и отправляет данные всем подключённым клиентам
с интервалом в 2 секунды.
Raises:
Exception: Если произошла ошибка при получении данных или трансляции.
"""
while True: while True:
try: try:
network_data = await get_network_data() network_data = await get_network_data()
@ -96,4 +149,4 @@ async def broadcast_data():
await manager.broadcast(network_data) await manager.broadcast(network_data)
except Exception as e: except Exception as e:
logger.error(f"Ошибка в broadcast_data:{e}") logger.error(f"Ошибка в broadcast_data:{e}")
await asyncio.sleep(2) await asyncio.sleep(2)

View file

@ -5,10 +5,21 @@ from netaddr import IPNetwork
class NetworkScanner: class NetworkScanner:
"""Иницилазицая сетевого сканера""" """Инициализация сетевого сканера.
Предоставляет методы для сканирования сети с использованием ARP-запросов и
вывода информации о клиентах.
"""
def __init__(self, interface=None): def __init__(self, interface=None):
"""Инициализация параметров интерфейсов""" """Инициализация параметров сетевого сканера.
Args:
interface (str, optional): Название сетевого интерфейса. По умолчанию
используется интерфейс по умолчанию из netifaces.gateways().
Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес.
"""
self.interface = ( self.interface = (
interface or netifaces.gateways()["default"][netifaces.AF_INET][1]) interface or netifaces.gateways()["default"][netifaces.AF_INET][1])
self.ip_info = netifaces.ifaddresses(self.interface).get( self.ip_info = netifaces.ifaddresses(self.interface).get(
@ -20,13 +31,34 @@ class NetworkScanner:
self.interface)[netifaces.AF_LINK][0]["addr"] self.interface)[netifaces.AF_LINK][0]["addr"]
def _create_arp_packet(self, target_ip): 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) arp = ARP(pdst=target_ip)
ether = Ether(dst="ff:ff:ff:ff:ff:ff") ether = Ether(dst="ff:ff:ff:ff:ff:ff")
return ether / arp return ether / arp
def scan_network(self, attempts=3, timeout=2): 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 = [] all_clients = []
for network in self.networks: for network in self.networks:
@ -41,30 +73,35 @@ class NetworkScanner:
verbose=0)[0] verbose=0)[0]
for _, received in result: for _, received in result:
client = {"ip": received.psrc, "mac": received.hwsrc} client = {"ip": received.psrc, "mac": received.hwsrc}
if client not in clients: # Избегаем дублирования клиентов if client not in clients:
clients.append(client) clients.append(client)
all_clients.extend(clients) all_clients.extend(clients)
# Добавляет все локальные IP-адреса интерфейса как клиенты
for ip_info in self.ip_info: for ip_info in self.ip_info:
local_client = {"ip": ip_info["addr"], "mac": self.local_mac} local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
if local_client not in all_clients: if local_client not in all_clients:
all_clients.append(local_client) all_clients.append(local_client)
# Удаление дубликатов
unique_clients = {tuple(client.items()) for client in all_clients} unique_clients = {tuple(client.items()) for client in all_clients}
return [dict(client) for client in unique_clients] return [dict(client) for client in unique_clients]
def print_clients(self, clients): def print_clients(self, clients):
"""Лист адресов клиентов""" """Выводит список адресов клиентов в формате таблицы.
Args:
clients (List[dict]): Список словарей с ключами "ip" и "mac",
представляющих клиентов.
Выводит IP-адреса и MAC-адреса в отсортированном по IP порядке.
"""
print("Доступные устройства в сети:") print("Доступные устройства в сети:")
print("IP" + " " * 18 + "MAC") print("IP" + " " * 18 + "MAC")
for client in sorted(clients, key=lambda x: x["ip"]): for client in sorted(clients, key=lambda x: x["ip"]):
print(f"{client['ip']:<16} {client['mac']}") print(f"{client['ip']:<16} {client['mac']}")
# Оставил нативный запуск сервиса для тестов
if __name__ == "__main__": if __name__ == "__main__":
scanner = NetworkScanner() scanner = NetworkScanner()
clients = scanner.scan_network() clients = scanner.scan_network()
scanner.print_clients(clients) scanner.print_clients(clients)

View file

@ -1,4 +1,3 @@
# services/auth_service.py
from jose import JWTError, jwt from jose import JWTError, jwt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import HTTPException, Depends, Request from fastapi import HTTPException, Depends, Request

View file

@ -6,11 +6,28 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(level
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WifiConnectionService: class WifiConnectionService:
"""Сервис для управления подключением к Wi-Fi сетям с использованием nmcli.
Предоставляет методы для подключения, отключения и проверки статуса Wi-Fi соединений.
"""
def __init__(self): def __init__(self):
"""Инициализирует сервис Wi-Fi.
Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя.
"""
self.interface = self._get_wifi_interface() self.interface = self._get_wifi_interface()
def _get_wifi_interface(self) -> str: def _get_wifi_interface(self) -> str:
"""Автоматически определяет Wi-Fi-интерфейс.""" """Автоматически определяет Wi-Fi-интерфейс.
Returns:
str: Название Wi-Fi-интерфейса (например, "wlan0") или пустая строка,
если интерфейс не найден или Wi-Fi отключён.
Raises:
Exception: Если произошла ошибка при выполнении команд nmcli.
"""
try: try:
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True) result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
if "enabled" in result.stdout.lower(): if "enabled" in result.stdout.lower():
@ -29,7 +46,21 @@ class WifiConnectionService:
return "" return ""
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]: 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: try:
if not self.interface: if not self.interface:
return False, "Wi-Fi-интерфейс не доступен" return False, "Wi-Fi-интерфейс не доступен"
@ -63,7 +94,20 @@ class WifiConnectionService:
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}" return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]: 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: try:
if not self.interface: if not self.interface:
return False, "Wi-Fi-интерфейс не доступен" return False, "Wi-Fi-интерфейс не доступен"
@ -91,7 +135,16 @@ class WifiConnectionService:
return False, f"Ошибка отключения от {ssid}: {str(e)}" return False, f"Ошибка отключения от {ssid}: {str(e)}"
def get_status(self) -> Tuple[bool, Optional[str]]: 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: try:
if not self.interface: if not self.interface:
return False, "Wi-Fi-интерфейс не доступен" return False, "Wi-Fi-интерфейс не доступен"

View file

@ -1,4 +1,3 @@
import asyncio
from typing import Dict, Any from typing import Dict, Any
from sdbus_async.networkmanager import ( from sdbus_async.networkmanager import (
NetworkManager, NetworkManager,
@ -12,17 +11,28 @@ from services.arp_service import NetworkScanner
class IPService: class IPService:
""" """Сервис проверки доступности IP-адреса и применения настроек сети.
Сервис проверки доступности IP-адреса и применения настроек сети
Предоставляет асинхронные методы для проверки занятости IP-адреса с использованием
ARP-сканирования и применения настроек через NetworkManager.
""" """
def __init__(self): def __init__(self):
"""Инициализирует сервис IP.
Создаёт объект ARP-сканера и устанавливает системную шину D-Bus как
по умолчанию.
"""
self.arp_scanner = NetworkScanner() self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system() self.bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(self.bus) sdbus.set_default_bus(self.bus)
def _reset_bus(self): def _reset_bus(self):
"""Перезапускает соединение с системной шиной D-Bus""" """Перезапускает соединение с системной шиной D-Bus.
Закрывает текущее соединение и создаёт новое, устанавливая его как
по умолчанию.
"""
new_bus = sdbus.sd_bus_open_system() new_bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(new_bus) sdbus.set_default_bus(new_bus)
if hasattr(self, "bus") and self.bus is not None: if hasattr(self, "bus") and self.bus is not None:
@ -30,7 +40,23 @@ class IPService:
self.bus = new_bus self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]: 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() self._reset_bus()
clients = self.arp_scanner.scan_network() clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"] 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: 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() self._reset_bus()
nm = NetworkManager() nm = NetworkManager()
settings = NetworkManagerSettings() settings = NetworkManagerSettings()
@ -77,10 +119,8 @@ class IPService:
if await device.interface == interface_name: if await device.interface == interface_name:
await device.disconnect() await device.disconnect()
print(f"Отключён от {interface_name}") print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0], await nm.activate_connection(connection=connection_paths[0],device=device_path)
device=device_path)
print(f"Переподключён к {args['connection_id']}") print(f"Переподключён к {args['connection_id']}")
return return
raise RuntimeError( raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")
f"Устройство для интерфейса {interface_name} не найдено")

View file

@ -23,16 +23,35 @@ import subprocess
class NetworkService: class NetworkService:
"""Сервис для взаимодействия с NetworkManager через D-Bus""" """Сервис для взаимодействия с NetworkManager через D-Bus.
Предоставляет асинхронные методы для получения информации о сетевых интерфейсах,
их конфигурациях IP, Wi-Fi и состояниях.
"""
def __init__(self): def __init__(self):
"""Инициализирует сервис NetworkManager.
Устанавливает системный D-Bus и создаёт объекты для взаимодействия с
NetworkManager и его настройками.
"""
self.system_bus = sd_bus_open_system() self.system_bus = sd_bus_open_system()
self.nm = NetworkManager(self.system_bus) self.nm = NetworkManager(self.system_bus)
self.nm_settings = NetworkManagerSettings(self.system_bus) self.nm_settings = NetworkManagerSettings(self.system_bus)
async def get_ip_method(self, device: NetworkDeviceGeneric, async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str:
ip_type: str) -> str: """Получает метод IP (например, "auto", "manual") для указанного устройства.
"""Получает метод IP для устройства"""
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
ip_type (str): Тип IP (например, "ipv4" или "ipv6").
Returns:
str: Метод IP или "unknown" в случае ошибки.
Raises:
Exception: Если произошла ошибка при получении настроек соединения.
"""
try: try:
config = await device.get_applied_connection() config = await device.get_applied_connection()
settings = config[0].get(ip_type, {}) settings = config[0].get(ip_type, {})
@ -41,64 +60,80 @@ class NetworkService:
return "unknown" return "unknown"
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str: async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства""" """Извлекает имя соединения (GENERAL.CONNECTION) для устройства.
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
Returns:
str: Имя соединения или "unknown" в случае ошибки.
Raises:
Exception: Если произошла ошибка при получении настроек соединения.
"""
try: try:
applied_connection = await device.get_applied_connection() applied_connection = await device.get_applied_connection()
connection_settings = applied_connection[0] connection_settings = applied_connection[0]
connection_info = connection_settings.get("connection", {}) connection_info = connection_settings.get("connection", {})
return str(connection_info.get("id", ("s", "unknown"))[1]) return str(connection_info.get("id", ("s", "unknown"))[1])
except Exception as e: except Exception as e:
# print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}")
# срёт ошибки дл отключенных интерфейсов
# добавить потом условия для отключенных интерфейсов
return "unknown" return "unknown"
async def get_ip_config(self, device: NetworkDeviceGeneric, async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]:
ip_version: str) -> List[IndividualIPConfig]: """Получает конфигурацию IP для устройства.
"""Получает конфигурацию IP для устройства"""
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
ip_version (str): Версия IP ("4" или "6").
Returns:
List[IndividualIPConfig]: Список конфигураций IP, включая адреса и маршруты.
Raises:
Exception: Если произошла ошибка при получении данных IP.
"""
configs = [] configs = []
device_ip = await getattr(device, f"ip{ip_version}_config") device_ip = await getattr(device, f"ip{ip_version}_config")
if device_ip != "/": if device_ip != "/":
ip_config = globals()[f"IPv{ip_version}Config"](device_ip, ip_config = globals()[f"IPv{ip_version}Config"](device_ip, self.system_bus)
self.system_bus)
address_data = await ip_config.address_data address_data = await ip_config.address_data
for address in address_data: for address in address_data:
config = IndividualIPConfig( config = IndividualIPConfig(
address=str(address["address"][1]), address=str(address["address"][1]),
routes=[ routes=[
globals()[f"IPv{ip_version}Route"]( globals()[f"IPv{ip_version}Route"](
destination=route.get( destination=route.get("dest", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1],
"dest",
("s",
"0.0.0.0" if ip_version == "4" else "::"))[1],
prefix=route.get("prefix", ("u", 0))[1], prefix=route.get("prefix", ("u", 0))[1],
next_hop=route.get( next_hop=route.get("next-hop", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1],
"next-hop",
("s",
"0.0.0.0" if ip_version == "4" else "::"),
)[1],
metric=route.get("metric", ("u", 0))[1], metric=route.get("metric", ("u", 0))[1],
) for route in await ip_config.route_data ) for route in await ip_config.route_data
], ],
method=await self.get_ip_method(device, method=await self.get_ip_method(device, f"ipv{ip_version}"),
f"ipv{ip_version}"),
gateway=str(await ip_config.gateway), gateway=str(await ip_config.gateway),
dns=[ dns=[
str(ns["address"][1]) if ip_version == "4" else str(ns) str(ns["address"][1]) if ip_version == "4" else str(ns)
for ns in await getattr( for ns in await getattr(
ip_config, ip_config,
"nameserver_data" if ip_version == "nameserver_data" if ip_version == "4" else "nameservers",
"4" else "nameservers",
) )
], ],
) )
configs.append(config) configs.append(config)
return configs return configs
async def get_wifi_info( async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]:
self, wifi_device: NetworkDeviceWireless """Получает информацию о Wi-Fi устройстве.
) -> 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 active_ap = await wifi_device.active_access_point
wifi_info = { wifi_info = {
"ssid": None, "ssid": None,
@ -123,7 +158,18 @@ class NetworkService:
self, self,
initial_positions: Dict[str, Tuple[int, int]] = None initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[Union[NetworkInterface, WiFiDevice]]: ) -> 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() devices_paths = await self.nm.get_devices()
network_interfaces = [] network_interfaces = []
initial_positions = initial_positions or {} initial_positions = initial_positions or {}
@ -141,43 +187,37 @@ class NetworkService:
continue continue
common_data = { common_data = {
"name": "name": interface_name,
interface_name, "type": device_type,
"type": "device_state": device_state,
device_type, "active_connection": await self.get_connection_name(generic_device),
"device_state": "mac_address": str(await generic_device.hw_address),
device_state, "device_mtu": str(await generic_device.mtu),
"active_connection": "ipv4": IPConfig(configurations=await self.get_ip_config(generic_device, "4")),
await self.get_connection_name(generic_device), "ipv6": IPConfig(configurations=await self.get_ip_config(generic_device, "6")),
"mac_address": "x": initial_positions.get(interface_name, (100, 100))[0],
str(await generic_device.hw_address), "y": initial_positions.get(interface_name, (100, 100))[1],
"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": if device_type == "WIFI":
wifi_device = NetworkDeviceWireless(device_path, wifi_device = NetworkDeviceWireless(device_path, self.system_bus)
self.system_bus)
wifi_info = await self.get_wifi_info(wifi_device) wifi_info = await self.get_wifi_info(wifi_device)
network_interfaces.append( network_interfaces.append(WiFiDevice(**common_data, **wifi_info))
WiFiDevice(**common_data, **wifi_info))
else: else:
network_interfaces.append(NetworkInterface(**common_data)) network_interfaces.append(NetworkInterface(**common_data))
return network_interfaces return network_interfaces
async def get_hostname(self) -> Dict[str, str]: async def get_hostname(self) -> Dict[str, str]:
"""Возвращает имя хоста из команды hostname в формате JSON""" """Возвращает имя хоста из команды hostname в формате JSON.
Returns:
Dict[str, str]: Словарь с ключом "hostname" и значением имени хоста.
Raises:
RuntimeError: Если произошла ошибка при выполнении команды hostname
или обработке вывода.
"""
try: try:
result = subprocess.run(["hostname"], result = subprocess.run(["hostname"],
capture_output=True, capture_output=True,
@ -195,8 +235,15 @@ class NetworkService:
state: str, state: str,
initial_positions: Dict[str, Tuple[int, int]] = None initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]: ) -> 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) all_interfaces = await self.get_network_interfaces(initial_positions)
return [ return [
@ -211,10 +258,21 @@ class NetworkService:
container_x: int = 0, container_x: int = 0,
container_y: int = 0, container_y: int = 0,
) -> NetworkInterfacesContainer: ) -> 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: if state:
interfaces = await self.get_interfaces_by_state( interfaces = await self.get_interfaces_by_state(state, initial_positions)
state, initial_positions)
else: else:
interfaces = await self.get_network_interfaces(initial_positions) interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces, return NetworkInterfacesContainer(interfaces=interfaces,
@ -225,7 +283,15 @@ class NetworkService:
self, self,
initial_positions: Optional[Dict[str, Tuple[int, int]]] = None, initial_positions: Optional[Dict[str, Tuple[int, int]]] = None,
) -> NetworkInterfacesContainer: ) -> 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) interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces, return NetworkInterfacesContainer(interfaces=interfaces,
container_x=0, container_x=0,
@ -235,14 +301,30 @@ class NetworkService:
self, self,
initial_positions: Dict[str, Tuple[int, int]] = None initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]: ) -> 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) return await self.get_interfaces_by_state("100", initial_positions)
async def get_deactivated_interfaces( async def get_deactivated_interfaces(
self, self,
initial_positions: Dict[str, Tuple[int, int]] = None initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]: ) -> 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) all_interfaces = await self.get_network_interfaces(initial_positions)
return [ return [
interface for interface in all_interfaces interface for interface in all_interfaces
@ -255,9 +337,18 @@ class NetworkService:
container_x: int = 0, container_x: int = 0,
container_y: int = 0, container_y: int = 0,
) -> NetworkInterfacesContainer: ) -> 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( async def get_deactivated_interfaces_container(
self, self,
@ -265,8 +356,16 @@ class NetworkService:
container_x: int = 0, container_x: int = 0,
container_y: int = 0, container_y: int = 0,
) -> NetworkInterfacesContainer: ) -> 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) interfaces = await self.get_deactivated_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces, return NetworkInterfacesContainer(interfaces=interfaces, container_x=container_x, container_y=container_y)
container_x=container_x,
container_y=container_y)

View file

@ -11,7 +11,6 @@ from functools import lru_cache
import json import json
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import subprocess import subprocess
from models.subnet import SubnetScanResult, Device from models.subnet import SubnetScanResult, Device
@ -26,11 +25,33 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NetworkScanner: class NetworkScanner:
"""Класс для сканирования подсети с использованием ARP-запросов.
Выполняет обнаружение устройств в указанной подсети через ARP-пакеты.
"""
def __init__(self, interface: str): def __init__(self, interface: str):
"""Инициализирует сканер сети с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface self.interface = interface
conf.iface = interface conf.iface = interface
def scan(self, subnet: str) -> List[Tuple[str, str]]: 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: try:
arp = ARP(pdst=subnet) arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff") ether = Ether(dst="ff:ff:ff:ff:ff:ff")
@ -46,13 +67,42 @@ class NetworkScanner:
return [] return []
class PortScanner: class PortScanner:
"""Класс для сканирования открытых портов на устройствах.
Выполняет сканирование портов с использованием TCP SYN-запросов.
"""
def __init__(self, interface: str): def __init__(self, interface: str):
"""Инициализирует сканер портов с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface self.interface = interface
def scan(self, ip: str) -> List[str]: def scan(self, ip: str) -> List[str]:
"""Сканирует открытые порты на указанном IP-адресе.
Args:
ip (str): IP-адрес устройства для сканирования.
Returns:
List[str]: Список имён открытых портов (например, "HTTP", "SSH").
Raises:
Exception: Если произошла ошибка при сканировании портов.
"""
open_ports = [] open_ports = []
def check_port(port: int) -> Optional[str]: def check_port(port: int) -> Optional[str]:
"""Проверяет доступность конкретного порта.
Args:
port (int): Номер порта для проверки.
Returns:
Optional[str]: Имя порта, если открыт, иначе None.
"""
try: try:
pkt = sr1( pkt = sr1(
IP(dst=ip) / TCP(dport=port, flags="S"), IP(dst=ip) / TCP(dport=port, flags="S"),
@ -80,13 +130,33 @@ class PortScanner:
return [] return []
class DeviceInfoCollector: class DeviceInfoCollector:
"""Класс для сбора информации об устройствах, включая вендоров и хостнеймы.
Управляет кэшированием данных NetBIOS и DNS.
"""
def __init__(self): def __init__(self):
"""Инициализирует коллектор информации об устройствах.
Создаёт объект MacLookup для определения вендоров и инициализирует кэши.
"""
self.mac_lookup = MacLookup() self.mac_lookup = MacLookup()
self.nbtscan_cache = {} self.nbtscan_cache = {}
self.nbtscan_results = {} self.nbtscan_results = {}
@lru_cache(maxsize=1000) @lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str: def get_vendor(self, mac: str) -> str:
"""Определяет вендора по MAC-адресу с использованием кэширования.
Args:
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
Returns:
str: Название вендора или "Unknown", если вендор не определён.
Raises:
Exception: Если произошла ошибка при запросе к базе вендоров.
"""
try: try:
return self.mac_lookup.lookup(mac) return self.mac_lookup.lookup(mac)
except Exception as e: except Exception as e:
@ -95,6 +165,13 @@ class DeviceInfoCollector:
return "Unknown" return "Unknown"
def scan_nbtscan(self, subnet: str) -> None: def scan_nbtscan(self, subnet: str) -> None:
"""Выполняет сканирование NetBIOS для указанной подсети.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Сохраняет результаты в кэш, игнорируя хостнеймы "<unknown>".
"""
if subnet not in self.nbtscan_results: if subnet not in self.nbtscan_results:
try: try:
logger.debug(f"Запуск nbtscan для подсети {subnet}") logger.debug(f"Запуск nbtscan для подсети {subnet}")
@ -121,6 +198,16 @@ class DeviceInfoCollector:
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}") logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
def get_hostname(self, ip: str) -> Optional[str]: def get_hostname(self, ip: str) -> Optional[str]:
"""Определяет хостнейм для указанного IP-адреса.
Args:
ip (str): IP-адрес устройства.
Returns:
Optional[str]: Хостнейм, если найден, иначе None.
Сначала пытается использовать DNS, затем NetBIOS через nbtscan.
"""
try: try:
socket.setdefaulttimeout(2) socket.setdefaulttimeout(2)
return socket.gethostbyaddr(ip)[0] return socket.gethostbyaddr(ip)[0]
@ -138,12 +225,33 @@ class DeviceInfoCollector:
return None return None
class DeviceStorage: class DeviceStorage:
"""Класс для хранения и управления данными об устройствах.
Хранит устройства в JSON-файле и удаляет устаревшие записи.
"""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.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 = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True) self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.offline_timeout = timedelta(days=7) self.offline_timeout = timedelta(days=7)
def load_devices(self) -> Dict[str, Device]: def load_devices(self) -> Dict[str, Device]:
"""Загружает устройства из файла хранения.
Returns:
Dict[str, Device]: Словарь устройств, где ключ — MAC-адрес.
Удаляет устройства с неверным форматом last_seen и возвращает пустой словарь
при ошибке.
"""
try: try:
if self.storage_file.exists(): if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f: with open(self.storage_file, "r", encoding="utf-8") as f:
@ -160,6 +268,15 @@ class DeviceStorage:
return {} return {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]: 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() now = datetime.now()
filtered = {} filtered = {}
for mac, device in devices.items(): for mac, device in devices.items():
@ -177,6 +294,14 @@ class DeviceStorage:
return filtered return filtered
def save_devices(self, devices: List[Device]) -> None: def save_devices(self, devices: List[Device]) -> None:
"""Сохраняет список устройств в файл.
Args:
devices (List[Device]): Список объектов Device для сохранения.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try: try:
data = SubnetScanResult(devices=devices).model_dump() data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f: with open(self.storage_file, "w", encoding="utf-8") as f:
@ -186,7 +311,16 @@ class DeviceStorage:
logger.error(f"Ошибка при сохранении устройств: {str(e)}") logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService: class SubnetService:
"""Сервис для сканирования подсетей и управления устройствами.
Объединяет функционал сканирования сети, портов и хранения данных об устройствах.
"""
def __init__(self): def __init__(self):
"""Инициализирует сервис подсети с настройкой всех компонентов.
Автоматически определяет интерфейс и инициализирует сканеры и хранилище.
"""
self.interface = self._get_default_interface() self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface) self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface) self.port_scanner = PortScanner(self.interface)
@ -194,6 +328,13 @@ class SubnetService:
self.storage = DeviceStorage() self.storage = DeviceStorage()
def _get_default_interface(self) -> str: def _get_default_interface(self) -> str:
"""Определяет имя сетевого интерфейса по умолчанию.
Returns:
str: Название интерфейса (например, "eth0").
Использует информацию о шлюзе или первый доступный интерфейс, отличный от "lo".
"""
try: try:
gateways = netifaces.gateways() gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET) default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
@ -209,6 +350,19 @@ class SubnetService:
return "eth0" return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult: 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() start_time = time.time()
devices = [] devices = []
now = datetime.now().astimezone() now = datetime.now().astimezone()
@ -228,6 +382,14 @@ class SubnetService:
self.device_info.scan_nbtscan(subnet) self.device_info.scan_nbtscan(subnet)
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]: 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 ip, mac = ip_mac
if local_device and ip == local_device.ip: if local_device and ip == local_device.ip:
return None return None
@ -286,6 +448,14 @@ class SubnetService:
return SubnetScanResult(devices=devices) return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]: 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() now = datetime.now().astimezone()
formatted_now = now.strftime("%d-%m-%Y, %H:%M") formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try: try:

View file

@ -6,11 +6,9 @@ from functools import lru_cache
from typing import List, Optional, Dict, Tuple from typing import List, Optional, Dict, Tuple
from mac_vendor_lookup import MacLookup from mac_vendor_lookup import MacLookup
import pywifi import pywifi
from pywifi import Profile
from models.wifi import WifiAccessPoint, WifiScanResult from models.wifi import WifiAccessPoint, WifiScanResult
from pathlib import Path from pathlib import Path
import json import json
from datetime import datetime
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
@ -18,14 +16,34 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WifiStorage: class WifiStorage:
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID.""" """Класс для хранения данных о Wi-Fi сетях и их заметках по SSID.
Предоставляет методы для загрузки и сохранения заметок, связанных с SSID сетей,
в JSON-файле на диске.
"""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/wifi/wifi.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 = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True) self.storage_file.parent.mkdir(parents=True, exist_ok=True)
def load_notes(self) -> Dict[str, Optional[str]]: def load_notes(self) -> Dict[str, Optional[str]]:
"""Загружает заметки по SSID из файла хранения.""" """Загружает заметки по SSID из файла хранения.
Returns:
Dict[str, Optional[str]]: Словарь, где ключ — SSID, значение — заметка
или None, если заметки нет.
Raises:
Exception: Если произошла ошибка при чтении или парсинге файла.
"""
try: try:
if self.storage_file.exists(): if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f: with open(self.storage_file, "r", encoding="utf-8") as f:
@ -37,7 +55,15 @@ class WifiStorage:
return {} return {}
def save_notes(self, notes: Dict[str, Optional[str]]) -> None: def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
"""Сохраняет заметки по SSID в файл.""" """Сохраняет заметки по SSID в файл.
Args:
notes (Dict[str, Optional[str]]): Словарь с SSID как ключами и заметками
как значениями.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try: try:
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]} data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
with open(self.storage_file, "w", encoding="utf-8") as f: with open(self.storage_file, "w", encoding="utf-8") as f:
@ -47,16 +73,33 @@ class WifiStorage:
logger.error(f"Ошибка при сохранении заметок: {str(e)}") logger.error(f"Ошибка при сохранении заметок: {str(e)}")
class WifiService: class WifiService:
"""Сервис для сканирования текущих Wi-Fi точек доступа""" """Сервис для сканирования текущих Wi-Fi точек доступа.
Предоставляет функционал для обнаружения Wi-Fi сетей с использованием pywifi и iw,
определения характеристик точек доступа (сигнал, шифрование, канал и т.д.),
а также управления заметками.
"""
def __init__(self): def __init__(self):
"""Инициализирует сервис Wi-Fi с настройкой интерфейса и хранилища.
Инициализирует объект MacLookup для определения вендоров по MAC-адресам,
PyWiFi для работы с Wi-Fi-интерфейсами и объект хранилища заметок.
"""
self.mac_lookup = MacLookup() self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi() self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface() self.interface = self._get_wifi_interface()
self.storage = WifiStorage() self.storage = WifiStorage()
def _get_wifi_interface(self) -> Optional[object]: def _get_wifi_interface(self) -> Optional[object]:
"""Автоматически определяет активный Wi-Fi-интерфейс""" """Автоматически определяет активный Wi-Fi-интерфейс.
Returns:
Optional[object]: Объект интерфейса PyWiFi или None, если интерфейс не найден.
Проверяет доступные интерфейсы, выполняет тестовое сканирование и возвращает
первый активный интерфейс, исключая P2P-интерфейсы.
"""
try: try:
interfaces = self.wifi.interfaces() interfaces = self.wifi.interfaces()
if not interfaces: if not interfaces:
@ -93,6 +136,17 @@ class WifiService:
@lru_cache(maxsize=1000) @lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str: def get_vendor(self, mac: str) -> str:
"""Определяет вендора по MAC-адресу с использованием кэширования.
Args:
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
Returns:
str: Название вендора или "Unknown", если вендор не определён.
Raises:
Exception: Если произошла ошибка при запросе к базе вендоров.
"""
try: try:
return self.mac_lookup.lookup(mac) return self.mac_lookup.lookup(mac)
except Exception as e: except Exception as e:
@ -101,7 +155,14 @@ class WifiService:
return "Unknown" return "Unknown"
def _get_encryption_from_iw(self, ap_info: Dict) -> str: 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", "") security = ap_info.get("security", "")
logger.debug( logger.debug(
f"Шифрование для BSSID {ap_info.get('bssid')}: {security}") f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
@ -116,7 +177,14 @@ class WifiService:
return "Open" return "Open"
def _get_channel(self, frequency: Optional[int]) -> Optional[int]: def _get_channel(self, frequency: Optional[int]) -> Optional[int]:
"""Преобразует частоту в канал.""" """Преобразует частоту в номер канала.
Args:
frequency (Optional[int]): Частота в МГц или None.
Returns:
Optional[int]: Номер канала или None, если частота вне диапазона.
"""
if frequency is None: if frequency is None:
return None return None
try: try:
@ -133,7 +201,14 @@ class WifiService:
return None return None
def _get_frequency(self, frequency: Optional[int]) -> Optional[str]: 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: if frequency is None:
return None return None
try: try:
@ -150,7 +225,17 @@ class WifiService:
return None return None
def _get_iw_scan_data(self) -> List[Dict]: 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: try:
if not self.interface: if not self.interface:
logger.error("Интерфейс не определён") logger.error("Интерфейс не определён")
@ -225,7 +310,18 @@ class WifiService:
return [] return []
def scan_wifi(self, timeout: int = 30) -> WifiScanResult: 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() start_time = time.time()
aps = [] aps = []
@ -282,12 +378,10 @@ class WifiService:
if ssid not in best_aps_by_ssid or ( if ssid not in best_aps_by_ssid or (
current_ap.signal and best_aps_by_ssid[ssid].signal current_ap.signal and best_aps_by_ssid[ssid].signal
and current_ap.signal > 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) current_ap.note = saved_notes.get(ssid)
best_aps_by_ssid[ssid] = current_ap best_aps_by_ssid[ssid] = current_ap
aps = list(best_aps_by_ssid.values()) aps = list(best_aps_by_ssid.values())
# Сохраняем заметки для всех найденных SSID
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid} notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
self.storage.save_notes(notes_to_save) self.storage.save_notes(notes_to_save)
@ -313,7 +407,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке). - Ошибка: (False, сообщение об ошибке).
Raises: Raises:
Exception: Если произошла ошибка при сохранении. Exception: Если произошла ошибка при сохранении заметки.
""" """
try: try:
saved_notes = self.storage.load_notes() saved_notes = self.storage.load_notes()
@ -343,7 +437,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке). - Ошибка: (False, сообщение об ошибке).
Raises: Raises:
Exception: Если произошла ошибка при сохранении. Exception: Если произошла ошибка при сохранении заметки.
""" """
try: try:
saved_notes = self.storage.load_notes() saved_notes = self.storage.load_notes()
@ -373,7 +467,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке). - Ошибка: (False, сообщение об ошибке).
Raises: Raises:
Exception: Если произошла ошибка при сохранении. Exception: Если произошла ошибка при сохранении изменений.
""" """
try: try:
saved_notes = self.storage.load_notes() saved_notes = self.storage.load_notes()

View file

@ -1,4 +1,3 @@
# storage/json_storage.py
import json import json
import os import os
import logging import logging
@ -9,22 +8,40 @@ from models.auth import UserCreate, UserResponse
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class JsonStorage: class JsonStorage:
"""Хранилище пользователей в JSON-файле""" """Хранилище пользователей в JSON-файле.
Предоставляет методы для создания, чтения и проверки данных пользователей,
используя шифрование паролей с помощью bcrypt.
"""
def __init__(self, file_path: str = "/etc/gonets/users.json"): 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.file_path = file_path
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self._ensure_file_exists() self._ensure_file_exists()
def _ensure_file_exists(self): def _ensure_file_exists(self):
"""Создаёт JSON-файл, если он не существует""" """Создаёт JSON-файл, если он не существует.
Создаёт родительские директории и файл с начальной структурой данных,
устанавливая права доступа 600.
Raises:
Exception: Если произошла ошибка при создании файла или директории.
"""
try: try:
logger.info(f"Проверка существования файла {self.file_path}") logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info( logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True) os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}") logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
@ -40,7 +57,15 @@ class JsonStorage:
raise raise
def _read_data(self) -> dict: def _read_data(self) -> dict:
"""Читает данные из JSON-файла""" """Читает данные из JSON-файла.
Returns:
dict: Словарь с данными пользователей и следующим ID.
Raises:
FileNotFoundError: Если файл отсутствует (возвращает пустую структуру).
Exception: Если произошла ошибка при чтении файла.
"""
try: try:
with open(self.file_path, "r") as f: with open(self.file_path, "r") as f:
return json.load(f) return json.load(f)
@ -56,7 +81,14 @@ class JsonStorage:
raise raise
def _write_data(self, data: dict): def _write_data(self, data: dict):
"""Записывает данные в JSON файл""" """Записывает данные в JSON-файл.
Args:
data (dict): Словарь с данными для записи (пользователи и next_id).
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try: try:
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
@ -67,7 +99,18 @@ class JsonStorage:
raise raise
def create_user(self, user: UserCreate) -> UserResponse: def create_user(self, user: UserCreate) -> UserResponse:
"""Создаёт нового пользователя""" """Создаёт нового пользователя с хешированием пароля.
Args:
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
Returns:
UserResponse: Объект с данными созданного пользователя (id, username, is_admin).
Raises:
ValueError: Если пользователь с таким именем уже существует.
Exception: Если произошла ошибка при записи данных.
"""
try: try:
data = self._read_data() data = self._read_data()
users = data["users"] users = data["users"]
@ -99,7 +142,17 @@ class JsonStorage:
raise raise
def get_user_by_username(self, username: str) -> Optional[dict]: def get_user_by_username(self, username: str) -> Optional[dict]:
"""Получает пользователя по имени""" """Получает данные пользователя по имени.
Args:
username (str): Имя пользователя для поиска.
Returns:
Optional[dict]: Словарь с данными пользователя или None, если пользователь не найден.
Raises:
Exception: Если произошла ошибка при чтении файла.
"""
try: try:
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info( logger.info(
@ -117,7 +170,14 @@ class JsonStorage:
) )
raise raise
def verify_password(self, plain_password: str, def verify_password(self, plain_password: str, hashed_password: str) -> bool:
hashed_password: str) -> bool: """Проверяет соответствие введённого пароля хэшированному.
"""Проверяет пароль"""
return self.pwd_context.verify(plain_password, hashed_password) Args:
plain_password (str): Введённый пользователем пароль.
hashed_password (str): Хэшированный пароль из хранилища.
Returns:
bool: True, если пароль верен, False в противном случае.
"""
return self.pwd_context.verify(plain_password, hashed_password)

View file

@ -1,4 +1,3 @@
# storage/session_storage.py
import json import json
import os import os
import uuid import uuid
@ -9,19 +8,38 @@ from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SessionStorage: class SessionStorage:
"""Хранилище сессий пользователей в JSON-файле.
Предоставляет методы для создания, получения, удаления и очистки сессий с
учётом срока действия.
"""
def __init__(self, file_path: str = "/etc/gonets/sessions.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.file_path = file_path
self._ensure_file_exists() self._ensure_file_exists()
def _ensure_file_exists(self): def _ensure_file_exists(self):
"""Создаёт JSON-файл, если он не существует.
Создаёт родительские директории и файл с начальной структурой данных,
устанавливая права доступа 600.
Raises:
Exception: Если произошла ошибка при создании файла или директории.
"""
try: try:
logger.info(f"Проверка существования файла {self.file_path}") logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info( logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True) os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}") logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
@ -37,6 +55,14 @@ class SessionStorage:
raise raise
def _read_data(self) -> Dict: def _read_data(self) -> Dict:
"""Читает данные из JSON-файла.
Returns:
Dict: Словарь с данными сессий.
Raises:
Exception: Если произошла ошибка при чтении файла.
"""
try: try:
with open(self.file_path, "r") as f: with open(self.file_path, "r") as f:
return json.load(f) return json.load(f)
@ -47,6 +73,14 @@ class SessionStorage:
raise raise
def _write_data(self, data: Dict): def _write_data(self, data: Dict):
"""Записывает данные в JSON-файл.
Args:
data (Dict): Словарь с данными сессий для записи.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try: try:
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
@ -57,7 +91,23 @@ class SessionStorage:
raise raise
def create_session(self, username: str, token: str, client_id: str) -> str: 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: try:
self.clean_expired_sessions() self.clean_expired_sessions()
data = self._read_data() data = self._read_data()
@ -83,7 +133,22 @@ class SessionStorage:
raise raise
def get_latest_session(self, client_id: str) -> Optional[Dict]: 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: try:
self.clean_expired_sessions() self.clean_expired_sessions()
data = self._read_data() data = self._read_data()
@ -114,7 +179,17 @@ class SessionStorage:
raise raise
def delete_session(self, session_id: str): def delete_session(self, session_id: str):
"""Удаляет сессию по session_id""" """Удаляет сессию по указанному session_id.
Args:
session_id (str): Уникальный идентификатор сессии.
Raises:
Exception: Если произошла ошибка при чтении или записи данных.
Note:
Очищает истекшие сессии перед удалением.
"""
try: try:
self.clean_expired_sessions() self.clean_expired_sessions()
data = self._read_data() data = self._read_data()
@ -129,25 +204,30 @@ class SessionStorage:
raise raise
def clean_expired_sessions(self): def clean_expired_sessions(self):
"""Удаляет все истекшие сессии из хранилища""" """Удаляет все истекшие сессии из хранилища.
Raises:
Exception: Если произошла ошибка при чтении или записи данных.
Note:
Проверяет срок действия каждой сессии и удаляет те, что истекли,
записывая обновлённые данные.
"""
try: try:
data = self._read_data() data = self._read_data()
expired_sessions = [] expired_sessions = []
current_time = datetime.utcnow() current_time = datetime.utcnow()
# Находим истекшие сессии
for session_id, session in list(data["sessions"].items()): for session_id, session in list(data["sessions"].items()):
expiry = datetime.fromisoformat(session["expiry"]) expiry = datetime.fromisoformat(session["expiry"])
if current_time > expiry: if current_time > expiry:
expired_sessions.append(session_id) expired_sessions.append(session_id)
# Удаляем истекшие сессии
for session_id in expired_sessions: for session_id in expired_sessions:
del data["sessions"][session_id] del data["sessions"][session_id]
logger.info( logger.info(
f"Удалена истекшая сессия с session_id {session_id}") f"Удалена истекшая сессия с session_id {session_id}")
# Записываем обновлённые данные
if expired_sessions: if expired_sessions:
self._write_data(data) self._write_data(data)
logger.info(f"Удалено {len(expired_sessions)} истекших сессий") logger.info(f"Удалено {len(expired_sessions)} истекших сессий")
@ -158,4 +238,4 @@ class SessionStorage:
logger.error( logger.error(
f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}" f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}"
) )
raise raise