From 01582540cab289910eb21ab7ef0cb1dd90af7f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=99=A8?= Date: Tue, 23 Nov 2021 16:53:13 +0800 Subject: [PATCH] feat: add query api Add calling prometheus interface Change-Id: I2aeac35d91c9b94dcd0549f1a0ac7ca25f6d268f --- docs/api/swagger.json | 322 ++++++++++++++++++ etc/skyline.yaml.sample | 4 + .../skyline_apiserver/api/v1/__init__.py | 3 +- .../skyline_apiserver/api/v1/prometheus.py | 174 ++++++++++ .../skyline_apiserver/config/default.py | 32 ++ .../skyline_apiserver/schemas/__init__.py | 14 + .../skyline_apiserver/schemas/prometheus.py | 41 +++ .../skyline_apiserver/types/constants.py | 4 + 8 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 libs/skyline-apiserver/skyline_apiserver/api/v1/prometheus.py create mode 100644 libs/skyline-apiserver/skyline_apiserver/schemas/prometheus.py diff --git a/docs/api/swagger.json b/docs/api/swagger.json index e1f3e21..089b9b4 100644 --- a/docs/api/swagger.json +++ b/docs/api/swagger.json @@ -1230,6 +1230,186 @@ } } }, + "/api/v1/query": { + "get": { + "tags": [ + "Prometheus" + ], + "summary": "Prometheus Query", + "description": "Prometheus query API.", + "operationId": "prometheus_query_api_v1_query_get", + "parameters": [ + { + "required": false, + "schema": { + "title": "Query", + "type": "string" + }, + "name": "query", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Time", + "type": "string" + }, + "name": "time", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Timeout", + "type": "string" + }, + "name": "timeout", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrometheusQueryResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedMessage" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/query_range": { + "get": { + "tags": [ + "Prometheus" + ], + "summary": "Prometheus Query Range", + "description": "Prometheus query_range API.", + "operationId": "prometheus_query_range_api_v1_query_range_get", + "parameters": [ + { + "required": false, + "schema": { + "title": "Query", + "type": "string" + }, + "name": "query", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Start", + "type": "string" + }, + "name": "start", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "End", + "type": "string" + }, + "name": "end", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Step", + "type": "string" + }, + "name": "step", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Timeout", + "type": "string" + }, + "name": "timeout", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrometheusQueryRangeResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedMessage" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/api/v1/contrib/keystone_endpoints": { "get": { "tags": [ @@ -2912,6 +3092,148 @@ } } }, + "PrometheusQueryData": { + "title": "PrometheusQueryData", + "required": [ + "result", + "resultType" + ], + "type": "object", + "properties": { + "result": { + "title": "Result", + "type": "array", + "items": { + "$ref": "#/components/schemas/PrometheusQueryResult" + } + }, + "resultType": { + "title": "Resulttype", + "type": "string" + } + } + }, + "PrometheusQueryRangeData": { + "title": "PrometheusQueryRangeData", + "required": [ + "result", + "resultType" + ], + "type": "object", + "properties": { + "result": { + "title": "Result", + "type": "array", + "items": { + "$ref": "#/components/schemas/PrometheusQueryRangeResult" + } + }, + "resultType": { + "title": "Resulttype", + "type": "string" + } + } + }, + "PrometheusQueryRangeResponse": { + "title": "PrometheusQueryRangeResponse", + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "title": "Status", + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/PrometheusQueryRangeData" + }, + "errorType": { + "title": "Errortype", + "type": "string" + }, + "error": { + "title": "Error", + "type": "string" + }, + "warnings": { + "title": "Warnings", + "type": "string" + } + } + }, + "PrometheusQueryRangeResult": { + "title": "PrometheusQueryRangeResult", + "required": [ + "metric", + "values" + ], + "type": "object", + "properties": { + "metric": { + "title": "Metric", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "values": { + "title": "Values", + "type": "array", + "items": {} + } + } + }, + "PrometheusQueryResponse": { + "title": "PrometheusQueryResponse", + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "title": "Status", + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/PrometheusQueryData" + }, + "errorType": { + "title": "Errortype", + "type": "string" + }, + "error": { + "title": "Error", + "type": "string" + }, + "warnings": { + "title": "Warnings", + "type": "string" + } + } + }, + "PrometheusQueryResult": { + "title": "PrometheusQueryResult", + "required": [ + "metric", + "value" + ], + "type": "object", + "properties": { + "metric": { + "title": "Metric", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "value": { + "title": "Value", + "type": "array", + "items": {} + } + } + }, "Role": { "title": "Role", "required": [ diff --git a/etc/skyline.yaml.sample b/etc/skyline.yaml.sample index 346bb1f..26c529d 100644 --- a/etc/skyline.yaml.sample +++ b/etc/skyline.yaml.sample @@ -5,6 +5,10 @@ default: database_url: mysql://root:root@localhost:3306/skyline debug: false log_dir: ./log + prometheus_basic_auth_password: '' + prometheus_basic_auth_user: '' + prometheus_enable_basic_auth: false + prometheus_endpoint: http://localhost:9091 secret_key: aCtmgbcUqYUy_HNVg5BDXCaeJgJQzHJXwqbXr0Nmb2o session_name: session developer: diff --git a/libs/skyline-apiserver/skyline_apiserver/api/v1/__init__.py b/libs/skyline-apiserver/skyline_apiserver/api/v1/__init__.py index fadb3bb..e326fc9 100644 --- a/libs/skyline-apiserver/skyline_apiserver/api/v1/__init__.py +++ b/libs/skyline-apiserver/skyline_apiserver/api/v1/__init__.py @@ -13,11 +13,12 @@ # limitations under the License. from fastapi import APIRouter -from skyline_apiserver.api.v1 import contrib, extension, login, policy, setting +from skyline_apiserver.api.v1 import contrib, extension, login, policy, prometheus, setting api_router = APIRouter() api_router.include_router(login.router, tags=["Login"]) api_router.include_router(extension.router, tags=["Extension"]) +api_router.include_router(prometheus.router, tags=["Prometheus"]) api_router.include_router(contrib.router, tags=["Contrib"]) api_router.include_router(policy.router, tags=["Policy"]) api_router.include_router(setting.router, tags=["Setting"]) diff --git a/libs/skyline-apiserver/skyline_apiserver/api/v1/prometheus.py b/libs/skyline-apiserver/skyline_apiserver/api/v1/prometheus.py new file mode 100644 index 0000000..ca29ff4 --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/api/v1/prometheus.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, status +from httpx import codes +from skyline_apiserver import schemas +from skyline_apiserver.api import deps +from skyline_apiserver.config import CONF +from skyline_apiserver.types import constants +from skyline_apiserver.utils.httpclient import _http_request +from skyline_apiserver.utils.roles import is_system_admin_or_reader + +router = APIRouter() + + +def get_prometheus_query_response( + resp: dict, + profile: schemas.Profile, +) -> schemas.PrometheusQueryResponse: + ret = schemas.PrometheusQueryResponse(status=resp["status"]) + if "warnings" in resp: + ret.warnings = resp["warnings"] + if "errorType" in resp: + ret.errorType = resp["errorType"] + if "error" in resp: + ret.error = resp["error"] + if "data" in resp: + result = [ + schemas.PrometheusQueryResult(metric=i["metric"], value=i["value"]) + for i in resp["data"]["result"] + ] + + if not is_system_admin_or_reader(profile): + result = [ + i + for i in result + if "project_id" in i.metric and i.metric["project_id"] == profile.project.id + ] + + data = schemas.PrometheusQueryData( + resultType=resp["data"]["resultType"], + result=result, + ) + ret.data = data + + return ret + + +def get_prometheus_query_range_response( + resp: dict, + profile: schemas.Profile, +) -> schemas.PrometheusQueryRangeResponse: + ret = schemas.PrometheusQueryRangeResponse(status=resp["status"]) + if "warnings" in resp: + ret.warnings = resp["warnings"] + if "errorType" in resp: + ret.errorType = resp["errorType"] + if "error" in resp: + ret.error = resp["error"] + if "data" in resp: + result = [ + schemas.PrometheusQueryRangeResult(metric=i["metric"], values=i["values"]) + for i in resp["data"]["result"] + ] + + if not is_system_admin_or_reader(profile): + result = [ + i + for i in result + if "project_id" in i.metric and i.metric["project_id"] == profile.project.id + ] + + data = schemas.PrometheusQueryRangeData( + resultType=resp["data"]["resultType"], + result=result, + ) + ret.data = data + + return ret + + +@router.get( + "/query", + description="Prometheus query API.", + responses={ + 200: {"model": schemas.PrometheusQueryResponse}, + 401: {"model": schemas.common.UnauthorizedMessage}, + 500: {"model": schemas.common.InternalServerErrorMessage}, + }, + response_model=schemas.PrometheusQueryResponse, + status_code=status.HTTP_200_OK, + response_description="OK", + response_model_exclude_none=True, +) +async def prometheus_query( + query: str = None, + time: str = None, + timeout: str = None, + profile: schemas.Profile = Depends(deps.get_profile_update_jwt), +) -> schemas.PrometheusQueryResponse: + kwargs = {} + if query is not None: + kwargs["query"] = query + if time is not None: + kwargs["time"] = time + if timeout is not None: + kwargs["timeout"] = timeout + + auth = None + if CONF.default.prometheus_enable_basic_auth: + auth = ( + CONF.default.prometheus_basic_auth_user, + CONF.default.prometheus_basic_auth_password, + ) + resp = await _http_request( + url=CONF.default.prometheus_endpoint + constants.PROMETHEUS_QUERY_API, + params=kwargs, + auth=auth, + ) + + if resp.status_code != codes.OK: + raise HTTPException(status_code=resp.status_code, detail=resp.text) + + return get_prometheus_query_response(resp.json(), profile) + + +@router.get( + "/query_range", + description="Prometheus query_range API.", + responses={ + 200: {"model": schemas.PrometheusQueryRangeResponse}, + 401: {"model": schemas.common.UnauthorizedMessage}, + 500: {"model": schemas.common.InternalServerErrorMessage}, + }, + response_model=schemas.PrometheusQueryRangeResponse, + status_code=status.HTTP_200_OK, + response_description="OK", + response_model_exclude_none=True, +) +async def prometheus_query_range( + query: str = None, + start: str = None, + end: str = None, + step: str = None, + timeout: str = None, + profile: schemas.Profile = Depends(deps.get_profile_update_jwt), +) -> schemas.PrometheusQueryRangeResponse: + kwargs = {} + if query is not None: + kwargs["query"] = query + if start is not None: + kwargs["start"] = start + if end is not None: + kwargs["end"] = end + if step is not None: + kwargs["step"] = step + if timeout is not None: + kwargs["timeout"] = timeout + + auth = None + if CONF.default.prometheus_enable_basic_auth: + auth = ( + CONF.default.prometheus_basic_auth_user, + CONF.default.prometheus_basic_auth_password, + ) + resp = await _http_request( + url=CONF.default.prometheus_endpoint + constants.PROMETHEUS_QUERY_RANGE_API, + params=kwargs, + auth=auth, + ) + + if resp.status_code != codes.OK: + raise HTTPException(status_code=resp.status_code, detail=resp.text) + + return get_prometheus_query_range_response(resp.json(), profile) diff --git a/libs/skyline-apiserver/skyline_apiserver/config/default.py b/libs/skyline-apiserver/skyline_apiserver/config/default.py index 5c245bd..71d331c 100644 --- a/libs/skyline-apiserver/skyline_apiserver/config/default.py +++ b/libs/skyline-apiserver/skyline_apiserver/config/default.py @@ -75,6 +75,34 @@ database_url = Opt( default="mysql://root:root@localhost:3306/skyline", ) +prometheus_endpoint = Opt( + name="prometheus_endpoint", + description="Prometheus Endpoint", + schema=StrictStr, + default="http://localhost:9091", +) + +prometheus_enable_basic_auth = Opt( + name="prometheus_enable_basic_auth", + description="Start Prometheus Basic Auth", + schema=StrictBool, + default=False, +) + +prometheus_basic_auth_user = Opt( + name="prometheus_basic_auth_user", + description="Prometheus Basic Auth username", + schema=StrictStr, + default="", +) + +prometheus_basic_auth_password = Opt( + name="prometheus_basic_auth_password", + description="Prometheus Basic Auth password", + schema=StrictStr, + default="", +) + GROUP_NAME = __name__.split(".")[-1] ALL_OPTS = ( debug, @@ -85,6 +113,10 @@ ALL_OPTS = ( cors_allow_origins, session_name, database_url, + prometheus_endpoint, + prometheus_enable_basic_auth, + prometheus_basic_auth_user, + prometheus_basic_auth_password, ) __all__ = ("GROUP_NAME", "ALL_OPTS") diff --git a/libs/skyline-apiserver/skyline_apiserver/schemas/__init__.py b/libs/skyline-apiserver/skyline_apiserver/schemas/__init__.py index 527cc67..06d9e5f 100644 --- a/libs/skyline-apiserver/skyline_apiserver/schemas/__init__.py +++ b/libs/skyline-apiserver/skyline_apiserver/schemas/__init__.py @@ -35,6 +35,14 @@ from .extension import ( ) from .login import Credential, Domain, License, Payload, Profile, Project, Region, Role from .policy import Policies, PoliciesRules +from .prometheus import ( + PrometheusQueryData, + PrometheusQueryRangeData, + PrometheusQueryRangeResponse, + PrometheusQueryRangeResult, + PrometheusQueryResponse, + PrometheusQueryResult, +) from .setting import Setting, Settings, UpdateSetting __all__ = ( @@ -70,4 +78,10 @@ __all__ = ( "Setting", "Settings", "UpdateSetting", + "PrometheusQueryResponse", + "PrometheusQueryData", + "PrometheusQueryResult", + "PrometheusQueryRangeResponse", + "PrometheusQueryRangeData", + "PrometheusQueryRangeResult", ) diff --git a/libs/skyline-apiserver/skyline_apiserver/schemas/prometheus.py b/libs/skyline-apiserver/skyline_apiserver/schemas/prometheus.py new file mode 100644 index 0000000..96f0e99 --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/schemas/prometheus.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel + + +class PrometheusQueryResult(BaseModel): + metric: Dict[str, str] + value: List[Any] + + +class PrometheusQueryData(BaseModel): + result: List[PrometheusQueryResult] + resultType: str + + +class PrometheusQueryResponse(BaseModel): + status: str + data: Optional[PrometheusQueryData] + errorType: Optional[str] + error: Optional[str] + warnings: Optional[str] + + +class PrometheusQueryRangeResult(BaseModel): + metric: Dict[str, str] + values: List[Any] + + +class PrometheusQueryRangeData(BaseModel): + result: List[PrometheusQueryRangeResult] + resultType: str + + +class PrometheusQueryRangeResponse(BaseModel): + status: str + data: Optional[PrometheusQueryRangeData] + errorType: Optional[str] + error: Optional[str] + warnings: Optional[str] diff --git a/libs/skyline-apiserver/skyline_apiserver/types/constants.py b/libs/skyline-apiserver/skyline_apiserver/types/constants.py index 3964651..ac08408 100644 --- a/libs/skyline-apiserver/skyline_apiserver/types/constants.py +++ b/libs/skyline-apiserver/skyline_apiserver/types/constants.py @@ -29,6 +29,10 @@ ERR_MSG_TOKEN_REVOKED = "The token has revoked." ERR_MSG_TOKEN_EXPIRED = "The token has expired." ERR_MSG_TOKEN_NOTFOUND = "Token not found." +# prometheus +PROMETHEUS_QUERY_API = "/api/v1/query" +PROMETHEUS_QUERY_RANGE_API = "/api/v1/query_range" + # RESTful API # neutron NEUTRON_PORTS_API = "/v2.0/ports"