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