diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index 0539432b..8949dad9 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -477,47 +477,6 @@ class RemnaWaveAPI: return response['response']['eventSent'] - async def list_components(self) -> List[Dict[str, Any]]: - response = await self._make_request('GET', '/api/components') - components = response.get('response') - - if isinstance(components, dict): - if 'components' in components: - components = components['components'] - elif 'items' in components: - components = components['items'] - - if not components: - return [] - - if isinstance(components, list): - return components - - return [components] - - async def get_component(self, component_id: str) -> Dict[str, Any]: - response = await self._make_request('GET', f'/api/components/{component_id}') - component = response.get('response') - - if isinstance(component, dict): - return component - - return {} - - async def perform_component_action( - self, - component_id: str, - action: str, - payload: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - endpoint = f'/api/components/{component_id}/actions/{action}' - response = await self._make_request('POST', endpoint, data=payload) - return response.get('response') or {} - - async def update_component(self, component_id: str, payload: Dict[str, Any]) -> Dict[str, Any]: - response = await self._make_request('PATCH', f'/api/components/{component_id}', payload) - return response.get('response') or {} - async def get_subscription_info(self, short_uuid: str) -> SubscriptionInfo: response = await self._make_request('GET', f'/api/sub/{short_uuid}/info') return self._parse_subscription_info(response['response']) diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index cb1aa63b..1a02a08f 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -254,7 +254,7 @@ class RemnaWaveService: try: if not bandwidth_str or bandwidth_str == '0 B' or bandwidth_str == '0': return 0 - + bandwidth_str = bandwidth_str.replace(' ', '').upper() units = { @@ -291,74 +291,9 @@ class RemnaWaveService: except Exception as e: logger.error(f"Ошибка парсинга строки трафика '{bandwidth_str}': {e}") return 0 - - def _normalize_component(self, component: Dict[str, Any]) -> Dict[str, Any]: - if not isinstance(component, dict): - return {} - - metadata = dict(component) - - component_uuid = ( - metadata.get("uuid") - or metadata.get("id") - or metadata.get("slug") - or metadata.get("name") - ) - - installed_version = ( - metadata.get("installedVersion") - or metadata.get("installed_version") - or metadata.get("version") - ) - latest_version = ( - metadata.get("latestVersion") - or metadata.get("latest_version") - or metadata.get("availableVersion") - ) - - status = metadata.get("status") or metadata.get("state") - - is_installed = metadata.get("isInstalled") - if is_installed is None: - for key in ("installed", "is_installed"): - if key in metadata: - is_installed = metadata[key] - break - - is_enabled = metadata.get("isEnabled") - if is_enabled is None: - for key in ("enabled", "is_enabled"): - if key in metadata: - is_enabled = metadata[key] - break - - tags = metadata.get("tags") or metadata.get("labels") or metadata.get("keywords") - if isinstance(tags, str): - tags = [tags] - elif isinstance(tags, list): - tags = [str(tag) for tag in tags] - else: - tags = [] - - return { - "uuid": component_uuid, - "name": metadata.get("name") or metadata.get("title") or component_uuid, - "slug": metadata.get("slug") or metadata.get("id"), - "status": status, - "installed_version": installed_version, - "latest_version": latest_version, - "category": metadata.get("category") or metadata.get("type"), - "description": metadata.get("description") or metadata.get("details"), - "is_installed": is_installed, - "is_enabled": is_enabled, - "installed_at": metadata.get("installedAt") or metadata.get("installed_at"), - "updated_at": metadata.get("updatedAt") or metadata.get("updated_at"), - "tags": tags, - "metadata": metadata, - } - + async def get_all_nodes(self) -> List[Dict[str, Any]]: - + try: async with self.get_api_client() as api: nodes = await api.get_all_nodes() @@ -402,7 +337,7 @@ class RemnaWaveService: try: async with self.get_api_client() as api: node = await api.get_node_by_uuid(node_uuid) - + if not node: return None @@ -423,163 +358,7 @@ class RemnaWaveService: except Exception as e: logger.error(f"Ошибка получения информации о ноде {node_uuid}: {e}") return None - - async def list_components(self) -> List[Dict[str, Any]]: - try: - async with self.get_api_client() as api: - raw_components = await api.list_components() - - normalized = [ - self._normalize_component(component) - for component in raw_components - if isinstance(component, dict) - ] - - logger.info(f"✅ Получено {len(normalized)} компонентов из Remnawave") - return normalized - - except RemnaWaveConfigurationError: - raise - except RemnaWaveAPIError: - raise - except Exception as error: - logger.error(f"Ошибка получения списка компонентов Remnawave: {error}") - raise RemnaWaveAPIError( - "Не удалось получить список компонентов", - response_data={"details": str(error)}, - ) - - async def get_component_details(self, component_id: str) -> Optional[Dict[str, Any]]: - try: - async with self.get_api_client() as api: - raw_component = await api.get_component(component_id) - - if not raw_component: - return None - - return self._normalize_component(raw_component) - - except RemnaWaveConfigurationError: - raise - except RemnaWaveAPIError as error: - if error.status_code == 404: - logger.info(f"Компонент Remnawave {component_id} не найден") - return None - raise - except Exception as err: - logger.error(f"Ошибка получения компонента Remnawave {component_id}: {err}") - raise RemnaWaveAPIError( - "Не удалось получить данные компонента", - response_data={"details": str(err)}, - ) - - async def perform_component_action( - self, - component_id: str, - action: str, - payload: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - if not action or not re.fullmatch(r"[A-Za-z0-9_-]+", action): - raise ValueError("Недопустимое действие для компонента") - - try: - async with self.get_api_client() as api: - raw_result = await api.perform_component_action(component_id, action, payload) - - except RemnaWaveConfigurationError: - raise - except RemnaWaveAPIError: - raise - except Exception as err: - logger.error( - f"Ошибка выполнения действия '{action}' для компонента {component_id}: {err}" - ) - raise RemnaWaveAPIError( - "Не удалось выполнить действие с компонентом", - response_data={"details": str(err)}, - ) - - details: Dict[str, Any] - if isinstance(raw_result, dict): - details = raw_result - elif raw_result is None: - details = {} - else: - details = {"response": raw_result} - - component_data = details.get("component") or details.get("data") or details.get("response") - normalized_component = ( - self._normalize_component(component_data) - if isinstance(component_data, dict) - else None - ) - - success = details.get("success") if isinstance(details.get("success"), bool) else None - if success is None: - success = True - - message = details.get("message") if isinstance(details.get("message"), str) else None - if not message: - message = f"Действие '{action}' выполнено" - - return { - "success": bool(success), - "message": message, - "details": details, - "component": normalized_component, - } - - async def update_component_settings( - self, component_id: str, payload: Dict[str, Any] - ) -> Dict[str, Any]: - if not payload: - raise ValueError("Данные для обновления компонента не могут быть пустыми") - - try: - async with self.get_api_client() as api: - raw_result = await api.update_component(component_id, payload) - - except RemnaWaveConfigurationError: - raise - except RemnaWaveAPIError: - raise - except Exception as err: - logger.error(f"Ошибка обновления компонента Remnawave {component_id}: {err}") - raise RemnaWaveAPIError( - "Не удалось обновить компонент", - response_data={"details": str(err)}, - ) - - details: Dict[str, Any] - if isinstance(raw_result, dict): - details = raw_result - elif raw_result is None: - details = {} - else: - details = {"response": raw_result} - - component_data = details.get("component") or details.get("response") - normalized_component = ( - self._normalize_component(component_data) - if isinstance(component_data, dict) - else None - ) - - success = details.get("success") if isinstance(details.get("success"), bool) else None - if success is None: - success = True - - message = details.get("message") if isinstance(details.get("message"), str) else None - if not message: - message = "Настройки компонента обновлены" - - return { - "success": bool(success), - "message": message, - "details": details, - "component": normalized_component, - } - + async def manage_node(self, node_uuid: str, action: str) -> bool: try: async with self.get_api_client() as api: diff --git a/app/webapi/app.py b/app/webapi/app.py index 8b11b82e..dd3249f2 100644 --- a/app/webapi/app.py +++ b/app/webapi/app.py @@ -10,7 +10,6 @@ from .routes import ( config, health, promo_groups, - remnawave, stats, subscriptions, tickets, @@ -52,7 +51,6 @@ def create_web_api_app() -> FastAPI: app.include_router(tickets.router, prefix="/tickets", tags=["support"]) app.include_router(transactions.router, prefix="/transactions", tags=["transactions"]) app.include_router(promo_groups.router, prefix="/promo-groups", tags=["promo-groups"]) - app.include_router(remnawave.router, prefix="/remnawave", tags=["remnawave"]) app.include_router(tokens.router, prefix="/tokens", tags=["auth"]) return app diff --git a/app/webapi/routes/remnawave.py b/app/webapi/routes/remnawave.py deleted file mode 100644 index adb02798..00000000 --- a/app/webapi/routes/remnawave.py +++ /dev/null @@ -1,152 +0,0 @@ -from __future__ import annotations - -from typing import Optional - -from fastapi import APIRouter, HTTPException, Security, status - -from app.external.remnawave_api import RemnaWaveAPIError -from app.services.remnawave_service import ( - RemnaWaveConfigurationError, - RemnaWaveService, -) - -from ..dependencies import require_api_token -from ..schemas.remnawave import ( - ComponentActionRequest, - ComponentActionResponse, - ComponentInfo, - ComponentListResponse, - ComponentUpdateRequest, -) - -router = APIRouter() - - -def _map_api_error(error: RemnaWaveAPIError) -> HTTPException: - status_code = error.status_code or status.HTTP_502_BAD_GATEWAY - if status_code < 400 or status_code >= 600: - status_code = status.HTTP_502_BAD_GATEWAY - return HTTPException(status_code=status_code, detail=error.message) - - -@router.get("/components", response_model=ComponentListResponse) -async def list_components( - _: object = Security(require_api_token), -) -> ComponentListResponse: - service = RemnaWaveService() - - try: - components = await service.list_components() - except RemnaWaveConfigurationError as error: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error) - ) from error - except RemnaWaveAPIError as error: - raise _map_api_error(error) - - items = [ComponentInfo(**component) for component in components] - return ComponentListResponse(items=items, total=len(items)) - - -@router.get("/components/{component_id}", response_model=ComponentInfo) -async def get_component( - component_id: str, - _: object = Security(require_api_token), -) -> ComponentInfo: - service = RemnaWaveService() - - try: - component = await service.get_component_details(component_id) - except RemnaWaveConfigurationError as error: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error) - ) from error - except RemnaWaveAPIError as error: - raise _map_api_error(error) - - if not component: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Компонент не найден") - - return ComponentInfo(**component) - - -@router.post( - "/components/{component_id}/actions/{action}", - response_model=ComponentActionResponse, -) -async def perform_component_action( - component_id: str, - action: str, - request: Optional[ComponentActionRequest] = None, - _: object = Security(require_api_token), -) -> ComponentActionResponse: - service = RemnaWaveService() - payload = request.payload if request else None - - try: - result = await service.perform_component_action(component_id, action, payload) - except RemnaWaveConfigurationError as error: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error) - ) from error - except RemnaWaveAPIError as error: - raise _map_api_error(error) - except ValueError as error: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error - - component_data = result.get("component") - component = ComponentInfo(**component_data) if isinstance(component_data, dict) else None - - details = result.get("details") - if details is not None and not isinstance(details, dict): - details = {"response": details} - - return ComponentActionResponse( - success=bool(result.get("success", True)), - message=result.get("message"), - component=component, - details=details, - ) - - -@router.patch( - "/components/{component_id}", - response_model=ComponentActionResponse, -) -async def update_component( - component_id: str, - request: ComponentUpdateRequest, - _: object = Security(require_api_token), -) -> ComponentActionResponse: - service = RemnaWaveService() - - if not request.payload: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Поле payload не может быть пустым", - ) - - try: - result = await service.update_component_settings(component_id, request.payload) - except RemnaWaveConfigurationError as error: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(error) - ) from error - except RemnaWaveAPIError as error: - raise _map_api_error(error) - except ValueError as error: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error - - component_data = result.get("component") - component = ComponentInfo(**component_data) if isinstance(component_data, dict) else None - - details = result.get("details") - if details is not None and not isinstance(details, dict): - details = {"response": details} - - return ComponentActionResponse( - success=bool(result.get("success", True)), - message=result.get("message"), - component=component, - details=details, - ) diff --git a/app/webapi/schemas/remnawave.py b/app/webapi/schemas/remnawave.py deleted file mode 100644 index bc2404dd..00000000 --- a/app/webapi/schemas/remnawave.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict, List, Optional - -from pydantic import BaseModel, ConfigDict, Field - - -class ComponentInfo(BaseModel): - """Normalized representation of a Remnawave component.""" - - model_config = ConfigDict(extra="allow", populate_by_name=True) - - uuid: Optional[str] = None - name: Optional[str] = None - slug: Optional[str] = None - status: Optional[str] = None - installed_version: Optional[str] = Field(default=None, alias="installedVersion") - latest_version: Optional[str] = Field(default=None, alias="latestVersion") - category: Optional[str] = None - description: Optional[str] = None - is_installed: Optional[bool] = Field(default=None, alias="isInstalled") - is_enabled: Optional[bool] = Field(default=None, alias="isEnabled") - installed_at: Optional[str] = Field(default=None, alias="installedAt") - updated_at: Optional[str] = Field(default=None, alias="updatedAt") - tags: Optional[List[str]] = None - metadata: Optional[Dict[str, Any]] = None - - -class ComponentListResponse(BaseModel): - items: List[ComponentInfo] - total: int - - -class ComponentActionRequest(BaseModel): - payload: Optional[Dict[str, Any]] = None - - -class ComponentUpdateRequest(BaseModel): - payload: Dict[str, Any] = Field(default_factory=dict) - - -class ComponentActionResponse(BaseModel): - success: bool - message: Optional[str] = None - component: Optional[ComponentInfo] = None - details: Optional[Dict[str, Any]] = None diff --git a/docs/web-admin-integration.md b/docs/web-admin-integration.md index e465f231..d5d2693d 100644 --- a/docs/web-admin-integration.md +++ b/docs/web-admin-integration.md @@ -126,10 +126,6 @@ curl -X POST "http://127.0.0.1:8080/tokens" \ | `POST` | `/promo-groups` | Создать промо-группу. | `PATCH` | `/promo-groups/{id}` | Обновить промо-группу. | `DELETE` | `/promo-groups/{id}` | Удалить промо-группу. -| `GET` | `/remnawave/components` | Список компонентов Remnawave. -| `GET` | `/remnawave/components/{id}` | Детали конкретного компонента. -| `POST` | `/remnawave/components/{id}/actions/{action}` | Выполнить действие над компонентом (install, update, restart и т.д.). -| `PATCH` | `/remnawave/components/{id}` | Обновить настройки компонента. | `GET` | `/tokens` | Управление токенами доступа. > Все списковые эндпоинты поддерживают пагинацию (`limit`, `offset`) и фильтры, описанные в OpenAPI спецификации. Если `WEB_API_DOCS_ENABLED=true`, документация доступна по `/docs`. В ответах `/settings` поле `choices` всегда массив: пустой список означает отсутствие предопределённых значений.