2020-01-16 02:52:00 -07:00
|
|
|
"""Input query validation model."""
|
|
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
# Standard Library
|
2021-12-08 23:20:30 -07:00
|
|
|
import typing as t
|
2020-01-26 02:15:19 -07:00
|
|
|
import hashlib
|
2020-04-18 07:58:46 -07:00
|
|
|
import secrets
|
2021-09-07 22:58:39 -07:00
|
|
|
from datetime import datetime
|
2020-01-16 02:52:00 -07:00
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
# Third Party
|
2022-12-11 17:27:53 -05:00
|
|
|
from pydantic import BaseModel, constr, validator
|
2020-01-16 02:52:00 -07:00
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
# Project
|
2021-09-07 22:58:39 -07:00
|
|
|
from hyperglass.log import log
|
2021-09-16 15:35:12 -07:00
|
|
|
from hyperglass.util import snake_to_camel, repr_from_attrs
|
2021-09-15 18:25:37 -07:00
|
|
|
from hyperglass.state import use_state
|
2021-09-26 11:39:46 -07:00
|
|
|
from hyperglass.plugins import InputPluginManager
|
2021-12-22 22:22:01 -07:00
|
|
|
from hyperglass.exceptions.public import InputInvalid, QueryTypeNotFound, QueryLocationNotFound
|
2021-09-07 22:58:39 -07:00
|
|
|
from hyperglass.exceptions.private import InputValidationError
|
2020-10-05 12:07:34 -07:00
|
|
|
|
2020-10-11 13:14:57 -07:00
|
|
|
# Local
|
2021-09-07 22:58:39 -07:00
|
|
|
from ..config.devices import Device
|
2020-01-16 02:52:00 -07:00
|
|
|
|
2021-09-16 13:46:50 -07:00
|
|
|
(TEXT := use_state("params").web.text)
|
2021-06-23 19:11:59 -07:00
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
QueryLocation = constr(strip_whitespace=True, strict=True, min_length=1)
|
2021-12-23 00:00:35 -07:00
|
|
|
QueryTarget = constr(strip_whitespace=True, min_length=1)
|
2022-12-11 17:27:53 -05:00
|
|
|
QueryType = constr(strip_whitespace=True, strict=True, min_length=1)
|
2021-12-23 00:00:35 -07:00
|
|
|
|
2021-06-23 19:11:59 -07:00
|
|
|
|
2020-01-16 02:52:00 -07:00
|
|
|
class Query(BaseModel):
|
|
|
|
|
"""Validation model for input query parameters."""
|
|
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
query_location: QueryLocation # Device `name` field
|
2021-12-23 00:00:35 -07:00
|
|
|
query_target: t.Union[t.List[QueryTarget], QueryTarget]
|
2022-12-11 17:27:53 -05:00
|
|
|
query_type: QueryType # Directive `id` field
|
2020-01-26 02:15:19 -07:00
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
class Config:
|
|
|
|
|
"""Pydantic model configuration."""
|
|
|
|
|
|
2020-04-18 07:58:46 -07:00
|
|
|
extra = "allow"
|
2021-09-07 22:58:39 -07:00
|
|
|
alias_generator = snake_to_camel
|
2022-12-11 17:27:53 -05:00
|
|
|
allow_population_by_field_name = True
|
|
|
|
|
|
|
|
|
|
def __init__(self, **data) -> None:
|
2020-04-18 07:58:46 -07:00
|
|
|
"""Initialize the query with a UTC timestamp at initialization time."""
|
2022-12-11 17:27:53 -05:00
|
|
|
super().__init__(**data)
|
2020-04-18 07:58:46 -07:00
|
|
|
self.timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
2022-12-11 17:27:53 -05:00
|
|
|
|
2021-09-15 18:25:37 -07:00
|
|
|
state = use_state()
|
2021-09-16 13:59:39 -07:00
|
|
|
self._state = state
|
2022-12-11 17:27:53 -05:00
|
|
|
|
2021-09-21 07:54:16 -07:00
|
|
|
query_directives = self.device.directives.matching(self.query_type)
|
2022-12-11 17:27:53 -05:00
|
|
|
|
2021-09-21 07:54:16 -07:00
|
|
|
if len(query_directives) < 1:
|
|
|
|
|
raise QueryTypeNotFound(query_type=self.query_type)
|
2022-12-11 17:27:53 -05:00
|
|
|
|
2021-09-21 07:54:16 -07:00
|
|
|
self.directive = query_directives[0]
|
2022-12-11 17:27:53 -05:00
|
|
|
|
2021-09-07 22:58:39 -07:00
|
|
|
try:
|
|
|
|
|
self.validate_query_target()
|
|
|
|
|
except InputValidationError as err:
|
|
|
|
|
raise InputInvalid(**err.kwargs)
|
2020-04-18 07:58:46 -07:00
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
def __repr__(self) -> str:
|
2020-04-18 07:58:46 -07:00
|
|
|
"""Represent only the query fields."""
|
2021-09-16 15:35:12 -07:00
|
|
|
return repr_from_attrs(self, self.__config__.fields.keys())
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
"""Alias __str__ to __repr__."""
|
|
|
|
|
return repr(self)
|
2020-04-18 07:58:46 -07:00
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
def digest(self) -> str:
|
2020-01-26 02:15:19 -07:00
|
|
|
"""Create SHA256 hash digest of model representation."""
|
|
|
|
|
return hashlib.sha256(repr(self).encode()).hexdigest()
|
2020-01-16 02:52:00 -07:00
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
def random(self) -> str:
|
2020-04-18 07:58:46 -07:00
|
|
|
"""Create a random string to prevent client or proxy caching."""
|
|
|
|
|
return hashlib.sha256(
|
|
|
|
|
secrets.token_bytes(8) + repr(self).encode() + secrets.token_bytes(8)
|
|
|
|
|
).hexdigest()
|
|
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
def validate_query_target(self) -> None:
|
2021-09-07 22:58:39 -07:00
|
|
|
"""Validate a query target after all fields/relationships havebeen initialized."""
|
2021-09-26 11:39:46 -07:00
|
|
|
# Run config/rule-based validations.
|
2021-09-07 22:58:39 -07:00
|
|
|
self.directive.validate_target(self.query_target)
|
2021-09-26 11:39:46 -07:00
|
|
|
# Run plugin-based validations.
|
|
|
|
|
manager = InputPluginManager()
|
|
|
|
|
manager.execute(query=self)
|
2021-09-16 15:35:12 -07:00
|
|
|
log.debug("Validation passed for query {!r}", self)
|
2021-09-07 22:58:39 -07:00
|
|
|
|
2022-12-11 17:27:53 -05:00
|
|
|
def dict(self) -> t.Dict[str, t.Union[t.List[str], str]]:
|
2021-12-08 23:20:30 -07:00
|
|
|
"""Include only public fields."""
|
|
|
|
|
return super().dict(include={"query_location", "query_target", "query_type"})
|
|
|
|
|
|
2020-05-29 17:47:53 -07:00
|
|
|
@property
|
2021-09-07 22:58:39 -07:00
|
|
|
def device(self) -> Device:
|
2020-05-29 17:50:30 -07:00
|
|
|
"""Get this query's device object by query_location."""
|
2021-09-16 13:59:39 -07:00
|
|
|
return self._state.devices[self.query_location]
|
2020-07-30 01:30:01 -07:00
|
|
|
|
2020-02-03 02:34:50 -07:00
|
|
|
@validator("query_location")
|
2020-01-16 02:52:00 -07:00
|
|
|
def validate_query_location(cls, value):
|
2021-02-25 23:38:57 -07:00
|
|
|
"""Ensure query_location is defined."""
|
2021-05-29 21:48:59 -07:00
|
|
|
|
2021-09-16 13:46:50 -07:00
|
|
|
devices = use_state("devices")
|
2021-05-29 21:48:59 -07:00
|
|
|
|
2021-09-24 00:16:26 -07:00
|
|
|
if not devices.valid_id_or_name(value):
|
2021-09-07 22:58:39 -07:00
|
|
|
raise QueryLocationNotFound(location=value)
|
2021-06-23 19:11:59 -07:00
|
|
|
|
2021-09-07 22:58:39 -07:00
|
|
|
return value
|
2022-12-11 17:27:53 -05:00
|
|
|
|
|
|
|
|
@validator("query_type")
|
|
|
|
|
def validate_query_type(cls, value: t.Any):
|
|
|
|
|
"""Ensure a requested query type exists."""
|
|
|
|
|
devices = use_state("devices")
|
|
|
|
|
if any((device.has_directives(value) for device in devices)):
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
raise QueryTypeNotFound(query_type=value)
|