forked from mirrors/thatmattlove-hyperglass
Implement global state
This commit is contained in:
parent
b002c9d520
commit
a2ee4b50fa
29 changed files with 702 additions and 345 deletions
2
.flake8
2
.flake8
|
|
@ -18,7 +18,7 @@ per-file-ignores=
|
||||||
hyperglass/models/*/__init__.py:F401
|
hyperglass/models/*/__init__.py:F401
|
||||||
# Disable assertion and docstring checks on tests.
|
# Disable assertion and docstring checks on tests.
|
||||||
hyperglass/**/test_*.py:S101,D103
|
hyperglass/**/test_*.py:S101,D103
|
||||||
ignore=W503,C0330,R504,D202,S403,S301,S404,E731
|
ignore=W503,C0330,R504,D202,S403,S301,S404,E731,D402
|
||||||
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
|
select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W
|
||||||
disable-noqa=False
|
disable-noqa=False
|
||||||
hang-closing=False
|
hang-closing=False
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,9 @@ from hyperglass.constants import __version__
|
||||||
from hyperglass.models.ui import UIParameters
|
from hyperglass.models.ui import UIParameters
|
||||||
from hyperglass.api.events import on_startup, on_shutdown
|
from hyperglass.api.events import on_startup, on_shutdown
|
||||||
from hyperglass.api.routes import docs, info, query, router, queries, routers, ui_props
|
from hyperglass.api.routes import docs, info, query, router, queries, routers, ui_props
|
||||||
|
from hyperglass.state import use_state
|
||||||
from hyperglass.exceptions import HyperglassError
|
from hyperglass.exceptions import HyperglassError
|
||||||
from hyperglass.configuration import URL_DEV, STATIC_PATH, params
|
from hyperglass.configuration import URL_DEV, STATIC_PATH
|
||||||
from hyperglass.api.error_handlers import (
|
from hyperglass.api.error_handlers import (
|
||||||
app_handler,
|
app_handler,
|
||||||
http_handler,
|
http_handler,
|
||||||
|
|
@ -39,6 +40,8 @@ from hyperglass.models.api.response import (
|
||||||
SupportedQueryResponse,
|
SupportedQueryResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STATE = use_state()
|
||||||
|
|
||||||
WORKING_DIR = Path(__file__).parent
|
WORKING_DIR = Path(__file__).parent
|
||||||
EXAMPLES_DIR = WORKING_DIR / "examples"
|
EXAMPLES_DIR = WORKING_DIR / "examples"
|
||||||
|
|
||||||
|
|
@ -54,18 +57,18 @@ EXAMPLE_QUERIES_CURL = EXAMPLES_DIR / "queries.sh"
|
||||||
EXAMPLE_QUERY_CURL = EXAMPLES_DIR / "query.sh"
|
EXAMPLE_QUERY_CURL = EXAMPLES_DIR / "query.sh"
|
||||||
|
|
||||||
ASGI_PARAMS = {
|
ASGI_PARAMS = {
|
||||||
"host": str(params.listen_address),
|
"host": str(STATE.settings.host),
|
||||||
"port": params.listen_port,
|
"port": STATE.settings.port,
|
||||||
"debug": params.debug,
|
"debug": STATE.settings.debug,
|
||||||
"workers": cpu_count(2),
|
"workers": cpu_count(2),
|
||||||
}
|
}
|
||||||
DOCS_PARAMS = {}
|
DOCS_PARAMS = {}
|
||||||
if params.docs.enable:
|
if STATE.params.docs.enable:
|
||||||
DOCS_PARAMS.update({"openapi_url": params.docs.openapi_uri})
|
DOCS_PARAMS.update({"openapi_url": STATE.params.docs.openapi_uri})
|
||||||
if params.docs.mode == "redoc":
|
if STATE.params.docs.mode == "redoc":
|
||||||
DOCS_PARAMS.update({"docs_url": None, "redoc_url": params.docs.uri})
|
DOCS_PARAMS.update({"docs_url": None, "redoc_url": STATE.params.docs.uri})
|
||||||
elif params.docs.mode == "swagger":
|
elif STATE.params.docs.mode == "swagger":
|
||||||
DOCS_PARAMS.update({"docs_url": params.docs.uri, "redoc_url": None})
|
DOCS_PARAMS.update({"docs_url": STATE.params.docs.uri, "redoc_url": None})
|
||||||
|
|
||||||
for directory in (UI_DIR, IMAGES_DIR):
|
for directory in (UI_DIR, IMAGES_DIR):
|
||||||
if not directory.exists():
|
if not directory.exists():
|
||||||
|
|
@ -74,9 +77,9 @@ for directory in (UI_DIR, IMAGES_DIR):
|
||||||
|
|
||||||
# Main App Definition
|
# Main App Definition
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=params.debug,
|
debug=STATE.settings.debug,
|
||||||
title=params.site_title,
|
title=STATE.params.site_title,
|
||||||
description=params.site_description,
|
description=STATE.params.site_description,
|
||||||
version=__version__,
|
version=__version__,
|
||||||
default_response_class=JSONResponse,
|
default_response_class=JSONResponse,
|
||||||
**DOCS_PARAMS,
|
**DOCS_PARAMS,
|
||||||
|
|
@ -108,12 +111,12 @@ app.add_exception_handler(Exception, default_handler)
|
||||||
def _custom_openapi():
|
def _custom_openapi():
|
||||||
"""Generate custom OpenAPI config."""
|
"""Generate custom OpenAPI config."""
|
||||||
openapi_schema = get_openapi(
|
openapi_schema = get_openapi(
|
||||||
title=params.docs.title.format(site_title=params.site_title),
|
title=STATE.params.docs.title.format(site_title=STATE.params.site_title),
|
||||||
version=__version__,
|
version=__version__,
|
||||||
description=params.docs.description,
|
description=STATE.params.docs.description,
|
||||||
routes=app.routes,
|
routes=app.routes,
|
||||||
)
|
)
|
||||||
openapi_schema["info"]["x-logo"] = {"url": "/images/light" + params.web.logo.light.suffix}
|
openapi_schema["info"]["x-logo"] = {"url": "/images/light" + STATE.params.web.logo.light.suffix}
|
||||||
|
|
||||||
query_samples = []
|
query_samples = []
|
||||||
queries_samples = []
|
queries_samples = []
|
||||||
|
|
@ -121,26 +124,36 @@ def _custom_openapi():
|
||||||
|
|
||||||
with EXAMPLE_QUERY_CURL.open("r") as e:
|
with EXAMPLE_QUERY_CURL.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
query_samples.append({"lang": "cURL", "source": example % str(params.docs.base_url)})
|
query_samples.append({"lang": "cURL", "source": example % str(STATE.params.docs.base_url)})
|
||||||
|
|
||||||
with EXAMPLE_QUERY_PY.open("r") as e:
|
with EXAMPLE_QUERY_PY.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
query_samples.append({"lang": "Python", "source": example % str(params.docs.base_url)})
|
query_samples.append(
|
||||||
|
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
||||||
|
)
|
||||||
|
|
||||||
with EXAMPLE_DEVICES_CURL.open("r") as e:
|
with EXAMPLE_DEVICES_CURL.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
queries_samples.append({"lang": "cURL", "source": example % str(params.docs.base_url)})
|
queries_samples.append(
|
||||||
|
{"lang": "cURL", "source": example % str(STATE.params.docs.base_url)}
|
||||||
|
)
|
||||||
with EXAMPLE_DEVICES_PY.open("r") as e:
|
with EXAMPLE_DEVICES_PY.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
queries_samples.append({"lang": "Python", "source": example % str(params.docs.base_url)})
|
queries_samples.append(
|
||||||
|
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
||||||
|
)
|
||||||
|
|
||||||
with EXAMPLE_QUERIES_CURL.open("r") as e:
|
with EXAMPLE_QUERIES_CURL.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
devices_samples.append({"lang": "cURL", "source": example % str(params.docs.base_url)})
|
devices_samples.append(
|
||||||
|
{"lang": "cURL", "source": example % str(STATE.params.docs.base_url)}
|
||||||
|
)
|
||||||
|
|
||||||
with EXAMPLE_QUERIES_PY.open("r") as e:
|
with EXAMPLE_QUERIES_PY.open("r") as e:
|
||||||
example = e.read()
|
example = e.read()
|
||||||
devices_samples.append({"lang": "Python", "source": example % str(params.docs.base_url)})
|
devices_samples.append(
|
||||||
|
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
||||||
|
)
|
||||||
|
|
||||||
openapi_schema["paths"]["/api/query/"]["post"]["x-code-samples"] = query_samples
|
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/devices"]["get"]["x-code-samples"] = devices_samples
|
||||||
|
|
@ -150,8 +163,8 @@ def _custom_openapi():
|
||||||
return app.openapi_schema
|
return app.openapi_schema
|
||||||
|
|
||||||
|
|
||||||
CORS_ORIGINS = params.cors_origins.copy()
|
CORS_ORIGINS = STATE.params.cors_origins.copy()
|
||||||
if params.developer_mode:
|
if STATE.settings.dev_mode:
|
||||||
CORS_ORIGINS = [*CORS_ORIGINS, URL_DEV, "http://localhost:3000"]
|
CORS_ORIGINS = [*CORS_ORIGINS, URL_DEV, "http://localhost:3000"]
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
|
|
@ -171,9 +184,9 @@ app.add_api_route(
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
response_model=InfoResponse,
|
response_model=InfoResponse,
|
||||||
response_class=JSONResponse,
|
response_class=JSONResponse,
|
||||||
summary=params.docs.info.summary,
|
summary=STATE.params.docs.info.summary,
|
||||||
description=params.docs.info.description,
|
description=STATE.params.docs.info.description,
|
||||||
tags=[params.docs.info.title],
|
tags=[STATE.params.docs.info.title],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
|
|
@ -182,9 +195,9 @@ app.add_api_route(
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
response_model=List[RoutersResponse],
|
response_model=List[RoutersResponse],
|
||||||
response_class=JSONResponse,
|
response_class=JSONResponse,
|
||||||
summary=params.docs.devices.summary,
|
summary=STATE.params.docs.devices.summary,
|
||||||
description=params.docs.devices.description,
|
description=STATE.params.docs.devices.description,
|
||||||
tags=[params.docs.devices.title],
|
tags=[STATE.params.docs.devices.title],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
|
|
@ -193,9 +206,9 @@ app.add_api_route(
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
response_model=RoutersResponse,
|
response_model=RoutersResponse,
|
||||||
response_class=JSONResponse,
|
response_class=JSONResponse,
|
||||||
summary=params.docs.devices.summary,
|
summary=STATE.params.docs.devices.summary,
|
||||||
description=params.docs.devices.description,
|
description=STATE.params.docs.devices.description,
|
||||||
tags=[params.docs.devices.title],
|
tags=[STATE.params.docs.devices.title],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
|
|
@ -204,24 +217,24 @@ app.add_api_route(
|
||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
response_class=JSONResponse,
|
response_class=JSONResponse,
|
||||||
response_model=List[SupportedQueryResponse],
|
response_model=List[SupportedQueryResponse],
|
||||||
summary=params.docs.queries.summary,
|
summary=STATE.params.docs.queries.summary,
|
||||||
description=params.docs.queries.description,
|
description=STATE.params.docs.queries.description,
|
||||||
tags=[params.docs.queries.title],
|
tags=[STATE.params.docs.queries.title],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
app.add_api_route(
|
||||||
path="/api/query/",
|
path="/api/query/",
|
||||||
endpoint=query,
|
endpoint=query,
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
summary=params.docs.query.summary,
|
summary=STATE.params.docs.query.summary,
|
||||||
description=params.docs.query.description,
|
description=STATE.params.docs.query.description,
|
||||||
responses={
|
responses={
|
||||||
400: {"model": QueryError, "description": "Request Content Error"},
|
400: {"model": QueryError, "description": "Request Content Error"},
|
||||||
422: {"model": QueryError, "description": "Request Format Error"},
|
422: {"model": QueryError, "description": "Request Format Error"},
|
||||||
500: {"model": QueryError, "description": "Server Error"},
|
500: {"model": QueryError, "description": "Server Error"},
|
||||||
},
|
},
|
||||||
response_model=QueryResponse,
|
response_model=QueryResponse,
|
||||||
tags=[params.docs.query.title],
|
tags=[STATE.params.docs.query.title],
|
||||||
response_class=JSONResponse,
|
response_class=JSONResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -235,8 +248,8 @@ app.add_api_route(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if params.docs.enable:
|
if STATE.params.docs.enable:
|
||||||
app.add_api_route(path=params.docs.uri, endpoint=docs, include_in_schema=False)
|
app.add_api_route(path=STATE.params.docs.uri, endpoint=docs, include_in_schema=False)
|
||||||
app.openapi = _custom_openapi
|
app.openapi = _custom_openapi
|
||||||
log.debug("API Docs config: {}", app.openapi())
|
log.debug("API Docs config: {}", app.openapi())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,21 @@
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.configuration import params
|
from hyperglass.state import use_state
|
||||||
|
|
||||||
|
|
||||||
async def default_handler(request, exc):
|
async def default_handler(request, exc):
|
||||||
"""Handle uncaught errors."""
|
"""Handle uncaught errors."""
|
||||||
|
state = use_state()
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"output": params.messages.general, "level": "danger", "keywords": []}, status_code=500,
|
{"output": state.params.messages.general, "level": "danger", "keywords": []},
|
||||||
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def http_handler(request, exc):
|
async def http_handler(request, exc):
|
||||||
"""Handle web server errors."""
|
"""Handle web server errors."""
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"output": exc.detail, "level": "danger", "keywords": []}, status_code=exc.status_code,
|
{"output": exc.detail, "level": "danger", "keywords": []}, status_code=exc.status_code,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
"""API Events."""
|
"""API Events."""
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cache import AsyncCache
|
from hyperglass.state import use_state
|
||||||
from hyperglass.configuration import REDIS_CONFIG, params
|
|
||||||
|
|
||||||
|
|
||||||
async def check_redis() -> bool:
|
def check_redis() -> bool:
|
||||||
"""Ensure Redis is running before starting server."""
|
"""Ensure Redis is running before starting server."""
|
||||||
cache = AsyncCache(db=params.cache.database, **REDIS_CONFIG)
|
state = use_state()
|
||||||
await cache.test()
|
return state._redis.ping()
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
on_startup = (check_redis,)
|
on_startup = (check_redis,)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"""API Routes."""
|
"""API Routes."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
|
|
@ -13,25 +13,28 @@ from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.cache import AsyncCache
|
from hyperglass.state import use_state
|
||||||
from hyperglass.external import Webhook, bgptools
|
from hyperglass.external import Webhook, bgptools
|
||||||
from hyperglass.api.tasks import process_headers
|
from hyperglass.api.tasks import process_headers
|
||||||
from hyperglass.constants import __version__
|
from hyperglass.constants import __version__
|
||||||
from hyperglass.exceptions import HyperglassError
|
from hyperglass.exceptions import HyperglassError
|
||||||
from hyperglass.models.api import Query
|
|
||||||
from hyperglass.configuration import REDIS_CONFIG, params, devices, ui_params
|
|
||||||
from hyperglass.execution.main import execute
|
from hyperglass.execution.main import execute
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .fake_output import fake_output
|
from .fake_output import fake_output
|
||||||
|
|
||||||
APP_PATH = os.environ["hyperglass_directory"]
|
if t.TYPE_CHECKING:
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.api import Query
|
||||||
|
|
||||||
|
|
||||||
async def send_webhook(query_data: Query, request: Request, timestamp: datetime):
|
STATE = use_state()
|
||||||
|
|
||||||
|
|
||||||
|
async def send_webhook(query_data: "Query", request: Request, timestamp: datetime):
|
||||||
"""If webhooks are enabled, get request info and send a webhook."""
|
"""If webhooks are enabled, get request info and send a webhook."""
|
||||||
try:
|
try:
|
||||||
if params.logging.http is not None:
|
if STATE.params.logging.http is not None:
|
||||||
headers = await process_headers(headers=request.headers)
|
headers = await process_headers(headers=request.headers)
|
||||||
|
|
||||||
if headers.get("x-real-ip") is not None:
|
if headers.get("x-real-ip") is not None:
|
||||||
|
|
@ -43,7 +46,7 @@ async def send_webhook(query_data: Query, request: Request, timestamp: datetime)
|
||||||
|
|
||||||
network_info = await bgptools.network_info(host)
|
network_info = await bgptools.network_info(host)
|
||||||
|
|
||||||
async with Webhook(params.logging.http) as hook:
|
async with Webhook(STATE.params.logging.http) as hook:
|
||||||
|
|
||||||
await hook.send(
|
await hook.send(
|
||||||
query={
|
query={
|
||||||
|
|
@ -55,30 +58,30 @@ async def send_webhook(query_data: Query, request: Request, timestamp: datetime)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.error("Error sending webhook to {}: {}", params.logging.http.provider, str(err))
|
log.error("Error sending webhook to {}: {}", STATE.params.logging.http.provider, str(err))
|
||||||
|
|
||||||
|
|
||||||
async def query(query_data: Query, request: Request, background_tasks: BackgroundTasks):
|
async def query(query_data: "Query", request: Request, background_tasks: BackgroundTasks):
|
||||||
"""Ingest request data pass it to the backend application to perform the query."""
|
"""Ingest request data pass it to the backend application to perform the query."""
|
||||||
|
|
||||||
timestamp = datetime.utcnow()
|
timestamp = datetime.utcnow()
|
||||||
background_tasks.add_task(send_webhook, query_data, request, timestamp)
|
background_tasks.add_task(send_webhook, query_data, request, timestamp)
|
||||||
|
|
||||||
# Initialize cache
|
# Initialize cache
|
||||||
cache = AsyncCache(db=params.cache.database, **REDIS_CONFIG)
|
cache = STATE.redis
|
||||||
log.debug("Initialized cache {}", repr(cache))
|
log.debug("Initialized cache {}", repr(cache))
|
||||||
|
|
||||||
# Use hashed query_data string as key for for k/v cache store so
|
# Use hashed query_data string as key for for k/v cache store so
|
||||||
# each command output value is unique.
|
# each command output value is unique.
|
||||||
cache_key = query_data.digest()
|
cache_key = f"hyperglass.query.{query_data.digest()}"
|
||||||
|
|
||||||
# Define cache entry expiry time
|
# Define cache entry expiry time
|
||||||
cache_timeout = params.cache.timeout
|
cache_timeout = STATE.params.cache.timeout
|
||||||
|
|
||||||
log.debug("Cache Timeout: {}", cache_timeout)
|
log.debug("Cache Timeout: {}", cache_timeout)
|
||||||
log.info("Starting query execution for query {}", query_data.summary)
|
log.info("Starting query execution for query {}", query_data.summary)
|
||||||
|
|
||||||
cache_response = await cache.get_dict(cache_key, "output")
|
cache_response = cache.get_dict(cache_key, "output")
|
||||||
|
|
||||||
json_output = False
|
json_output = False
|
||||||
|
|
||||||
|
|
@ -95,11 +98,11 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
|
||||||
log.debug("Query {} exists in cache", cache_key)
|
log.debug("Query {} exists in cache", cache_key)
|
||||||
|
|
||||||
# If a cached response exists, reset the expiration time.
|
# If a cached response exists, reset the expiration time.
|
||||||
await cache.expire(cache_key, seconds=cache_timeout)
|
cache.expire(cache_key, seconds=cache_timeout)
|
||||||
|
|
||||||
cached = True
|
cached = True
|
||||||
runtime = 0
|
runtime = 0
|
||||||
timestamp = await cache.get_dict(cache_key, "timestamp")
|
timestamp = cache.get_dict(cache_key, "timestamp")
|
||||||
|
|
||||||
elif not cache_response:
|
elif not cache_response:
|
||||||
log.debug("No existing cache entry for query {}", cache_key)
|
log.debug("No existing cache entry for query {}", cache_key)
|
||||||
|
|
@ -109,7 +112,7 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
|
||||||
|
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
|
|
||||||
if params.fake_output:
|
if STATE.params.fake_output:
|
||||||
# Return fake, static data for development purposes, if enabled.
|
# Return fake, static data for development purposes, if enabled.
|
||||||
cache_output = await fake_output(json_output)
|
cache_output = await fake_output(json_output)
|
||||||
else:
|
else:
|
||||||
|
|
@ -121,23 +124,23 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
|
||||||
log.debug("Query {} took {} seconds to run.", cache_key, elapsedtime)
|
log.debug("Query {} took {} seconds to run.", cache_key, elapsedtime)
|
||||||
|
|
||||||
if cache_output is None:
|
if cache_output is None:
|
||||||
raise HyperglassError(message=params.messages.general, alert="danger")
|
raise HyperglassError(message=STATE.params.messages.general, alert="danger")
|
||||||
|
|
||||||
# Create a cache entry
|
# Create a cache entry
|
||||||
if json_output:
|
if json_output:
|
||||||
raw_output = json.dumps(cache_output)
|
raw_output = json.dumps(cache_output)
|
||||||
else:
|
else:
|
||||||
raw_output = str(cache_output)
|
raw_output = str(cache_output)
|
||||||
await cache.set_dict(cache_key, "output", raw_output)
|
cache.set_dict(cache_key, "output", raw_output)
|
||||||
await cache.set_dict(cache_key, "timestamp", timestamp)
|
cache.set_dict(cache_key, "timestamp", timestamp)
|
||||||
await cache.expire(cache_key, seconds=cache_timeout)
|
cache.expire(cache_key, seconds=cache_timeout)
|
||||||
|
|
||||||
log.debug("Added cache entry for query: {}", cache_key)
|
log.debug("Added cache entry for query: {}", cache_key)
|
||||||
|
|
||||||
runtime = int(round(elapsedtime, 0))
|
runtime = int(round(elapsedtime, 0))
|
||||||
|
|
||||||
# If it does, return the cached entry
|
# If it does, return the cached entry
|
||||||
cache_response = await cache.get_dict(cache_key, "output")
|
cache_response = cache.get_dict(cache_key, "output")
|
||||||
response_format = "text/plain"
|
response_format = "text/plain"
|
||||||
|
|
||||||
if json_output:
|
if json_output:
|
||||||
|
|
@ -161,11 +164,11 @@ async def query(query_data: Query, request: Request, background_tasks: Backgroun
|
||||||
|
|
||||||
async def docs():
|
async def docs():
|
||||||
"""Serve custom docs."""
|
"""Serve custom docs."""
|
||||||
if params.docs.enable:
|
if STATE.params.docs.enable:
|
||||||
docs_func_map = {"swagger": get_swagger_ui_html, "redoc": get_redoc_html}
|
docs_func_map = {"swagger": get_swagger_ui_html, "redoc": get_redoc_html}
|
||||||
docs_func = docs_func_map[params.docs.mode]
|
docs_func = docs_func_map[STATE.params.docs.mode]
|
||||||
return docs_func(
|
return docs_func(
|
||||||
openapi_url=params.docs.openapi_url, title=params.site_title + " - API Docs"
|
openapi_url=STATE.params.docs.openapi_url, title=STATE.params.site_title + " - API Docs"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(detail="Not found", status_code=404)
|
raise HTTPException(detail="Not found", status_code=404)
|
||||||
|
|
@ -173,32 +176,32 @@ async def docs():
|
||||||
|
|
||||||
async def router(id: str):
|
async def router(id: str):
|
||||||
"""Get a device's API-facing attributes."""
|
"""Get a device's API-facing attributes."""
|
||||||
return devices[id].export_api()
|
return STATE.devices[id].export_api()
|
||||||
|
|
||||||
|
|
||||||
async def routers():
|
async def routers():
|
||||||
"""Serve list of configured routers and attributes."""
|
"""Serve list of configured routers and attributes."""
|
||||||
return devices.export_api()
|
return STATE.devices.export_api()
|
||||||
|
|
||||||
|
|
||||||
async def queries():
|
async def queries():
|
||||||
"""Serve list of enabled query types."""
|
"""Serve list of enabled query types."""
|
||||||
return params.queries.list
|
return STATE.params.queries.list
|
||||||
|
|
||||||
|
|
||||||
async def info():
|
async def info():
|
||||||
"""Serve general information about this instance of hyperglass."""
|
"""Serve general information about this instance of hyperglass."""
|
||||||
return {
|
return {
|
||||||
"name": params.site_title,
|
"name": STATE.params.site_title,
|
||||||
"organization": params.org_name,
|
"organization": STATE.params.org_name,
|
||||||
"primary_asn": int(params.primary_asn),
|
"primary_asn": int(STATE.params.primary_asn),
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def ui_props():
|
async def ui_props():
|
||||||
"""Serve UI configration."""
|
"""Serve UI configration."""
|
||||||
return ui_params
|
return STATE.ui_params
|
||||||
|
|
||||||
|
|
||||||
endpoints = [query, docs, routers, info, ui_props]
|
endpoints = [query, docs, routers, info, ui_props]
|
||||||
|
|
|
||||||
62
hyperglass/cache/aio.py
vendored
62
hyperglass/cache/aio.py
vendored
|
|
@ -4,25 +4,49 @@
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
|
import typing as t
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from aredis import StrictRedis as AsyncRedis # type: ignore
|
from aredis import StrictRedis as AsyncRedis # type: ignore
|
||||||
from aredis.pubsub import PubSub as AsyncPubSub # type: ignore
|
from pydantic import SecretStr
|
||||||
from aredis.exceptions import RedisError # type: ignore
|
from aredis.exceptions import RedisError # type: ignore
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cache.base import BaseCache
|
from hyperglass.cache.base import BaseCache
|
||||||
from hyperglass.exceptions.private import DependencyError
|
from hyperglass.exceptions.private import DependencyError
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Third Party
|
||||||
|
from aredis.pubsub import PubSub as AsyncPubSub # type: ignore
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.config.params import Params
|
||||||
|
from hyperglass.models.config.devices import Devices
|
||||||
|
|
||||||
|
|
||||||
class AsyncCache(BaseCache):
|
class AsyncCache(BaseCache):
|
||||||
"""Asynchronous Redis cache handler."""
|
"""Asynchronous Redis cache handler."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
db: int,
|
||||||
|
host: str = "localhost",
|
||||||
|
port: int = 6379,
|
||||||
|
password: t.Optional[SecretStr] = None,
|
||||||
|
decode_responses: bool = False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
):
|
||||||
"""Initialize Redis connection."""
|
"""Initialize Redis connection."""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(
|
||||||
|
db=db,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
password=password,
|
||||||
|
decode_responses=decode_responses,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
password = self.password
|
password = self.password
|
||||||
if password is not None:
|
if password is not None:
|
||||||
|
|
@ -62,7 +86,7 @@ class AsyncCache(BaseCache):
|
||||||
e=err_msg,
|
e=err_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get(self, *args: str) -> Any:
|
async def get(self, *args: str) -> t.Any:
|
||||||
"""Get item(s) from cache."""
|
"""Get item(s) from cache."""
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
raw = await self.instance.get(args[0])
|
raw = await self.instance.get(args[0])
|
||||||
|
|
@ -70,7 +94,7 @@ class AsyncCache(BaseCache):
|
||||||
raw = await self.instance.mget(args)
|
raw = await self.instance.mget(args)
|
||||||
return self.parse_types(raw)
|
return self.parse_types(raw)
|
||||||
|
|
||||||
async def get_dict(self, key: str, field: str = "") -> Any:
|
async def get_dict(self, key: str, field: str = "") -> t.Any:
|
||||||
"""Get hash map (dict) item(s)."""
|
"""Get hash map (dict) item(s)."""
|
||||||
if not field:
|
if not field:
|
||||||
raw = await self.instance.hgetall(key)
|
raw = await self.instance.hgetall(key)
|
||||||
|
|
@ -87,7 +111,7 @@ class AsyncCache(BaseCache):
|
||||||
"""Set hash map (dict) values."""
|
"""Set hash map (dict) values."""
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
if isinstance(value, Dict):
|
if isinstance(value, t.Dict):
|
||||||
value = json.dumps(value)
|
value = json.dumps(value)
|
||||||
else:
|
else:
|
||||||
value = str(value)
|
value = str(value)
|
||||||
|
|
@ -99,7 +123,7 @@ class AsyncCache(BaseCache):
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
async def wait(self, pubsub: AsyncPubSub, timeout: int = 30, **kwargs) -> Any:
|
async def wait(self, pubsub: "AsyncPubSub", timeout: int = 30, **kwargs) -> t.Any:
|
||||||
"""Wait for pub/sub messages & return posted message."""
|
"""Wait for pub/sub messages & return posted message."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
timeout = now + timeout
|
timeout = now + timeout
|
||||||
|
|
@ -117,7 +141,7 @@ class AsyncCache(BaseCache):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def pubsub(self) -> AsyncPubSub:
|
async def pubsub(self) -> "AsyncPubSub":
|
||||||
"""Provide an aredis.pubsub.Pubsub instance."""
|
"""Provide an aredis.pubsub.Pubsub instance."""
|
||||||
return self.instance.pubsub()
|
return self.instance.pubsub()
|
||||||
|
|
||||||
|
|
@ -139,8 +163,20 @@ class AsyncCache(BaseCache):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
await self.instance.expire(key, seconds)
|
await self.instance.expire(key, seconds)
|
||||||
|
|
||||||
async def get_config(self) -> Dict:
|
async def get_params(self: "AsyncCache") -> "Params":
|
||||||
"""Get picked config object from cache."""
|
"""Get Params object from the cache."""
|
||||||
|
params = await self.instance.get(self.CONFIG_KEY)
|
||||||
|
return pickle.loads(params)
|
||||||
|
|
||||||
pickled = await self.instance.get("HYPERGLASS_CONFIG")
|
async def get_devices(self: "AsyncCache") -> "Devices":
|
||||||
return pickle.loads(pickled)
|
"""Get Devices object from the cache."""
|
||||||
|
devices = await self.instance.get(self.DEVICES_KEY)
|
||||||
|
return pickle.loads(devices)
|
||||||
|
|
||||||
|
async def set_config(self: "AsyncCache", config: "Params") -> None:
|
||||||
|
"""Add a params instance to the cache."""
|
||||||
|
await self.instance.set(self.CONFIG_KEY, pickle.dumps(config))
|
||||||
|
|
||||||
|
async def set_devices(self: "AsyncCache", devices: "Devices") -> None:
|
||||||
|
"""Add a devices instance to the cache."""
|
||||||
|
await self.instance.set(self.DEVICES_KEY, pickle.dumps(devices))
|
||||||
|
|
|
||||||
35
hyperglass/cache/base.py
vendored
35
hyperglass/cache/base.py
vendored
|
|
@ -3,7 +3,7 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
from typing import Any, Optional
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
|
|
@ -12,30 +12,35 @@ from pydantic import SecretStr
|
||||||
class BaseCache:
|
class BaseCache:
|
||||||
"""Redis cache handler."""
|
"""Redis cache handler."""
|
||||||
|
|
||||||
|
CONFIG_KEY: str = "hyperglass.config"
|
||||||
|
DEVICES_KEY: str = "hyperglass.devices"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
db: int,
|
db: int,
|
||||||
host: str = "localhost",
|
host: str = "localhost",
|
||||||
port: int = 6379,
|
port: int = 6379,
|
||||||
password: Optional[SecretStr] = None,
|
password: t.Optional[SecretStr] = None,
|
||||||
decode_responses: bool = True,
|
decode_responses: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: t.Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Redis connection."""
|
"""Initialize Redis connection."""
|
||||||
self.db: int = db
|
self.db = db
|
||||||
self.host: str = str(host)
|
self.host = str(host)
|
||||||
self.port: int = port
|
self.port = port
|
||||||
self.password: Optional[SecretStr] = password
|
self.password = password
|
||||||
self.decode_responses: bool = decode_responses
|
self.decode_responses = decode_responses
|
||||||
self.redis_args: dict = kwargs
|
self.redis_args = kwargs
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Represent class state."""
|
"""Represent class state."""
|
||||||
return "HyperglassCache(db={}, host={}, port={}, password={})".format(
|
|
||||||
|
return "HyperglassCache(db={!s}, host={}, port={!s}, password={})".format(
|
||||||
self.db, self.host, self.port, self.password
|
self.db, self.host, self.port, self.password
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_types(self, value: str) -> Any:
|
def parse_types(self, value: str) -> t.Any:
|
||||||
"""Parse a string to standard python types."""
|
"""Parse a string to standard python types."""
|
||||||
|
|
||||||
def parse_string(str_value: str):
|
def parse_string(str_value: str):
|
||||||
|
|
@ -56,11 +61,11 @@ class BaseCache:
|
||||||
value = parse_string(value)
|
value = parse_string(value)
|
||||||
elif isinstance(value, bytes):
|
elif isinstance(value, bytes):
|
||||||
value = parse_string(value.decode("utf-8"))
|
value = parse_string(value.decode("utf-8"))
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, t.List):
|
||||||
value = [parse_string(i) for i in value]
|
value = [parse_string(i) for i in value]
|
||||||
elif isinstance(value, tuple):
|
elif isinstance(value, t.Tuple):
|
||||||
value = tuple(parse_string(i) for i in value)
|
value = tuple(parse_string(i) for i in value)
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, t.Dict):
|
||||||
value = {k: self.parse_types(v) for k, v in value.items()}
|
value = {k: self.parse_types(v) for k, v in value.items()}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
|
||||||
72
hyperglass/cache/sync.py
vendored
72
hyperglass/cache/sync.py
vendored
|
|
@ -4,24 +4,48 @@
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
from typing import Any, Dict
|
import typing as t
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from redis import Redis as SyncRedis
|
from redis import Redis as SyncRedis
|
||||||
from redis.client import PubSub as SyncPubsSub
|
from pydantic import SecretStr
|
||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.cache.base import BaseCache
|
from hyperglass.cache.base import BaseCache
|
||||||
from hyperglass.exceptions.private import DependencyError
|
from hyperglass.exceptions.private import DependencyError
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Third Party
|
||||||
|
from redis.client import PubSub as SyncPubsSub
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.config.params import Params
|
||||||
|
from hyperglass.models.config.devices import Devices
|
||||||
|
|
||||||
|
|
||||||
class SyncCache(BaseCache):
|
class SyncCache(BaseCache):
|
||||||
"""Synchronous Redis cache handler."""
|
"""Synchronous Redis cache handler."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
db: int,
|
||||||
|
host: str = "localhost",
|
||||||
|
port: int = 6379,
|
||||||
|
password: t.Optional[SecretStr] = None,
|
||||||
|
decode_responses: bool = False,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
):
|
||||||
"""Initialize Redis connection."""
|
"""Initialize Redis connection."""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(
|
||||||
|
db=db,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
password=password,
|
||||||
|
decode_responses=decode_responses,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
password = self.password
|
password = self.password
|
||||||
if password is not None:
|
if password is not None:
|
||||||
|
|
@ -60,15 +84,25 @@ class SyncCache(BaseCache):
|
||||||
e=err_msg,
|
e=err_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, *args: str) -> Any:
|
def get(self, *args: str, decode: bool = True) -> t.Any:
|
||||||
"""Get item(s) from cache."""
|
"""Get item(s) from cache."""
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
raw = self.instance.get(args[0])
|
raw = self.instance.get(args[0])
|
||||||
else:
|
else:
|
||||||
raw = self.instance.mget(args)
|
raw = self.instance.mget(args)
|
||||||
|
if decode and isinstance(raw, bytes):
|
||||||
|
raw = raw.decode()
|
||||||
|
|
||||||
return self.parse_types(raw)
|
return self.parse_types(raw)
|
||||||
|
|
||||||
def get_dict(self, key: str, field: str = "") -> Any:
|
GetObj = t.TypeVar("GetObj")
|
||||||
|
|
||||||
|
def get_object(self, name: str, _type: t.Type[GetObj] = t.Any) -> GetObj:
|
||||||
|
raw = self.instance.get(name)
|
||||||
|
obj: _type = pickle.loads(raw)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_dict(self, key: str, field: str = "", *, decode: bool = True) -> t.Any:
|
||||||
"""Get hash map (dict) item(s)."""
|
"""Get hash map (dict) item(s)."""
|
||||||
if not field:
|
if not field:
|
||||||
raw = self.instance.hgetall(key)
|
raw = self.instance.hgetall(key)
|
||||||
|
|
@ -85,7 +119,7 @@ class SyncCache(BaseCache):
|
||||||
"""Set hash map (dict) values."""
|
"""Set hash map (dict) values."""
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
if isinstance(value, Dict):
|
if isinstance(value, t.Dict):
|
||||||
value = json.dumps(value)
|
value = json.dumps(value)
|
||||||
else:
|
else:
|
||||||
value = str(value)
|
value = str(value)
|
||||||
|
|
@ -97,7 +131,7 @@ class SyncCache(BaseCache):
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def wait(self, pubsub: SyncPubsSub, timeout: int = 30, **kwargs) -> Any:
|
def wait(self, pubsub: "SyncPubsSub", timeout: int = 30, **kwargs) -> t.Any:
|
||||||
"""Wait for pub/sub messages & return posted message."""
|
"""Wait for pub/sub messages & return posted message."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
timeout = now + timeout
|
timeout = now + timeout
|
||||||
|
|
@ -115,7 +149,7 @@ class SyncCache(BaseCache):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def pubsub(self) -> SyncPubsSub:
|
def pubsub(self) -> "SyncPubsSub":
|
||||||
"""Provide a redis.client.Pubsub instance."""
|
"""Provide a redis.client.Pubsub instance."""
|
||||||
return self.instance.pubsub()
|
return self.instance.pubsub()
|
||||||
|
|
||||||
|
|
@ -137,8 +171,20 @@ class SyncCache(BaseCache):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
self.instance.expire(key, seconds)
|
self.instance.expire(key, seconds)
|
||||||
|
|
||||||
def get_config(self) -> Dict:
|
def get_params(self) -> "Params":
|
||||||
"""Get picked config object from cache."""
|
"""Get Params object from the cache."""
|
||||||
|
return self.get_object(self.CONFIG_KEY, "Params")
|
||||||
|
# return pickle.loads(self.get(self.CONFIG_KEY, decode=False, parse=False))
|
||||||
|
|
||||||
pickled = self.instance.get("HYPERGLASS_CONFIG")
|
def get_devices(self) -> "Devices":
|
||||||
return pickle.loads(pickled)
|
"""Get Devices object from the cache."""
|
||||||
|
return self.get_object(self.DEVICES_KEY, "Devices")
|
||||||
|
# return pickle.loads(self.get(self.DEVICES_KEY, decode=False, parse=False))
|
||||||
|
|
||||||
|
def set_config(self: "SyncCache", config: "Params") -> None:
|
||||||
|
"""Add a params instance to the cache."""
|
||||||
|
self.instance.set(self.CONFIG_KEY, pickle.dumps(config))
|
||||||
|
|
||||||
|
def set_devices(self: "SyncCache", devices: "Devices") -> None:
|
||||||
|
"""Add a devices instance to the cache."""
|
||||||
|
self.instance.set(self.DEVICES_KEY, pickle.dumps(devices))
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,8 @@ import yaml
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import (
|
from hyperglass.log import log, enable_file_logging, enable_syslog_logging
|
||||||
log,
|
from hyperglass.util import set_app_path, set_cache_env
|
||||||
set_log_level,
|
|
||||||
enable_file_logging,
|
|
||||||
enable_syslog_logging,
|
|
||||||
)
|
|
||||||
from hyperglass.util import set_app_path, set_cache_env, current_log_level
|
|
||||||
from hyperglass.defaults import CREDIT
|
from hyperglass.defaults import CREDIT
|
||||||
from hyperglass.constants import PARSED_RESPONSE_FIELDS, __version__
|
from hyperglass.constants import PARSED_RESPONSE_FIELDS, __version__
|
||||||
from hyperglass.models.ui import UIParameters
|
from hyperglass.models.ui import UIParameters
|
||||||
|
|
@ -135,20 +130,11 @@ def _get_devices(data: List[Dict], directives: List[Directive]) -> Devices:
|
||||||
user_config = _config_optional(CONFIG_MAIN)
|
user_config = _config_optional(CONFIG_MAIN)
|
||||||
|
|
||||||
# Read raw debug value from config to enable debugging quickly.
|
# Read raw debug value from config to enable debugging quickly.
|
||||||
set_log_level(logger=log, debug=user_config.get("debug", True))
|
|
||||||
|
|
||||||
# Map imported user configuration to expected schema.
|
# Map imported user configuration to expected schema.
|
||||||
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
|
log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config)
|
||||||
params = validate_config(config=user_config, importer=Params)
|
params = validate_config(config=user_config, importer=Params)
|
||||||
|
|
||||||
# Re-evaluate debug state after config is validated
|
|
||||||
log_level = current_log_level(log)
|
|
||||||
|
|
||||||
if params.debug and log_level != "debug":
|
|
||||||
set_log_level(logger=log, debug=True)
|
|
||||||
elif not params.debug and log_level == "debug":
|
|
||||||
set_log_level(logger=log, debug=False)
|
|
||||||
|
|
||||||
# Map imported user commands to expected schema.
|
# Map imported user commands to expected schema.
|
||||||
_user_commands = _config_optional(CONFIG_COMMANDS)
|
_user_commands = _config_optional(CONFIG_COMMANDS)
|
||||||
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
|
log.debug("Unvalidated commands from {}: {}", CONFIG_COMMANDS, _user_commands)
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,7 @@ class DependencyError(PrivateHyperglassError):
|
||||||
|
|
||||||
class PluginError(PrivateHyperglassError):
|
class PluginError(PrivateHyperglassError):
|
||||||
"""Raised when a plugin error occurs."""
|
"""Raised when a plugin error occurs."""
|
||||||
|
|
||||||
|
|
||||||
|
class StateError(PrivateHyperglassError):
|
||||||
|
"""Raised when an error occurs while fetching state from Redis."""
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,10 @@ from .agent import AgentConnection
|
||||||
from ._common import Connection
|
from ._common import Connection
|
||||||
from .ssh_netmiko import NetmikoConnection
|
from .ssh_netmiko import NetmikoConnection
|
||||||
from .ssh_scrapli import ScrapliConnection
|
from .ssh_scrapli import ScrapliConnection
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"AgentConnection",
|
||||||
|
"Connection",
|
||||||
|
"NetmikoConnection",
|
||||||
|
"ScrapliConnection",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,32 @@ hyperglass API modules.
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import re
|
import re
|
||||||
import json as _json
|
import json as _json
|
||||||
|
import typing as t
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.util import get_fmt_keys
|
from hyperglass.util import get_fmt_keys
|
||||||
from hyperglass.constants import TRANSPORT_REST, TARGET_FORMAT_SPACE
|
from hyperglass.constants import TRANSPORT_REST, TARGET_FORMAT_SPACE
|
||||||
from hyperglass.models.api.query import Query
|
|
||||||
from hyperglass.exceptions.public import InputInvalid
|
from hyperglass.exceptions.public import InputInvalid
|
||||||
from hyperglass.exceptions.private import ConfigError
|
from hyperglass.exceptions.private import ConfigError
|
||||||
from hyperglass.models.config.devices import Device
|
|
||||||
from hyperglass.models.commands.generic import Directive
|
if t.TYPE_CHECKING:
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.api.query import Query
|
||||||
|
from hyperglass.models.config.devices import Device
|
||||||
|
from hyperglass.models.commands.generic import Directive
|
||||||
|
|
||||||
|
|
||||||
class Construct:
|
class Construct:
|
||||||
"""Construct SSH commands/REST API parameters from validated query data."""
|
"""Construct SSH commands/REST API parameters from validated query data."""
|
||||||
|
|
||||||
directive: Directive
|
directive: "Directive"
|
||||||
device: Device
|
device: "Device"
|
||||||
query: Query
|
query: "Query"
|
||||||
transport: str
|
transport: str
|
||||||
target: str
|
target: str
|
||||||
|
|
||||||
def __init__(self, device, query):
|
def __init__(self, device: "Device", query: "Query"):
|
||||||
"""Initialize command construction."""
|
"""Initialize command construction."""
|
||||||
log.debug(
|
log.debug(
|
||||||
"Constructing '{}' query for '{}'", query.query_type, str(query.query_target),
|
"Constructing '{}' query for '{}'", query.query_type, str(query.query_target),
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ import httpx
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.util import parse_exception
|
from hyperglass.util import parse_exception
|
||||||
|
from hyperglass.state import use_state
|
||||||
from hyperglass.encode import jwt_decode, jwt_encode
|
from hyperglass.encode import jwt_decode, jwt_encode
|
||||||
from hyperglass.configuration import params
|
|
||||||
from hyperglass.exceptions.public import RestError, ResponseEmpty
|
from hyperglass.exceptions.public import RestError, ResponseEmpty
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
|
@ -38,10 +38,11 @@ class AgentConnection(Connection):
|
||||||
async def collect(self) -> Iterable: # noqa: C901
|
async def collect(self) -> Iterable: # noqa: C901
|
||||||
"""Connect to a device running hyperglass-agent via HTTP."""
|
"""Connect to a device running hyperglass-agent via HTTP."""
|
||||||
log.debug("Query parameters: {}", self.query)
|
log.debug("Query parameters: {}", self.query)
|
||||||
|
state = use_state()
|
||||||
|
|
||||||
client_params = {
|
client_params = {
|
||||||
"headers": {"Content-Type": "application/json"},
|
"headers": {"Content-Type": "application/json"},
|
||||||
"timeout": params.request_timeout,
|
"timeout": state.params.request_timeout,
|
||||||
}
|
}
|
||||||
if self.device.ssl is not None and self.device.ssl.enable:
|
if self.device.ssl is not None and self.device.ssl.enable:
|
||||||
with self.device.ssl.cert.open("r") as file:
|
with self.device.ssl.cert.open("r") as file:
|
||||||
|
|
@ -76,7 +77,7 @@ class AgentConnection(Connection):
|
||||||
encoded_query = await jwt_encode(
|
encoded_query = await jwt_encode(
|
||||||
payload=query,
|
payload=query,
|
||||||
secret=self.device.credential.password.get_secret_value(),
|
secret=self.device.credential.password.get_secret_value(),
|
||||||
duration=params.request_timeout,
|
duration=state.params.request_timeout,
|
||||||
)
|
)
|
||||||
log.debug("Encoded JWT: {}", encoded_query)
|
log.debug("Encoded JWT: {}", encoded_query)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.configuration import params
|
from hyperglass.state import use_state
|
||||||
from hyperglass.compat._sshtunnel import BaseSSHTunnelForwarderError, open_tunnel
|
from hyperglass.compat._sshtunnel import BaseSSHTunnelForwarderError, open_tunnel
|
||||||
from hyperglass.exceptions.public import ScrapeError
|
from hyperglass.exceptions.public import ScrapeError
|
||||||
|
|
||||||
|
|
@ -24,6 +24,7 @@ class SSHConnection(Connection):
|
||||||
"""Return a preconfigured sshtunnel.SSHTunnelForwarder instance."""
|
"""Return a preconfigured sshtunnel.SSHTunnelForwarder instance."""
|
||||||
|
|
||||||
proxy = self.device.proxy
|
proxy = self.device.proxy
|
||||||
|
state = use_state()
|
||||||
|
|
||||||
def opener():
|
def opener():
|
||||||
"""Set up an SSH tunnel according to a device's configuration."""
|
"""Set up an SSH tunnel according to a device's configuration."""
|
||||||
|
|
@ -32,7 +33,7 @@ class SSHConnection(Connection):
|
||||||
"remote_bind_address": (self.device._target, self.device.port),
|
"remote_bind_address": (self.device._target, self.device.port),
|
||||||
"local_bind_address": ("localhost", 0),
|
"local_bind_address": ("localhost", 0),
|
||||||
"skip_tunnel_checkup": False,
|
"skip_tunnel_checkup": False,
|
||||||
"gateway_timeout": params.request_timeout - 2,
|
"gateway_timeout": state.params.request_timeout - 2,
|
||||||
}
|
}
|
||||||
if proxy.credential._method == "password":
|
if proxy.credential._method == "password":
|
||||||
# Use password auth if no key is defined.
|
# Use password auth if no key is defined.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from netmiko import ( # type: ignore
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.configuration import params
|
from hyperglass.state import state
|
||||||
from hyperglass.exceptions.public import AuthError, DeviceTimeout, ResponseEmpty
|
from hyperglass.exceptions.public import AuthError, DeviceTimeout, ResponseEmpty
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
|
@ -65,9 +65,9 @@ class NetmikoConnection(SSHConnection):
|
||||||
"port": port or self.device.port,
|
"port": port or self.device.port,
|
||||||
"device_type": self.device.type,
|
"device_type": self.device.type,
|
||||||
"username": self.device.credential.username,
|
"username": self.device.credential.username,
|
||||||
"global_delay_factor": params.netmiko_delay_factor,
|
"global_delay_factor": state.params.netmiko_delay_factor,
|
||||||
"timeout": math.floor(params.request_timeout * 1.25),
|
"timeout": math.floor(state.params.request_timeout * 1.25),
|
||||||
"session_timeout": math.ceil(params.request_timeout - 1),
|
"session_timeout": math.ceil(state.params.request_timeout - 1),
|
||||||
**global_args,
|
**global_args,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from scrapli.driver.core import (
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.configuration import params
|
from hyperglass.state import use_state
|
||||||
from hyperglass.exceptions.public import (
|
from hyperglass.exceptions.public import (
|
||||||
AuthError,
|
AuthError,
|
||||||
ScrapeError,
|
ScrapeError,
|
||||||
|
|
@ -71,6 +71,7 @@ class ScrapliConnection(SSHConnection):
|
||||||
Directly connects to the router via Netmiko library, returns the
|
Directly connects to the router via Netmiko library, returns the
|
||||||
command output.
|
command output.
|
||||||
"""
|
"""
|
||||||
|
state = use_state()
|
||||||
driver = _map_driver(self.device.type)
|
driver = _map_driver(self.device.type)
|
||||||
|
|
||||||
if host is not None:
|
if host is not None:
|
||||||
|
|
@ -89,7 +90,7 @@ class ScrapliConnection(SSHConnection):
|
||||||
"host": host or self.device._target,
|
"host": host or self.device._target,
|
||||||
"port": port or self.device.port,
|
"port": port or self.device.port,
|
||||||
"auth_username": self.device.credential.username,
|
"auth_username": self.device.credential.username,
|
||||||
"timeout_ops": math.floor(params.request_timeout * 1.25),
|
"timeout_ops": math.floor(state.params.request_timeout * 1.25),
|
||||||
"transport": "asyncssh",
|
"transport": "asyncssh",
|
||||||
"auth_strict_key": False,
|
"auth_strict_key": False,
|
||||||
"ssh_known_hosts_file": False,
|
"ssh_known_hosts_file": False,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Dict, Union, Callable
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.configuration import params
|
from hyperglass.state import use_state
|
||||||
from hyperglass.exceptions.public import DeviceTimeout, ResponseEmpty
|
from hyperglass.exceptions.public import DeviceTimeout, ResponseEmpty
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -47,8 +47,8 @@ def handle_timeout(**exc_args: Any) -> Callable:
|
||||||
|
|
||||||
async def execute(query: "Query") -> Union["OutputDataModel", str]:
|
async def execute(query: "Query") -> Union["OutputDataModel", str]:
|
||||||
"""Initiate query validation and execution."""
|
"""Initiate query validation and execution."""
|
||||||
|
state = use_state()
|
||||||
output = params.messages.general
|
output = state.params.messages.general
|
||||||
|
|
||||||
log.debug("Received query for {}", query.json())
|
log.debug("Received query for {}", query.json())
|
||||||
log.debug("Matched device config: {}", query.device)
|
log.debug("Matched device config: {}", query.device)
|
||||||
|
|
@ -60,7 +60,7 @@ async def execute(query: "Query") -> Union["OutputDataModel", str]:
|
||||||
signal.SIGALRM,
|
signal.SIGALRM,
|
||||||
handle_timeout(error=TimeoutError("Connection timed out"), device=query.device),
|
handle_timeout(error=TimeoutError("Connection timed out"), device=query.device),
|
||||||
)
|
)
|
||||||
signal.alarm(params.request_timeout - 1)
|
signal.alarm(state.params.request_timeout - 1)
|
||||||
|
|
||||||
if query.device.proxy:
|
if query.device.proxy:
|
||||||
proxy = driver.setup_proxy()
|
proxy = driver.setup_proxy()
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,17 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import sys
|
import sys
|
||||||
import math
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import typing as t
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from gunicorn.app.base import BaseApplication # type: ignore
|
from gunicorn.app.base import BaseApplication # type: ignore
|
||||||
from gunicorn.glogging import Logger # type: ignore
|
from gunicorn.glogging import Logger # type: ignore
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .log import log, setup_lib_logging
|
from .log import log, set_log_level, setup_lib_logging
|
||||||
from .plugins import (
|
from .plugins import (
|
||||||
InputPluginManager,
|
InputPluginManager,
|
||||||
OutputPluginManager,
|
OutputPluginManager,
|
||||||
|
|
@ -23,7 +22,7 @@ from .plugins import (
|
||||||
from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__
|
from .constants import MIN_NODE_VERSION, MIN_PYTHON_VERSION, __version__
|
||||||
from .util.frontend import get_node_version
|
from .util.frontend import get_node_version
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
# Third Party
|
# Third Party
|
||||||
from gunicorn.arbiter import Arbiter # type: ignore
|
from gunicorn.arbiter import Arbiter # type: ignore
|
||||||
|
|
||||||
|
|
@ -39,33 +38,19 @@ if sys.version_info < MIN_PYTHON_VERSION:
|
||||||
node_major, _, __ = get_node_version()
|
node_major, _, __ = get_node_version()
|
||||||
|
|
||||||
if node_major != MIN_NODE_VERSION:
|
if node_major != MIN_NODE_VERSION:
|
||||||
raise RuntimeError(f"NodeJS {MIN_NODE_VERSION}+ is required.")
|
raise RuntimeError(f"NodeJS {MIN_NODE_VERSION!s}+ is required.")
|
||||||
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.compat._asyncio import aiorun
|
from hyperglass.compat._asyncio import aiorun
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .util import cpu_count, clear_redis_cache, format_listen_address
|
from .util import cpu_count
|
||||||
from .cache import SyncCache
|
from .state import use_state
|
||||||
from .configuration import (
|
from .settings import Settings
|
||||||
URL_DEV,
|
from .configuration import URL_DEV, URL_PROD
|
||||||
URL_PROD,
|
|
||||||
CONFIG_PATH,
|
|
||||||
REDIS_CONFIG,
|
|
||||||
params,
|
|
||||||
devices,
|
|
||||||
ui_params,
|
|
||||||
)
|
|
||||||
from .util.frontend import build_frontend
|
from .util.frontend import build_frontend
|
||||||
|
|
||||||
if params.debug:
|
|
||||||
workers = 1
|
|
||||||
loglevel = "DEBUG"
|
|
||||||
else:
|
|
||||||
workers = cpu_count(2)
|
|
||||||
loglevel = "WARNING"
|
|
||||||
|
|
||||||
|
|
||||||
class StubbedGunicornLogger(Logger):
|
class StubbedGunicornLogger(Logger):
|
||||||
"""Custom logging to direct Gunicorn/Uvicorn logs to Loguru/Rich.
|
"""Custom logging to direct Gunicorn/Uvicorn logs to Loguru/Rich.
|
||||||
|
|
@ -73,58 +58,30 @@ class StubbedGunicornLogger(Logger):
|
||||||
See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
|
See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setup(self, cfg):
|
def setup(self, cfg: t.Any) -> None:
|
||||||
"""Override Gunicorn setup."""
|
"""Override Gunicorn setup."""
|
||||||
handler = logging.NullHandler()
|
handler = logging.NullHandler()
|
||||||
self.error_logger = logging.getLogger("gunicorn.error")
|
self.error_logger = logging.getLogger("gunicorn.error")
|
||||||
self.error_logger.addHandler(handler)
|
self.error_logger.addHandler(handler)
|
||||||
self.access_logger = logging.getLogger("gunicorn.access")
|
self.access_logger = logging.getLogger("gunicorn.access")
|
||||||
self.access_logger.addHandler(handler)
|
self.access_logger.addHandler(handler)
|
||||||
self.error_logger.setLevel(loglevel)
|
self.error_logger.setLevel(Settings.log_level)
|
||||||
self.access_logger.setLevel(loglevel)
|
self.access_logger.setLevel(Settings.log_level)
|
||||||
|
|
||||||
|
|
||||||
def check_redis_instance() -> bool:
|
|
||||||
"""Ensure Redis is running before starting server."""
|
|
||||||
|
|
||||||
cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
|
|
||||||
cache.test()
|
|
||||||
log.debug("Redis is running at: {}:{}", REDIS_CONFIG["host"], REDIS_CONFIG["port"])
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def build_ui() -> bool:
|
async def build_ui() -> bool:
|
||||||
"""Perform a UI build prior to starting the application."""
|
"""Perform a UI build prior to starting the application."""
|
||||||
|
state = use_state()
|
||||||
await build_frontend(
|
await build_frontend(
|
||||||
dev_mode=params.developer_mode,
|
dev_mode=Settings.dev_mode,
|
||||||
dev_url=URL_DEV,
|
dev_url=URL_DEV,
|
||||||
prod_url=URL_PROD,
|
prod_url=URL_PROD,
|
||||||
params=ui_params,
|
params=state.ui_params,
|
||||||
app_path=CONFIG_PATH,
|
app_path=Settings.app_path,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def clear_cache():
|
|
||||||
"""Clear the Redis cache on shutdown."""
|
|
||||||
try:
|
|
||||||
await clear_redis_cache(db=params.cache.database, config=REDIS_CONFIG)
|
|
||||||
except RuntimeError as e:
|
|
||||||
log.error(str(e))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def cache_config() -> bool:
|
|
||||||
"""Add configuration to Redis cache as a pickled object."""
|
|
||||||
# Standard Library
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
|
|
||||||
cache.set("HYPERGLASS_CONFIG", pickle.dumps(params))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def register_all_plugins(devices: "Devices") -> None:
|
def register_all_plugins(devices: "Devices") -> None:
|
||||||
"""Validate and register configured plugins."""
|
"""Validate and register configured plugins."""
|
||||||
|
|
||||||
|
|
@ -149,23 +106,21 @@ def unregister_all_plugins() -> None:
|
||||||
def on_starting(server: "Arbiter"):
|
def on_starting(server: "Arbiter"):
|
||||||
"""Gunicorn pre-start tasks."""
|
"""Gunicorn pre-start tasks."""
|
||||||
|
|
||||||
setup_lib_logging()
|
|
||||||
|
|
||||||
python_version = platform.python_version()
|
python_version = platform.python_version()
|
||||||
required = ".".join((str(v) for v in MIN_PYTHON_VERSION))
|
required = ".".join((str(v) for v in MIN_PYTHON_VERSION))
|
||||||
log.info("Python {} detected ({} required)", python_version, required)
|
log.debug("Python {} detected ({} required)", python_version, required)
|
||||||
|
|
||||||
|
state = use_state()
|
||||||
|
|
||||||
|
register_all_plugins(state.devices)
|
||||||
|
|
||||||
check_redis_instance()
|
|
||||||
aiorun(build_ui())
|
aiorun(build_ui())
|
||||||
cache_config()
|
|
||||||
register_all_plugins(devices)
|
|
||||||
|
|
||||||
log.success(
|
log.success(
|
||||||
"Started hyperglass {v} on http://{h}:{p} with {w} workers",
|
"Started hyperglass {} on http://{} with {!s} workers",
|
||||||
v=__version__,
|
__version__,
|
||||||
h=format_listen_address(params.listen_address),
|
Settings.bind(),
|
||||||
p=str(params.listen_port),
|
server.app.cfg.settings["workers"].value,
|
||||||
w=server.app.cfg.settings["workers"].value,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -174,11 +129,10 @@ def on_exit(server: "Arbiter"):
|
||||||
|
|
||||||
log.critical("Stopping hyperglass {}", __version__)
|
log.critical("Stopping hyperglass {}", __version__)
|
||||||
|
|
||||||
async def runner():
|
state = use_state()
|
||||||
if not params.developer_mode:
|
if not Settings.dev_mode:
|
||||||
await clear_cache()
|
state.clear()
|
||||||
|
|
||||||
aiorun(runner())
|
|
||||||
unregister_all_plugins()
|
unregister_all_plugins()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -210,24 +164,29 @@ class HyperglassWSGI(BaseApplication):
|
||||||
def start(**kwargs):
|
def start(**kwargs):
|
||||||
"""Start hyperglass via gunicorn."""
|
"""Start hyperglass via gunicorn."""
|
||||||
|
|
||||||
|
set_log_level(log, Settings.debug)
|
||||||
|
|
||||||
|
log.debug("System settings: {!r}", Settings)
|
||||||
|
setup_lib_logging()
|
||||||
|
|
||||||
|
workers, log_level = 1, "DEBUG"
|
||||||
|
if Settings.debug is False:
|
||||||
|
workers, log_level = cpu_count(2), "WARNING"
|
||||||
|
|
||||||
HyperglassWSGI(
|
HyperglassWSGI(
|
||||||
app="hyperglass.api:app",
|
app="hyperglass.api:app",
|
||||||
options={
|
options={
|
||||||
"worker_class": "uvicorn.workers.UvicornWorker",
|
|
||||||
"preload": True,
|
"preload": True,
|
||||||
"keepalive": 10,
|
|
||||||
"command": shutil.which("gunicorn"),
|
|
||||||
"bind": ":".join(
|
|
||||||
(format_listen_address(params.listen_address), str(params.listen_port))
|
|
||||||
),
|
|
||||||
"workers": workers,
|
|
||||||
"loglevel": loglevel,
|
|
||||||
"timeout": math.ceil(params.request_timeout * 1.25),
|
|
||||||
"on_starting": on_starting,
|
|
||||||
"on_exit": on_exit,
|
|
||||||
"logger_class": StubbedGunicornLogger,
|
|
||||||
"accesslog": "-",
|
|
||||||
"errorlog": "-",
|
"errorlog": "-",
|
||||||
|
"accesslog": "-",
|
||||||
|
"workers": workers,
|
||||||
|
"on_exit": on_exit,
|
||||||
|
"loglevel": log_level,
|
||||||
|
"bind": Settings.bind(),
|
||||||
|
"on_starting": on_starting,
|
||||||
|
"command": shutil.which("gunicorn"),
|
||||||
|
"logger_class": StubbedGunicornLogger,
|
||||||
|
"worker_class": "uvicorn.workers.UvicornWorker",
|
||||||
"logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}},
|
"logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}},
|
||||||
**kwargs,
|
**kwargs,
|
||||||
},
|
},
|
||||||
|
|
@ -235,4 +194,12 @@ def start(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
start()
|
try:
|
||||||
|
start()
|
||||||
|
except Exception as error:
|
||||||
|
if not Settings.dev_mode:
|
||||||
|
state = use_state()
|
||||||
|
state.clear()
|
||||||
|
log.info("Cleared Redis cache")
|
||||||
|
unregister_all_plugins()
|
||||||
|
raise error
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Dict, List, Union, Literal, Optional
|
import typing as t
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import IPv4Network, IPv6Network, ip_network
|
from ipaddress import IPv4Network, IPv6Network, ip_network
|
||||||
|
|
||||||
|
|
@ -25,15 +25,18 @@ from hyperglass.exceptions.private import InputValidationError
|
||||||
# Local
|
# Local
|
||||||
from ..main import HyperglassModel, HyperglassModelWithId
|
from ..main import HyperglassModel, HyperglassModelWithId
|
||||||
from ..fields import Action
|
from ..fields import Action
|
||||||
from ..config.params import Params
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Local
|
||||||
|
from ..config.params import Params
|
||||||
|
|
||||||
IPv4PrefixLength = conint(ge=0, le=32)
|
IPv4PrefixLength = conint(ge=0, le=32)
|
||||||
IPv6PrefixLength = conint(ge=0, le=128)
|
IPv6PrefixLength = conint(ge=0, le=128)
|
||||||
IPNetwork = Union[IPv4Network, IPv6Network]
|
IPNetwork = t.Union[IPv4Network, IPv6Network]
|
||||||
StringOrArray = Union[StrictStr, List[StrictStr]]
|
StringOrArray = t.Union[StrictStr, t.List[StrictStr]]
|
||||||
Condition = Union[IPv4Network, IPv6Network, StrictStr]
|
Condition = t.Union[IPv4Network, IPv6Network, StrictStr]
|
||||||
RuleValidation = Union[Literal["ipv4", "ipv6", "pattern"], None]
|
RuleValidation = t.Union[t.Literal["ipv4", "ipv6", "pattern"], None]
|
||||||
PassedValidation = Union[bool, None]
|
PassedValidation = t.Union[bool, None]
|
||||||
|
|
||||||
|
|
||||||
class Input(HyperglassModel):
|
class Input(HyperglassModel):
|
||||||
|
|
@ -57,14 +60,14 @@ class Text(Input):
|
||||||
"""Text/input field model."""
|
"""Text/input field model."""
|
||||||
|
|
||||||
_type: PrivateAttr = PrivateAttr("text")
|
_type: PrivateAttr = PrivateAttr("text")
|
||||||
validation: Optional[StrictStr]
|
validation: t.Optional[StrictStr]
|
||||||
|
|
||||||
|
|
||||||
class Option(HyperglassModel):
|
class Option(HyperglassModel):
|
||||||
"""Select option model."""
|
"""Select option model."""
|
||||||
|
|
||||||
name: Optional[StrictStr]
|
name: t.Optional[StrictStr]
|
||||||
description: Optional[StrictStr]
|
description: t.Optional[StrictStr]
|
||||||
value: StrictStr
|
value: StrictStr
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -72,7 +75,7 @@ class Select(Input):
|
||||||
"""Select field model."""
|
"""Select field model."""
|
||||||
|
|
||||||
_type: PrivateAttr = PrivateAttr("select")
|
_type: PrivateAttr = PrivateAttr("select")
|
||||||
options: List[Option]
|
options: t.List[Option]
|
||||||
|
|
||||||
|
|
||||||
class Rule(HyperglassModel, allow_population_by_field_name=True):
|
class Rule(HyperglassModel, allow_population_by_field_name=True):
|
||||||
|
|
@ -82,10 +85,10 @@ class Rule(HyperglassModel, allow_population_by_field_name=True):
|
||||||
_passed: PassedValidation = PrivateAttr(None)
|
_passed: PassedValidation = PrivateAttr(None)
|
||||||
condition: Condition
|
condition: Condition
|
||||||
action: Action = Action("permit")
|
action: Action = Action("permit")
|
||||||
commands: List[str] = Field([], alias="command")
|
commands: t.List[str] = Field([], alias="command")
|
||||||
|
|
||||||
@validator("commands", pre=True, allow_reuse=True)
|
@validator("commands", pre=True, allow_reuse=True)
|
||||||
def validate_commands(cls, value: Union[str, List[str]]) -> List[str]:
|
def validate_commands(cls, value: t.Union[str, t.List[str]]) -> t.List[str]:
|
||||||
"""Ensure commands is a list."""
|
"""Ensure commands is a list."""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return [value]
|
return [value]
|
||||||
|
|
@ -215,13 +218,13 @@ class RuleWithoutValidation(Rule):
|
||||||
_validation: RuleValidation = PrivateAttr(None)
|
_validation: RuleValidation = PrivateAttr(None)
|
||||||
condition: None
|
condition: None
|
||||||
|
|
||||||
def validate_target(self, target: str) -> Literal[True]:
|
def validate_target(self, target: str) -> t.Literal[True]:
|
||||||
"""Don't validate a target. Always returns `True`."""
|
"""Don't validate a target. Always returns `True`."""
|
||||||
self._passed = True
|
self._passed = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
Rules = Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation]
|
Rules = t.Union[RuleWithIPv4, RuleWithIPv6, RuleWithPattern, RuleWithoutValidation]
|
||||||
|
|
||||||
|
|
||||||
class Directive(HyperglassModelWithId):
|
class Directive(HyperglassModelWithId):
|
||||||
|
|
@ -229,11 +232,12 @@ class Directive(HyperglassModelWithId):
|
||||||
|
|
||||||
id: StrictStr
|
id: StrictStr
|
||||||
name: StrictStr
|
name: StrictStr
|
||||||
rules: List[Rules]
|
rules: t.List[Rules]
|
||||||
field: Union[Text, Select, None]
|
field: t.Union[Text, Select, None]
|
||||||
info: Optional[FilePath]
|
info: t.Optional[FilePath]
|
||||||
plugins: List[StrictStr] = []
|
plugins: t.List[StrictStr] = []
|
||||||
groups: List[
|
disable_builtins: StrictBool = False
|
||||||
|
groups: t.List[
|
||||||
StrictStr
|
StrictStr
|
||||||
] = [] # TODO: Flesh this out. Replace VRFs, but use same logic in React to filter available commands for multi-device queries.
|
] = [] # TODO: Flesh this out. Replace VRFs, but use same logic in React to filter available commands for multi-device queries.
|
||||||
|
|
||||||
|
|
@ -247,7 +251,7 @@ class Directive(HyperglassModelWithId):
|
||||||
raise InputValidationError(error="No matched validation rules", target=target)
|
raise InputValidationError(error="No matched validation rules", target=target)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def field_type(self) -> Literal["text", "select", None]:
|
def field_type(self) -> t.Literal["text", "select", None]:
|
||||||
"""Get the linked field type."""
|
"""Get the linked field type."""
|
||||||
|
|
||||||
if self.field.is_select:
|
if self.field.is_select:
|
||||||
|
|
@ -257,7 +261,7 @@ class Directive(HyperglassModelWithId):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@validator("plugins")
|
@validator("plugins")
|
||||||
def validate_plugins(cls: "Directive", plugins: List[str]) -> List[str]:
|
def validate_plugins(cls: "Directive", plugins: t.List[str]) -> t.List[str]:
|
||||||
"""Validate and register configured plugins."""
|
"""Validate and register configured plugins."""
|
||||||
plugin_dir = Path(os.environ["hyperglass_directory"]) / "plugins"
|
plugin_dir = Path(os.environ["hyperglass_directory"]) / "plugins"
|
||||||
if plugin_dir.exists():
|
if plugin_dir.exists():
|
||||||
|
|
@ -271,7 +275,7 @@ class Directive(HyperglassModelWithId):
|
||||||
return [str(f) for f in matching_plugins]
|
return [str(f) for f in matching_plugins]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def frontend(self, params: Params) -> Dict:
|
def frontend(self: "Directive", params: "Params") -> t.Dict[str, t.Any]:
|
||||||
"""Prepare a representation of the directive for the UI."""
|
"""Prepare a representation of the directive for the UI."""
|
||||||
|
|
||||||
value = {
|
value = {
|
||||||
|
|
|
||||||
116
hyperglass/models/system.py
Normal file
116
hyperglass/models/system.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""hyperglass System Settings model."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import typing as t
|
||||||
|
from ipaddress import ip_address
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from pydantic import (
|
||||||
|
RedisDsn,
|
||||||
|
SecretStr,
|
||||||
|
BaseSettings,
|
||||||
|
DirectoryPath,
|
||||||
|
IPvAnyAddress,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.util import at_least, cpu_count
|
||||||
|
|
||||||
|
ListenHost = t.Union[None, IPvAnyAddress, t.Literal["localhost"]]
|
||||||
|
|
||||||
|
|
||||||
|
class HyperglassSystem(BaseSettings):
|
||||||
|
"""hyperglass system settings, required to start hyperglass."""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""hyperglass system settings configuration."""
|
||||||
|
|
||||||
|
env_prefix = "hyperglass_"
|
||||||
|
|
||||||
|
debug: bool = False
|
||||||
|
dev_mode: bool = False
|
||||||
|
app_path: DirectoryPath
|
||||||
|
redis_host: str = "localhost"
|
||||||
|
redis_password: t.Optional[SecretStr]
|
||||||
|
redis_db: int = 1
|
||||||
|
redis_dsn: RedisDsn = None
|
||||||
|
host: IPvAnyAddress = None
|
||||||
|
port: int = 8001
|
||||||
|
|
||||||
|
@validator("host", pre=True, always=True)
|
||||||
|
def validate_host(
|
||||||
|
cls: "HyperglassSystem", value: t.Any, values: t.Dict[str, t.Any]
|
||||||
|
) -> IPvAnyAddress:
|
||||||
|
"""Set default host based on debug mode."""
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
if values["debug"] is False:
|
||||||
|
return ip_address("127.0.0.1")
|
||||||
|
elif values["debug"] is True:
|
||||||
|
return ip_address("0.0.0.0")
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value != "localhost":
|
||||||
|
try:
|
||||||
|
return ip_address(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(str(value))
|
||||||
|
|
||||||
|
elif value == "localhost":
|
||||||
|
return ip_address("127.0.0.1")
|
||||||
|
|
||||||
|
raise ValueError(str(value))
|
||||||
|
|
||||||
|
@validator("redis_dsn", always=True)
|
||||||
|
def validate_redis_dsn(
|
||||||
|
cls: "HyperglassSystem", value: t.Any, values: t.Dict[str, t.Any]
|
||||||
|
) -> RedisDsn:
|
||||||
|
"""Construct a Redis DSN if none is provided."""
|
||||||
|
if value is None:
|
||||||
|
dsn = "redis://{}/{!s}".format(values["redis_host"], values["redis_db"])
|
||||||
|
password = values.get("redis_password")
|
||||||
|
if password is not None:
|
||||||
|
dsn = "redis://:{}@{}/{!s}".format(
|
||||||
|
password.get_secret_value(), values["redis_host"], values["redis_db"],
|
||||||
|
)
|
||||||
|
return dsn
|
||||||
|
return value
|
||||||
|
|
||||||
|
def bind(self: "HyperglassSystem") -> str:
|
||||||
|
"""Format a listen_address. Wraps IPv6 address in brackets."""
|
||||||
|
if self.host.version == 6:
|
||||||
|
return f"[{self.host!s}]:{self.port!s}"
|
||||||
|
return f"{self.host!s}:{self.port!s}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log_level(self: "HyperglassSystem") -> str:
|
||||||
|
"""Get log level as string, inferred from debug mode."""
|
||||||
|
if self.debug:
|
||||||
|
return "DEBUG"
|
||||||
|
return "WARNING"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workers(self: "HyperglassSystem") -> int:
|
||||||
|
"""Get worker count, inferred from debug mode."""
|
||||||
|
if self.debug:
|
||||||
|
return 1
|
||||||
|
return cpu_count(2)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redis(self: "HyperglassSystem") -> t.Dict[str, t.Union[None, int, str]]:
|
||||||
|
"""Get redis parameters as a dict for convenient connection setups."""
|
||||||
|
password = None
|
||||||
|
if self.redis_password is not None:
|
||||||
|
password = self.redis_password.get_secret_value()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"db": self.redis_db,
|
||||||
|
"host": self.redis_host,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def redis_connection_pool(self: "HyperglassSystem") -> t.Dict[str, t.Any]:
|
||||||
|
"""Get Redis ConnectionPool keyword arguments."""
|
||||||
|
return {"url": str(self.redis_dsn), "max_connections": at_least(8, cpu_count(2))}
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
"""Plugin manager definition."""
|
"""Plugin manager definition."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import json
|
import typing as t
|
||||||
import codecs
|
|
||||||
import pickle
|
|
||||||
from typing import TYPE_CHECKING, Any, List, Generic, TypeVar, Callable, Generator
|
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.cache import SyncCache
|
from hyperglass.state.redis import use_state
|
||||||
from hyperglass.configuration import REDIS_CONFIG, params
|
|
||||||
from hyperglass.exceptions.private import PluginError
|
from hyperglass.exceptions.private import PluginError
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
|
@ -18,26 +14,27 @@ from ._base import PluginType, HyperglassPlugin
|
||||||
from ._input import InputPlugin, InputPluginReturn
|
from ._input import InputPlugin, InputPluginReturn
|
||||||
from ._output import OutputType, OutputPlugin
|
from ._output import OutputType, OutputPlugin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
# Project
|
# Project
|
||||||
|
from hyperglass.state.redis import HyperglassState
|
||||||
from hyperglass.models.api.query import Query
|
from hyperglass.models.api.query import Query
|
||||||
from hyperglass.models.config.devices import Device
|
from hyperglass.models.config.devices import Device
|
||||||
from hyperglass.models.commands.generic import Directive
|
from hyperglass.models.commands.generic import Directive
|
||||||
|
|
||||||
PluginT = TypeVar("PluginT")
|
PluginT = t.TypeVar("PluginT", bound=HyperglassPlugin)
|
||||||
|
|
||||||
|
|
||||||
class PluginManager(Generic[PluginT]):
|
class PluginManager(t.Generic[PluginT]):
|
||||||
"""Manage all plugins."""
|
"""Manage all plugins."""
|
||||||
|
|
||||||
_type: PluginType
|
_type: PluginType
|
||||||
_cache: SyncCache
|
_state: "HyperglassState"
|
||||||
_index: int = 0
|
_index: int = 0
|
||||||
_cache_key: str
|
_cache_key: str
|
||||||
|
|
||||||
def __init__(self: "PluginManager") -> None:
|
def __init__(self: "PluginManager") -> None:
|
||||||
"""Initialize plugin manager."""
|
"""Initialize plugin manager."""
|
||||||
self._cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
|
self._state = use_state()
|
||||||
self._cache_key = f"hyperglass.plugins.{self._type}"
|
self._cache_key = f"hyperglass.plugins.{self._type}"
|
||||||
|
|
||||||
def __init_subclass__(cls: "PluginManager", **kwargs: PluginType) -> None:
|
def __init_subclass__(cls: "PluginManager", **kwargs: PluginType) -> None:
|
||||||
|
|
@ -61,20 +58,19 @@ class PluginManager(Generic[PluginT]):
|
||||||
self._index = 0
|
self._index = 0
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|
||||||
def _get_plugins(self: "PluginManager") -> List[PluginT]:
|
|
||||||
"""Retrieve plugins from cache."""
|
|
||||||
cached = self._cache.get(self._cache_key)
|
|
||||||
return list({pickle.loads(codecs.decode(plugin.encode(), "base64")) for plugin in cached})
|
|
||||||
|
|
||||||
def _clear_plugins(self: "PluginManager") -> None:
|
|
||||||
"""Remove all plugins."""
|
|
||||||
self._cache.set(self._cache_key, json.dumps([]))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugins(self: "PluginManager") -> List[PluginT]:
|
def plugins(self: "PluginManager", builtins: bool = True) -> t.List[PluginT]:
|
||||||
"""Get all plugins, with built-in plugins last."""
|
"""Get all plugins, with built-in plugins last."""
|
||||||
|
plugins = self._state.plugins(self._type)
|
||||||
|
if builtins is False:
|
||||||
|
plugins = [p for p in plugins if p.__hyperglass_builtin__ is False]
|
||||||
|
|
||||||
|
# Sort plugins by their name attribute, which is the name of the class by default.
|
||||||
|
sorted_by_name = sorted(plugins, key=lambda p: str(p))
|
||||||
|
|
||||||
|
# Sort with built-in plugins last.
|
||||||
return sorted(
|
return sorted(
|
||||||
self._get_plugins(),
|
sorted_by_name,
|
||||||
key=lambda p: -1 if p.__hyperglass_builtin__ else 1, # flake8: noqa IF100
|
key=lambda p: -1 if p.__hyperglass_builtin__ else 1, # flake8: noqa IF100
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
@ -84,7 +80,7 @@ class PluginManager(Generic[PluginT]):
|
||||||
"""Get this plugin manager's name."""
|
"""Get this plugin manager's name."""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def methods(self: "PluginManager", name: str) -> Generator[Callable, None, None]:
|
def methods(self: "PluginManager", name: str) -> t.Generator[t.Callable, None, None]:
|
||||||
"""Get methods of all registered plugins matching `name`."""
|
"""Get methods of all registered plugins matching `name`."""
|
||||||
for plugin in self.plugins:
|
for plugin in self.plugins:
|
||||||
if hasattr(plugin, name):
|
if hasattr(plugin, name):
|
||||||
|
|
@ -99,39 +95,24 @@ class PluginManager(Generic[PluginT]):
|
||||||
def reset(self: "PluginManager") -> None:
|
def reset(self: "PluginManager") -> None:
|
||||||
"""Remove all plugins."""
|
"""Remove all plugins."""
|
||||||
self._index = 0
|
self._index = 0
|
||||||
self._cache = SyncCache(db=params.cache.database, **REDIS_CONFIG)
|
self._state.reset_plugins(self._type)
|
||||||
return self._clear_plugins()
|
|
||||||
|
|
||||||
def unregister(self: "PluginManager", plugin: PluginT) -> None:
|
def unregister(self: "PluginManager", plugin: PluginT) -> None:
|
||||||
"""Remove a plugin from currently active plugins."""
|
"""Remove a plugin from currently active plugins."""
|
||||||
if isclass(plugin):
|
if isclass(plugin):
|
||||||
if issubclass(plugin, HyperglassPlugin):
|
if issubclass(plugin, HyperglassPlugin):
|
||||||
plugins = {
|
self._state.remove_plugin(self._type, plugin)
|
||||||
# Create a base64 representation of a picked plugin.
|
|
||||||
codecs.encode(pickle.dumps(p), "base64").decode()
|
|
||||||
# Merge current plugins with the new plugin.
|
|
||||||
for p in self._get_plugins()
|
|
||||||
if p != plugin
|
|
||||||
}
|
|
||||||
# Add plugins from cache.
|
|
||||||
self._cache.set(f"hyperglass.plugins.{self._type}", json.dumps(list(plugins)))
|
|
||||||
return
|
return
|
||||||
raise PluginError("Plugin '{}' is not a valid hyperglass plugin", repr(plugin))
|
raise PluginError("Plugin '{}' is not a valid hyperglass plugin", repr(plugin))
|
||||||
|
|
||||||
def register(self: "PluginManager", plugin: PluginT, *args: Any, **kwargs: Any) -> None:
|
def register(self: "PluginManager", plugin: PluginT, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
"""Add a plugin to currently active plugins."""
|
"""Add a plugin to currently active plugins."""
|
||||||
# Create a set of plugins so duplicate plugins are not mistakenly added.
|
# Create a set of plugins so duplicate plugins are not mistakenly added.
|
||||||
try:
|
try:
|
||||||
if issubclass(plugin, HyperglassPlugin):
|
if issubclass(plugin, HyperglassPlugin):
|
||||||
instance = plugin(*args, **kwargs)
|
instance = plugin(*args, **kwargs)
|
||||||
plugins = {
|
self._state.add_plugin(self._type, instance)
|
||||||
# Create a base64 representation of a picked plugin.
|
|
||||||
codecs.encode(pickle.dumps(p), "base64").decode()
|
|
||||||
# Merge current plugins with the new plugin.
|
|
||||||
for p in [*self._get_plugins(), instance]
|
|
||||||
}
|
|
||||||
# Add plugins from cache.
|
|
||||||
self._cache.set(f"hyperglass.plugins.{self._type}", json.dumps(list(plugins)))
|
|
||||||
if instance.__hyperglass_builtin__ is True:
|
if instance.__hyperglass_builtin__ is True:
|
||||||
log.debug("Registered built-in plugin '{}'", instance.name)
|
log.debug("Registered built-in plugin '{}'", instance.name)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ def _register_from_module(module: Any, **kwargs: Any) -> Tuple[str, ...]:
|
||||||
"""Register defined classes from the module."""
|
"""Register defined classes from the module."""
|
||||||
failures = ()
|
failures = ()
|
||||||
defs = getmembers(module, lambda o: _is_class(module, o))
|
defs = getmembers(module, lambda o: _is_class(module, o))
|
||||||
|
sys.modules[module.__name__] = module
|
||||||
for name, plugin in defs:
|
for name, plugin in defs:
|
||||||
if issubclass(plugin, OutputPlugin):
|
if issubclass(plugin, OutputPlugin):
|
||||||
manager = OutputPluginManager()
|
manager = OutputPluginManager()
|
||||||
|
|
@ -55,7 +56,6 @@ def _module_from_file(file: Path) -> Any:
|
||||||
for k, v in _PLUGIN_GLOBALS.items():
|
for k, v in _PLUGIN_GLOBALS.items():
|
||||||
setattr(module, k, v)
|
setattr(module, k, v)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
sys.modules[module.__name__] = module
|
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
17
hyperglass/settings.py
Normal file
17
hyperglass/settings.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Standard Library
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Local
|
||||||
|
from .models.system import HyperglassSystem
|
||||||
|
|
||||||
|
|
||||||
|
def _system_settings() -> "HyperglassSystem":
|
||||||
|
"""Get system settings from local environment."""
|
||||||
|
# Local
|
||||||
|
from .models.system import HyperglassSystem
|
||||||
|
|
||||||
|
return HyperglassSystem()
|
||||||
|
|
||||||
|
|
||||||
|
Settings = _system_settings()
|
||||||
6
hyperglass/state/__init__.py
Normal file
6
hyperglass/state/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""hyperglass global state management."""
|
||||||
|
|
||||||
|
# Local
|
||||||
|
from .redis import use_state
|
||||||
|
|
||||||
|
__all__ = ("use_state",)
|
||||||
133
hyperglass/state/redis.py
Normal file
133
hyperglass/state/redis.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
"""hyperglass global state."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import codecs
|
||||||
|
import pickle
|
||||||
|
import typing as t
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from redis import Redis, ConnectionPool
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.configuration import params, devices, ui_params
|
||||||
|
from hyperglass.exceptions.private import StateError
|
||||||
|
|
||||||
|
# Local
|
||||||
|
from ..settings import Settings
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.ui import UIParameters
|
||||||
|
from hyperglass.models.system import HyperglassSystem
|
||||||
|
from hyperglass.plugins._base import HyperglassPlugin
|
||||||
|
from hyperglass.models.config.params import Params
|
||||||
|
from hyperglass.models.config.devices import Devices
|
||||||
|
|
||||||
|
PluginT = t.TypeVar("PluginT", bound="HyperglassPlugin")
|
||||||
|
|
||||||
|
|
||||||
|
class HyperglassState:
|
||||||
|
"""Global State Manager.
|
||||||
|
|
||||||
|
Maintains configuration objects in Redis cache and accesses them as needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
settings: "HyperglassSystem"
|
||||||
|
redis: Redis
|
||||||
|
_connection_pool: ConnectionPool
|
||||||
|
_namespace: str = "hyperglass.state"
|
||||||
|
|
||||||
|
def __init__(self, *, settings: "HyperglassSystem") -> None:
|
||||||
|
"""Set up Redis connection and add configuration objects."""
|
||||||
|
|
||||||
|
self.settings = settings
|
||||||
|
self._connection_pool = ConnectionPool.from_url(**self.settings.redis_connection_pool)
|
||||||
|
self.redis = Redis(connection_pool=self._connection_pool)
|
||||||
|
|
||||||
|
# Add configuration objects.
|
||||||
|
self.set_object("params", params)
|
||||||
|
self.set_object("devices", devices)
|
||||||
|
self.set_object("ui_params", ui_params)
|
||||||
|
|
||||||
|
# Ensure plugins are empty.
|
||||||
|
self.reset_plugins("output")
|
||||||
|
self.reset_plugins("input")
|
||||||
|
|
||||||
|
def key(self, *keys: str) -> str:
|
||||||
|
"""Format keys with state namespace."""
|
||||||
|
return ".".join((*self._namespace.split("."), *keys))
|
||||||
|
|
||||||
|
def get_object(self, name: str, raise_if_none: bool = False) -> t.Any:
|
||||||
|
"""Get an object (class instance) from the cache."""
|
||||||
|
value = self.redis.get(name)
|
||||||
|
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return pickle.loads(value)
|
||||||
|
elif isinstance(value, str):
|
||||||
|
return pickle.loads(value.encode())
|
||||||
|
if raise_if_none is True:
|
||||||
|
raise StateError("'{key}' does not exist in Redis store", key=name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_object(self, name: str, obj: t.Any) -> None:
|
||||||
|
"""Add an object (class instance) to the cache."""
|
||||||
|
value = pickle.dumps(obj)
|
||||||
|
self.redis.set(self.key(name), value)
|
||||||
|
|
||||||
|
def add_plugin(self, _type: str, plugin: "HyperglassPlugin") -> None:
|
||||||
|
"""Add a plugin to its list by type."""
|
||||||
|
current = self.plugins(_type)
|
||||||
|
plugins = {
|
||||||
|
# Create a base64 representation of a picked plugin.
|
||||||
|
codecs.encode(pickle.dumps(p), "base64").decode()
|
||||||
|
# Merge current plugins with the new plugin.
|
||||||
|
for p in [*current, plugin]
|
||||||
|
}
|
||||||
|
self.set_object(self.key("plugins", _type), list(plugins))
|
||||||
|
|
||||||
|
def remove_plugin(self, _type: str, plugin: "HyperglassPlugin") -> None:
|
||||||
|
"""Remove a plugin from its list by type."""
|
||||||
|
current = self.plugins(_type)
|
||||||
|
plugins = {
|
||||||
|
# Create a base64 representation of a picked plugin.
|
||||||
|
codecs.encode(pickle.dumps(p), "base64").decode()
|
||||||
|
# Merge current plugins with the new plugin.
|
||||||
|
for p in current
|
||||||
|
if p != plugin
|
||||||
|
}
|
||||||
|
self.set_object(self.key("plugins", _type), list(plugins))
|
||||||
|
|
||||||
|
def reset_plugins(self, _type: str) -> None:
|
||||||
|
"""Remove all plugins of `_type`."""
|
||||||
|
self.set_object(self.key("plugins", _type), [])
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Delete all cache keys."""
|
||||||
|
self.redis.flushdb(asynchronous=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params(self) -> "Params":
|
||||||
|
"""Get hyperglass configuration parameters (`hyperglass.yaml`)."""
|
||||||
|
return self.get_object(self.key("params"), raise_if_none=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices(self) -> "Devices":
|
||||||
|
"""Get hyperglass devices (`devices.yaml`)."""
|
||||||
|
return self.get_object(self.key("devices"), raise_if_none=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui_params(self) -> "UIParameters":
|
||||||
|
"""UI parameters, built from params."""
|
||||||
|
return self.get_object(self.key("ui_params"), raise_if_none=True)
|
||||||
|
|
||||||
|
def plugins(self, _type: str) -> t.List[PluginT]:
|
||||||
|
"""Get plugins by type."""
|
||||||
|
current = self.get_object(self.key("plugins", _type), raise_if_none=False) or []
|
||||||
|
return list({pickle.loads(codecs.decode(plugin.encode(), "base64")) for plugin in current})
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def use_state() -> "HyperglassState":
|
||||||
|
"""Access hyperglass global state."""
|
||||||
|
return HyperglassState(settings=Settings)
|
||||||
|
|
@ -146,23 +146,6 @@ to access the following directories:
|
||||||
return matched_path
|
return matched_path
|
||||||
|
|
||||||
|
|
||||||
def format_listen_address(listen_address: Union[IPv4Address, IPv6Address, str]) -> str:
|
|
||||||
"""Format a listen_address. Wraps IPv6 address in brackets."""
|
|
||||||
fmt = str(listen_address)
|
|
||||||
|
|
||||||
if isinstance(listen_address, str):
|
|
||||||
try:
|
|
||||||
listen_address = ip_address(listen_address)
|
|
||||||
except ValueError as err:
|
|
||||||
log.error(err)
|
|
||||||
pass
|
|
||||||
|
|
||||||
if isinstance(listen_address, (IPv4Address, IPv6Address)) and listen_address.version == 6:
|
|
||||||
fmt = f"[{str(listen_address)}]"
|
|
||||||
|
|
||||||
return fmt
|
|
||||||
|
|
||||||
|
|
||||||
def split_on_uppercase(s):
|
def split_on_uppercase(s):
|
||||||
"""Split characters by uppercase letters.
|
"""Split characters by uppercase letters.
|
||||||
|
|
||||||
|
|
@ -363,3 +346,10 @@ def deep_convert_keys(_dict: Type[DeepConvert], predicate: Callable[[str], str])
|
||||||
converted[predicate(key)] = get_value(value)
|
converted[predicate(key)] = get_value(value)
|
||||||
|
|
||||||
return converted
|
return converted
|
||||||
|
|
||||||
|
|
||||||
|
def at_least(minimum: int, value: int,) -> int:
|
||||||
|
"""Get a number value that is at least a specified minimum."""
|
||||||
|
if value < minimum:
|
||||||
|
return minimum
|
||||||
|
return value
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,23 @@ import os
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import shutil
|
import shutil
|
||||||
|
import typing as t
|
||||||
import asyncio
|
import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Dict, Tuple, Optional
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.models.ui import UIParameters
|
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .files import copyfiles, check_path
|
from .files import copyfiles, check_path
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Project
|
||||||
|
from hyperglass.models.ui import UIParameters
|
||||||
|
|
||||||
def get_node_version() -> Tuple[int, int, int]:
|
|
||||||
|
def get_node_version() -> t.Tuple[int, int, int]:
|
||||||
"""Get the system's NodeJS version."""
|
"""Get the system's NodeJS version."""
|
||||||
node_path = shutil.which("node")
|
node_path = shutil.which("node")
|
||||||
|
|
||||||
|
|
@ -30,7 +33,7 @@ def get_node_version() -> Tuple[int, int, int]:
|
||||||
return tuple((int(v) for v in version.split(".")))
|
return tuple((int(v) for v in version.split(".")))
|
||||||
|
|
||||||
|
|
||||||
def get_ui_build_timeout() -> Optional[int]:
|
def get_ui_build_timeout() -> t.Optional[int]:
|
||||||
"""Read the UI build timeout from environment variables or set a default."""
|
"""Read the UI build timeout from environment variables or set a default."""
|
||||||
timeout = None
|
timeout = None
|
||||||
|
|
||||||
|
|
@ -60,7 +63,7 @@ async def check_node_modules() -> bool:
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
|
|
||||||
async def read_package_json() -> Dict:
|
async def read_package_json() -> t.Dict[str, t.Any]:
|
||||||
"""Import package.json as a python dict."""
|
"""Import package.json as a python dict."""
|
||||||
|
|
||||||
package_json_file = Path(__file__).parent.parent / "ui" / "package.json"
|
package_json_file = Path(__file__).parent.parent / "ui" / "package.json"
|
||||||
|
|
@ -114,7 +117,7 @@ async def node_initial(timeout: int = 180, dev_mode: bool = False) -> str:
|
||||||
return "\n".join(all_messages)
|
return "\n".join(all_messages)
|
||||||
|
|
||||||
|
|
||||||
async def build_ui(app_path):
|
async def build_ui(app_path: Path):
|
||||||
"""Execute `next build` & `next export` from UI directory.
|
"""Execute `next build` & `next export` from UI directory.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
|
@ -216,7 +219,7 @@ def generate_opengraph(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def migrate_images(app_path: Path, params: UIParameters):
|
def migrate_images(app_path: Path, params: "UIParameters"):
|
||||||
"""Migrate images from source code to install directory."""
|
"""Migrate images from source code to install directory."""
|
||||||
images_dir = app_path / "static" / "images"
|
images_dir = app_path / "static" / "images"
|
||||||
favicon_dir = images_dir / "favicons"
|
favicon_dir = images_dir / "favicons"
|
||||||
|
|
@ -236,7 +239,7 @@ async def build_frontend( # noqa: C901
|
||||||
dev_mode: bool,
|
dev_mode: bool,
|
||||||
dev_url: str,
|
dev_url: str,
|
||||||
prod_url: str,
|
prod_url: str,
|
||||||
params: UIParameters,
|
params: "UIParameters",
|
||||||
app_path: Path,
|
app_path: Path,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
timeout: int = 180,
|
timeout: int = 180,
|
||||||
|
|
@ -264,8 +267,6 @@ async def build_frontend( # noqa: C901
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.constants import __version__
|
from hyperglass.constants import __version__
|
||||||
|
|
||||||
log.info("Starting UI build")
|
|
||||||
|
|
||||||
# Create temporary file. json file extension is added for easy
|
# Create temporary file. json file extension is added for easy
|
||||||
# webpack JSON parsing.
|
# webpack JSON parsing.
|
||||||
env_file = Path("/tmp/hyperglass.env.json") # noqa: S108
|
env_file = Path("/tmp/hyperglass.env.json") # noqa: S108
|
||||||
|
|
@ -344,6 +345,7 @@ async def build_frontend( # noqa: C901
|
||||||
|
|
||||||
# Initiate Next.JS export process.
|
# Initiate Next.JS export process.
|
||||||
if any((not dev_mode, force, full)):
|
if any((not dev_mode, force, full)):
|
||||||
|
log.info("Starting UI build")
|
||||||
initialize_result = await node_initial(timeout, dev_mode)
|
initialize_result = await node_initial(timeout, dev_mode)
|
||||||
build_result = await build_ui(app_path=app_path)
|
build_result = await build_ui(app_path=app_path)
|
||||||
|
|
||||||
|
|
|
||||||
34
poetry.lock
generated
34
poetry.lock
generated
|
|
@ -6,6 +6,21 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aioredis"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "asyncio (PEP 3156) Redis support"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
async-timeout = "*"
|
||||||
|
typing-extensions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
hiredis = ["hiredis (>=1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansicon"
|
name = "ansicon"
|
||||||
version = "1.89.0"
|
version = "1.89.0"
|
||||||
|
|
@ -41,6 +56,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-timeout"
|
||||||
|
version = "3.0.1"
|
||||||
|
description = "Timeout context manager for asyncio programs"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asyncssh"
|
name = "asyncssh"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
|
|
@ -880,6 +903,7 @@ optional = false
|
||||||
python-versions = ">=3.6.1"
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""}
|
||||||
typing-extensions = ">=3.7.4.3"
|
typing-extensions = ">=3.7.4.3"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
|
@ -1398,13 +1422,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.8.1,<4.0"
|
python-versions = ">=3.8.1,<4.0"
|
||||||
content-hash = "b99fec86745b99f5b0c132dadf90e07f8529aa751c8fbd582c36d6b82cd79dd3"
|
content-hash = "34e21443d0af22b763bd715875da90ca519cde388af0e54b4d9a71180b14ca13"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
{file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"},
|
{file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"},
|
||||||
{file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"},
|
{file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"},
|
||||||
]
|
]
|
||||||
|
aioredis = [
|
||||||
|
{file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"},
|
||||||
|
{file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"},
|
||||||
|
]
|
||||||
ansicon = [
|
ansicon = [
|
||||||
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
|
{file = "ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec"},
|
||||||
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
|
{file = "ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1"},
|
||||||
|
|
@ -1420,6 +1448,10 @@ aredis = [
|
||||||
{file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"},
|
{file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"},
|
||||||
{file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"},
|
{file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"},
|
||||||
]
|
]
|
||||||
|
async-timeout = [
|
||||||
|
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
||||||
|
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
|
||||||
|
]
|
||||||
asyncssh = [
|
asyncssh = [
|
||||||
{file = "asyncssh-2.7.0-py3-none-any.whl", hash = "sha256:ccc62a1b311c71d4bf8e4bc3ac141eb00ebb28b324e375aed1d0a03232893ca1"},
|
{file = "asyncssh-2.7.0-py3-none-any.whl", hash = "sha256:ccc62a1b311c71d4bf8e4bc3ac141eb00ebb28b324e375aed1d0a03232893ca1"},
|
||||||
{file = "asyncssh-2.7.0.tar.gz", hash = "sha256:185013d8e67747c3c0f01b72416b8bd78417da1df48c71f76da53c607ef541b6"},
|
{file = "asyncssh-2.7.0.tar.gz", hash = "sha256:185013d8e67747c3c0f01b72416b8bd78417da1df48c71f76da53c607ef541b6"},
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ netmiko = "^3.4.0"
|
||||||
paramiko = "^2.7.2"
|
paramiko = "^2.7.2"
|
||||||
psutil = "^5.7.2"
|
psutil = "^5.7.2"
|
||||||
py-cpuinfo = "^7.0.0"
|
py-cpuinfo = "^7.0.0"
|
||||||
pydantic = "1.8.2"
|
pydantic = {extras = ["dotenv"], version = "^1.8.2"}
|
||||||
python = ">=3.8.1,<4.0"
|
python = ">=3.8.1,<4.0"
|
||||||
redis = "^3.5.3"
|
redis = "^3.5.3"
|
||||||
scrapli = {version = "2021.07.30", extras = ["asyncssh"]}
|
scrapli = {version = "2021.07.30", extras = ["asyncssh"]}
|
||||||
|
|
@ -55,6 +55,7 @@ typing-extensions = "^3.7.4"
|
||||||
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
uvicorn = {extras = ["standard"], version = "^0.13.4"}
|
||||||
uvloop = "^0.14.0"
|
uvloop = "^0.14.0"
|
||||||
xmltodict = "^0.12.0"
|
xmltodict = "^0.12.0"
|
||||||
|
aioredis = "^2.0.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
bandit = "^1.6.2"
|
bandit = "^1.6.2"
|
||||||
|
|
@ -97,7 +98,7 @@ reportMissingTypeStubs = true
|
||||||
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
|
check = {cmd = "task lint && task ui-lint", help = "Run all lint checks"}
|
||||||
lint = {cmd = "flake8 hyperglass", help = "Run Flake8"}
|
lint = {cmd = "flake8 hyperglass", help = "Run Flake8"}
|
||||||
sort = {cmd = "isort hyperglass", help = "Run iSort"}
|
sort = {cmd = "isort hyperglass", help = "Run iSort"}
|
||||||
start = {cmd = "python3 -m hyperglass.console start", help = "Start hyperglass"}
|
start = {cmd = "python3 -m hyperglass.main", help = "Start hyperglass"}
|
||||||
start-asgi = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
|
start-asgi = {cmd = "uvicorn hyperglass.api:app", help = "Start hyperglass via Uvicorn"}
|
||||||
test = {cmd = "pytest hyperglass", help = "Run hyperglass tests"}
|
test = {cmd = "pytest hyperglass", help = "Run hyperglass tests"}
|
||||||
ui-build = {cmd = "python3 -m hyperglass.console build-ui", help = "Run a UI Build"}
|
ui-build = {cmd = "python3 -m hyperglass.console build-ui", help = "Run a UI Build"}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue