add API docs examples

This commit is contained in:
checktheroads 2020-02-03 02:34:50 -07:00
parent 0ca5bc0ff6
commit 7ff8fa317d
16 changed files with 273 additions and 187 deletions

View file

@ -1,46 +1,63 @@
"""hyperglass REST API & Web UI."""
# Standard Library Imports
# Standard Library
from typing import List
from pathlib import Path
# Third Party Imports
# Third Party
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.utils import get_openapi
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import UJSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.openapi.utils import get_openapi
from starlette.staticfiles import StaticFiles
from starlette.middleware.cors import CORSMiddleware
# Project Imports
from hyperglass.api.error_handlers import app_handler
from hyperglass.api.error_handlers import default_handler
from hyperglass.api.error_handlers import http_handler
from hyperglass.api.error_handlers import validation_handler
from hyperglass.api.events import on_shutdown
from hyperglass.api.events import on_startup
from hyperglass.api.models.response import QueryResponse
from hyperglass.api.models.response import RoutersResponse
from hyperglass.api.models.response import SupportedQueryResponse
from hyperglass.api.routes import docs
from hyperglass.api.routes import queries
from hyperglass.api.routes import query
from hyperglass.api.routes import routers
from hyperglass.configuration import URL_DEV
from hyperglass.configuration import params
from hyperglass.constants import __version__
from hyperglass.exceptions import HyperglassError
# Project
from hyperglass.util import log
from hyperglass.constants import __version__
from hyperglass.api.events import on_startup, on_shutdown
from hyperglass.api.routes import docs, query, queries, routers
from hyperglass.exceptions import HyperglassError
from hyperglass.configuration import URL_DEV, params
from hyperglass.api.error_handlers import (
app_handler,
http_handler,
default_handler,
validation_handler,
)
from hyperglass.api.models.response import (
QueryError,
QueryResponse,
RoutersResponse,
SupportedQueryResponse,
)
STATIC_DIR = Path(__file__).parent.parent / "static"
WORKING_DIR = Path(__file__).parent
STATIC_DIR = WORKING_DIR.parent / "static"
UI_DIR = STATIC_DIR / "ui"
IMAGES_DIR = STATIC_DIR / "images"
EXAMPLES_DIR = WORKING_DIR / "examples"
EXAMPLE_DEVICES_PY = EXAMPLES_DIR / "devices.py"
EXAMPLE_QUERIES_PY = EXAMPLES_DIR / "queries.py"
EXAMPLE_QUERY_PY = EXAMPLES_DIR / "query.py"
EXAMPLE_DEVICES_CURL = EXAMPLES_DIR / "devices.sh"
EXAMPLE_QUERIES_CURL = EXAMPLES_DIR / "queries.sh"
EXAMPLE_QUERY_CURL = EXAMPLES_DIR / "query.sh"
ASGI_PARAMS = {
"host": str(params.listen_address),
"port": params.listen_port,
"debug": params.debug,
}
DOCS_PARAMS = {}
if params.docs.enable:
DOCS_PARAMS.update({"openapi_url": params.docs.openapi_uri})
if params.docs.mode == "redoc":
DOCS_PARAMS.update({"docs_url": None, "redoc_url": params.docs.uri})
elif params.docs.mode == "swagger":
DOCS_PARAMS.update({"docs_url": params.docs.uri, "redoc_url": None})
# Main App Definition
app = FastAPI(
@ -49,9 +66,7 @@ app = FastAPI(
description=params.site_description,
version=__version__,
default_response_class=UJSONResponse,
docs_url=None,
redoc_url=None,
openapi_url=params.docs.openapi_uri,
**DOCS_PARAMS,
)
# Add Event Handlers
@ -77,20 +92,60 @@ app.add_exception_handler(Exception, default_handler)
def _custom_openapi():
"""Generate custom OpenAPI config."""
openapi_schema = get_openapi(
title=params.site_title,
title=params.docs.title.format(site_title=params.site_title),
version=__version__,
description=params.site_description,
description=params.docs.description,
routes=app.routes,
)
openapi_schema["info"]["x-logo"] = {"url": str(params.web.logo.light)}
query_samples = []
queries_samples = []
devices_samples = []
with EXAMPLE_QUERY_CURL.open("r") as e:
example = e.read()
query_samples.append(
{"lang": "cURL", "source": example % str(params.docs.base_url)}
)
with EXAMPLE_QUERY_PY.open("r") as e:
example = e.read()
query_samples.append(
{"lang": "Python", "source": example % str(params.docs.base_url)}
)
with EXAMPLE_DEVICES_CURL.open("r") as e:
example = e.read()
queries_samples.append(
{"lang": "cURL", "source": example % str(params.docs.base_url)}
)
with EXAMPLE_DEVICES_PY.open("r") as e:
example = e.read()
queries_samples.append(
{"lang": "Python", "source": example % str(params.docs.base_url)}
)
with EXAMPLE_QUERIES_CURL.open("r") as e:
example = e.read()
devices_samples.append(
{"lang": "cURL", "source": example % str(params.docs.base_url)}
)
with EXAMPLE_QUERIES_PY.open("r") as e:
example = e.read()
devices_samples.append(
{"lang": "Python", "source": example % str(params.docs.base_url)}
)
openapi_schema["paths"]["/api/query/"]["post"]["x-code-samples"] = query_samples
openapi_schema["paths"]["/api/devices"]["get"]["x-code-samples"] = devices_samples
openapi_schema["paths"]["/api/queries"]["get"]["x-code-samples"] = queries_samples
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = _custom_openapi
if params.docs.enable:
log.debug(f"API Docs config: {app.openapi()}")
CORS_ORIGINS = params.cors_origins.copy()
if params.developer_mode:
CORS_ORIGINS.append(URL_DEV)
@ -107,7 +162,7 @@ app.add_api_route(
path="/api/devices",
endpoint=routers,
methods=["GET"],
response_model=RoutersResponse,
response_model=List[RoutersResponse],
response_class=UJSONResponse,
summary=params.docs.devices.summary,
description=params.docs.devices.description,
@ -118,7 +173,7 @@ app.add_api_route(
endpoint=queries,
methods=["GET"],
response_class=UJSONResponse,
response_model=SupportedQueryResponse,
response_model=List[SupportedQueryResponse],
summary=params.docs.queries.summary,
description=params.docs.queries.description,
tags=[params.docs.queries.title],
@ -129,6 +184,11 @@ app.add_api_route(
methods=["POST"],
summary=params.docs.query.summary,
description=params.docs.query.description,
responses={
400: {"model": QueryError, "description": "Request Content Error"},
422: {"model": QueryError, "description": "Request Format Error"},
500: {"model": QueryError, "description": "Server Error"},
},
response_model=QueryResponse,
tags=[params.docs.query.title],
response_class=UJSONResponse,
@ -136,6 +196,8 @@ app.add_api_route(
if params.docs.enable:
app.add_api_route(path=params.docs.uri, endpoint=docs, include_in_schema=False)
app.openapi = _custom_openapi
log.debug(f"API Docs config: {app.openapi()}")
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui")

View file

@ -1,9 +1,9 @@
"""API Error Handlers."""
# Third Party Imports
# Third Party
from starlette.responses import UJSONResponse
# Project Imports
# Project
from hyperglass.configuration import params
@ -36,5 +36,5 @@ async def validation_handler(request, exc):
error = exc.errors()[0]
return UJSONResponse(
{"output": error["msg"], "level": "error", "keywords": error["loc"]},
status_code=400,
status_code=422,
)

View file

@ -1,19 +1,23 @@
"""API Events."""
# Third Party Imports
# Third Party
from starlette.exceptions import HTTPException
# Project Imports
from hyperglass.configuration import REDIS_CONFIG
from hyperglass.configuration import URL_DEV
from hyperglass.configuration import URL_PROD
from hyperglass.configuration import frontend_params
from hyperglass.configuration import params
# Project
from hyperglass.util import (
log,
check_redis,
check_python,
build_frontend,
clear_redis_cache,
)
from hyperglass.exceptions import HyperglassError
from hyperglass.util import build_frontend
from hyperglass.util import check_python
from hyperglass.util import check_redis
from hyperglass.util import clear_redis_cache
from hyperglass.util import log
from hyperglass.configuration import (
URL_DEV,
URL_PROD,
REDIS_CONFIG,
params,
frontend_params,
)
async def check_python_version():

View file

@ -0,0 +1,6 @@
# Third Party
import httpx
request = httpx.get("%s/api/devices")
print(request.json())

View file

@ -0,0 +1 @@
curl %s/api/devices

View file

@ -0,0 +1,6 @@
# Third Party
import httpx
request = httpx.get("%s/api/queries")
print(request.json())

View file

@ -0,0 +1 @@
curl %s/api/queries

View file

@ -0,0 +1,13 @@
# Third Party
import httpx
query = {
"query_location": "router01",
"query_type": "bgp_route",
"query_vrf": "default",
"query_target": "1.1.1.0/24",
}
request = httpx.post("%s/api/query/", data=query)
print(request.json().get("output"))

View file

@ -0,0 +1,7 @@
curl -X POST %s/api/query/ -d \
'{
"query_location": "router01",
"query_type": "bgp_route",
"query_vrf": "default",
"query_target": "1.1.1.0/24"
}'

View file

@ -1,10 +1,6 @@
"""Query & Response Validation Models."""
# Project Imports
from hyperglass.api.models import query
from hyperglass.api.models import response
from hyperglass.api.models import rfc8522
from hyperglass.api.models import types
from hyperglass.api.models import validators
# Project
from hyperglass.api.models import query, types, rfc8522, response, validators
# flake8: noqa: F401

View file

@ -1,22 +1,21 @@
"""Input query validation model."""
# Standard Library Imports
# Standard Library
import hashlib
# Third Party Imports
from pydantic import BaseModel
from pydantic import StrictStr
from pydantic import validator
# Third Party
from pydantic import BaseModel, StrictStr, validator
# Project Imports
from hyperglass.api.models.types import SupportedQuery
from hyperglass.api.models.validators import validate_aspath
from hyperglass.api.models.validators import validate_community
from hyperglass.api.models.validators import validate_ip
from hyperglass.configuration import devices
from hyperglass.configuration import params
from hyperglass.configuration.models.vrfs import Vrf
# Project
from hyperglass.util import log
from hyperglass.exceptions import InputInvalid
from hyperglass.configuration import params, devices
from hyperglass.api.models.types import SupportedQuery
from hyperglass.api.models.validators import (
validate_ip,
validate_aspath,
validate_community,
)
def get_vrf_object(vrf_name):
@ -51,14 +50,43 @@ class Query(BaseModel):
query_location: StrictStr
query_type: SupportedQuery
query_vrf: Vrf
query_vrf: StrictStr
query_target: StrictStr
class Config:
"""Pydantic model configuration."""
fields = {
"query_location": {
"title": params.web.text.query_location,
"description": "Router/Location Name",
"example": "router01",
},
"query_type": {
"title": params.web.text.query_type,
"description": "Type of Query to Execute",
"example": "bgp_route",
},
"query_vrf": {
"title": params.web.text.query_vrf,
"description": "Routing Table/VRF",
"example": "default",
},
"query_target": {
"title": params.web.text.query_target,
"description": "IP Address, Community, or AS Path",
"example": "1.1.1.0/24",
},
}
schema_extra = {
"x-code-samples": [{"lang": "Python", "source": "print('stuff')"}]
}
def digest(self):
"""Create SHA256 hash digest of model representation."""
return hashlib.sha256(repr(self).encode()).hexdigest()
@validator("query_location", pre=True, always=True)
@validator("query_location")
def validate_query_location(cls, value):
"""Ensure query_location is defined.
@ -80,7 +108,7 @@ class Query(BaseModel):
)
return value
@validator("query_vrf", always=True, pre=True)
@validator("query_vrf")
def validate_query_vrf(cls, value, values):
"""Ensure query_vrf is defined.
@ -108,10 +136,11 @@ class Query(BaseModel):
)
return device_vrf
@validator("query_target", always=True)
@validator("query_target")
def validate_query_target(cls, value, values):
"""Validate query target value based on query_type."""
log.debug(values)
query_type = values["query_type"]
# Use relevant function based on query_type.

View file

@ -1,20 +1,20 @@
"""Response model."""
# Standard Library Imports
# Standard Library
from typing import List
# Third Party Imports
from pydantic import BaseModel
from pydantic import StrictBool
from pydantic import StrictStr
from pydantic import constr
# Third Party
from pydantic import BaseModel, StrictStr, StrictBool, constr
# Project
from hyperglass.configuration import params
class QueryError(BaseModel):
"""Query response model."""
output: StrictStr
level: constr(regex=r"(success|warning|error|danger)")
keywords: List[StrictStr]
output: StrictStr = params.messages.general
level: constr(regex=r"(success|warning|error|danger)") = "danger"
keywords: List[StrictStr] = []
class Config:
"""Pydantic model configuration."""
@ -23,6 +23,23 @@ class QueryError(BaseModel):
description = (
"Response received when there is an error executing the requested query."
)
fields = {
"output": {
"title": "Output",
"description": "Error Details",
"example": "192.0.2.1/32 is not allowed.",
},
"level": {
"title": "Level",
"description": "Error Severity",
"example": "danger",
},
"keywords": {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["192.0.2.1/32"],
},
}
schema_extra = {
"examples": [
{
@ -38,7 +55,7 @@ class QueryResponse(BaseModel):
"""Query response model."""
output: StrictStr
level: constr(regex=r"(success|warning|error|danger)")
level: constr(regex=r"success") = "success"
keywords: List[StrictStr] = []
class Config:
@ -46,66 +63,45 @@ class QueryResponse(BaseModel):
title = "Query Response"
description = "Looking glass response"
fields = {
"level": {"title": "Level", "description": "Severity"},
"keywords": {
"title": "Keywords",
"description": "Relevant keyword values contained in the `output` field, which can be used for formatting.",
"example": ["1.1.1.0/24", "best #1"],
},
"output": {
"title": "Output",
"description": "Looking Glass Response",
"example": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 65000:1 65000:2
""",
},
}
schema_extra = {
"examples": [
{
"output": """
BGP routing table entry for 1.1.1.0/24, version 224184946
BGP Bestpath: deterministic-med
Paths: (12 available, best #9, table default)
Paths: (12 available, best #1, table default)
Advertised to update-groups:
1 40
13335, (aggregated by 13335 172.68.129.1), (received & used)
199.34.92.5 (metric 51) from 199.34.92.5 (199.34.92.5)
192.0.2.1 (metric 51) from 192.0.2.1 (192.0.2.1)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 14525:1021 14525:2840 14525:3003 14525:4003 14525:5200 14525:5300 14525:5306
13335, (aggregated by 13335 172.68.129.1), (received & used)
199.34.92.6 (metric 51) from 199.34.92.6 (199.34.92.6)
Origin IGP, metric 0, localpref 250, valid, internal
Community: 14525:1021 14525:2840 14525:3003 14525:4003 14525:5200 14525:5300 14525:5306
1299 13335, (aggregated by 13335 162.158.140.1)
62.115.171.124 from 62.115.171.124 (2.255.254.51)
Origin IGP, metric 0, localpref 100, weight 200, valid, external
Community: 14525:0 14525:1021 14525:2840 14525:3001 14525:4001 14525:5100 14525:5103
1299 13335, (aggregated by 13335 162.158.140.1), (received-only)
62.115.171.124 from 62.115.171.124 (2.255.254.51)
Origin IGP, localpref 100, valid, external
Community: 1299:35000
174 13335, (aggregated by 13335 108.162.239.1)
199.34.92.7 (metric 1100) from 199.34.92.7 (199.34.92.7)
Origin IGP, metric 0, localpref 100, weight 125, valid, internal
Community: 14525:0 14525:840 14525:1021 14525:3004 14525:4004 14525:5100 14525:5101
174 13335, (aggregated by 13335 108.162.239.1), (received-only)
199.34.92.7 (metric 1100) from 199.34.92.7 (199.34.92.7)
Origin IGP, metric 0, localpref 100, valid, internal
Community: 14525:0 14525:840 14525:1021 14525:3004 14525:4004 14525:5100 14525:5101
174 13335, (aggregated by 13335 162.158.140.1), (Received from a RR-client)
199.34.92.2 (metric 26) from 199.34.92.2 (199.34.92.2)
Origin IGP, metric 0, localpref 100, weight 200, valid, internal
Community: 14525:0 14525:1021 14525:2840 14525:3001 14525:4001 14525:5100 14525:5101
174 13335, (aggregated by 13335 162.158.140.1), (Received from a RR-client), (received-only)
199.34.92.2 (metric 26) from 199.34.92.2 (199.34.92.2)
Origin IGP, metric 0, localpref 100, valid, internal
Community: 14525:0 14525:1021 14525:2840 14525:3001 14525:4001 14525:5100 14525:5101
174 13335, (aggregated by 13335 162.158.140.1)
38.140.141.25 from 38.140.141.25 (154.26.6.194)
Origin IGP, metric 0, localpref 100, weight 200, valid, external, best
Community: 14525:0 14525:1021 14525:2840 14525:3001 14525:4001 14525:5100 14525:5101
174 13335, (aggregated by 13335 162.158.140.1), (received-only)
38.140.141.25 from 38.140.141.25 (154.26.6.194)
Origin IGP, metric 2020, localpref 100, valid, external
Community: 174:21001 174:22013
3257 13335, (aggregated by 13335 141.101.72.1)
199.34.92.3 (metric 200) from 199.34.92.3 (199.34.92.3)
Origin IGP, metric 0, localpref 100, weight 200, valid, internal
Community: 14525:0 14525:840 14525:1021 14525:3002 14525:4002 14525:5100 14525:5104
3257 13335, (aggregated by 13335 141.101.72.1), (received-only)
199.34.92.3 (metric 200) from 199.34.92.3 (199.34.92.3)
Origin IGP, metric 0, localpref 100, valid, internal
Community: 14525:0 14525:840 14525:1021 14525:3002 14525:4002 14525:5100 14525:5104
Community: 65000:1 65000:2
""",
"level": "success",
"keywords": ["1.1.1.0/24", "best #9"],
"keywords": ["1.1.1.0/24", "best #1"],
}
]
}
@ -144,7 +140,7 @@ class Network(BaseModel):
schema_extra = {"examples": [{"name": "primary", "display_name": "AS65000"}]}
class Router(BaseModel):
class RoutersResponse(BaseModel):
"""Response model for /api/devices list items."""
name: StrictStr
@ -169,19 +165,7 @@ class Router(BaseModel):
}
class RoutersResponse(BaseModel):
"""Response model for /api/devices endpoint."""
__root__: List[Router]
class Config:
"""Pydantic model configuration."""
title = "Devices"
description = "List of all devices"
class SupportedQuery(BaseModel):
class SupportedQueryResponse(BaseModel):
"""Response model for /api/queries list items."""
name: StrictStr
@ -198,15 +182,3 @@ class SupportedQuery(BaseModel):
{"name": "bgp_route", "display_name": "BGP Route", "enable": True}
]
}
class SupportedQueryResponse(BaseModel):
"""Response model for /api/queries endpoint."""
__root__: List[SupportedQuery]
class Config:
"""Pydantic model configuration."""
title = "Supported Query Types"
description = "Enabled query type attributes."

View file

@ -1,22 +1,15 @@
"""Response model."""
# Standard Library Imports
# Standard Library
# flake8: noqa
import math
import secrets
from typing import List, Union, Optional
from datetime import datetime
from typing import List
from typing import Optional
from typing import Union
# Third Party Imports
# Third Party
import ujson
from pydantic import BaseModel
from pydantic import StrictFloat
from pydantic import StrictInt
from pydantic import StrictStr
from pydantic import constr
from pydantic import validator
from pydantic import BaseModel, StrictInt, StrictStr, StrictFloat, constr, validator
"""Patterns:

View file

@ -1,6 +1,6 @@
"""Custom validation types."""
# Project Imports
# Project
from hyperglass.constants import SUPPORTED_QUERY_TYPES

View file

@ -1,15 +1,14 @@
"""Input validation functions for submitted queries."""
# Standard Library Imports
import operator
# Standard Library
import re
import operator
from ipaddress import ip_network
# Project Imports
from hyperglass.configuration import params
from hyperglass.exceptions import InputInvalid
from hyperglass.exceptions import InputNotAllowed
# Project
from hyperglass.util import log
from hyperglass.exceptions import InputInvalid, InputNotAllowed
from hyperglass.configuration import params
def _member_of(target, network):

View file

@ -1,23 +1,20 @@
"""API Routes."""
# Standard Library Imports
# Standard Library
import time
# Third Party Imports
# Third Party
import aredis
from fastapi import HTTPException
from fastapi.openapi.docs import get_redoc_html
from fastapi.openapi.docs import get_swagger_ui_html
from starlette.requests import Request
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
# Project Imports
from hyperglass.api.models.query import Query
from hyperglass.configuration import REDIS_CONFIG
from hyperglass.configuration import devices
from hyperglass.configuration import params
from hyperglass.exceptions import HyperglassError
from hyperglass.execution.execute import Execute
# Project
from hyperglass.util import log
from hyperglass.exceptions import HyperglassError
from hyperglass.configuration import REDIS_CONFIG, params, devices
from hyperglass.api.models.query import Query
from hyperglass.execution.execute import Execute
Cache = aredis.StrictRedis(db=params.cache.database, **REDIS_CONFIG)