With custom validators, you can implement business logic in Python. Schema-enforcer will automatically
load your plugins from the validator_directory
and run them against your host data.
The validator plugin provides a few base classes: BaseValidation, JmesPathModelValidation, and PydanticValidation. BaseValidation can be used when you want to implement all logic, JmesPathModelValidation can be used as a shortcut for jmespath validation, and PydanticValidation will validate data against a specific Pydantic model.
Use this class to implement arbitrary validation logic in Python. In order to work correctly, your Python script must meet the following criteria:
-
Exist in the
validator_directory
dir. -
Include a subclass of the BaseValidation class to correctly register with schema-enforcer.
-
Ensure you call
super().__init__()
in your class__init__
if you override. -
Provide a class method in your subclass with the following signature:
def validate(data: dict, strict: bool):
- Data is a dictionary of variables on a per-host basis.
- Strict is set to true when the strict flag is set via the CLI. You can use this to offer strict validation behavior or ignore it if not needed.
The name of your class will be used as the schema-id for mapping purposes. You can override the default schema ID
by providing a class-level id
variable.
Helper functions are provided to add pass/fail results:
def add_validation_error(self, message: str, **kwargs):
"""Add validator error to results.
Args:
message (str): error message
kwargs (optional): additional arguments to add to ValidationResult when required
"""
def add_validation_pass(self, **kwargs):
"""Add validator pass to results.
Args:
kwargs (optional): additional arguments to add to ValidationResult when required
"""
In most cases, you will not need to provide kwargs. However, if you find a use case that requires updating other fields in the ValidationResult, you can send the key/value pairs to update the result directly. This is for advanced users only.
Use this class for basic validation using jmespath expressions to query specific values in your data. In order to work correctly, your Python script must meet the following criteria:
-
Exist in the
validator_directory
dir. -
Include a subclass of the JmesPathModelValidation class to correctly register with schema-enforcer.
-
Provide the following class level variables:
top_level_properties
: Field for mapping of validator to dataid
: Schema ID to use for reporting purposes (optional - defaults to class name)left
: Jmespath expression to query your host dataright
: Value or a compiled jmespath expressionoperator
: Operator to use for comparison between left and right hand side of expressionerror
: Message to report when validation fails
The class provides the following operators for basic use cases:
"gt": int(left) > int(right),
"gte": int(left) >= int(right),
"eq": left == right,
"lt": int(left) < int(right),
"lte": int(left) <= int(right),
"contains": right in left,
If you require additional logic or need to compare other types, use the BaseValidation class and create your own validate method.
from schema_enforcer.schemas.validator import JmesPathModelValidation
class CheckInterface(JmesPathModelValidation): # pylint: disable=too-few-public-methods
top_level_properties = ["interfaces"]
id = "CheckInterface" # pylint: disable=invalid-name
left = "interfaces.*[@.type=='core'][] | length([?@])"
right = 2
operator = "gte"
error = "Less than two core interfaces"
import jmespath
from schema_enforcer.schemas.validator import JmesPathModelValidation
class CheckInterfaceIPv4(JmesPathModelValidation): # pylint: disable=too-few-public-methods
top_level_properties = ["interfaces"]
id = "CheckInterfaceIPv4" # pylint: disable=invalid-name
left = "interfaces.*[@.type=='core'][] | length([?@])"
right = jmespath.compile("interfaces.* | length([[email protected]=='core'][].ipv4)")
operator = "eq"
error = "All core interfaces do not have IPv4 addresses"
Schema Enforcer supports utilizing Pydantic models for validation. Pydantic models can be loaded two different ways.
- Store your models in your
validator_directory
. - Load from a separate library using the
schema_enforcer.schemas.PydanticManager
. These must be defined within theschema_enforcer
configuration file.pydantic_validators
requires a list of library paths to aPydanticManager
instance.
Both methods will replace the Pydantic BaseModel
with the PydanticValidation
class that provides the required validate
method that uses the model_validate
method to validate data. The model is set to the original Pydantic model to validate data against.
class PydanticValidation(BaseValidation):
"""Basic wrapper for Pydantic models to be used as validators."""
model: BaseModel
def validate(self, data: dict, strict: bool = False):
"""Validate data against Pydantic model.
Args:
data (dict): variables to be validated by validator
strict (bool): true when --strict cli option is used to request strict validation (if provided)
Returns:
None
Use add_validation_error and add_validation_pass to report results.
"""
try:
self.model.model_validate(data, strict=strict)
self.add_validation_pass()
except ValidationError as err:
self.add_validation_error(str(err))
The Pydantic models can be located in any Python file within this directory (new or existing). The only requirement is these are valid Pydantic BaseModel
subclasses.
These will be loaded and can be referenced by their class name. For example, CheckHostname
will show up as CheckHostname
.
"""Validate hostname is valid."""
from pydantic import BaseModel
class CheckHostname(BaseModel):
"""Validate hostname is valid."""
hostname: str
# jsonschema: Hostname
---
hostname: "az-phx-rtr01"
As an example, we will look at models that are within our my_custom_pydantic_models.manager
. If a prefix is defined, you can reference the validators like f"{prefix}/{model.__name__}
.
"""Load our models to be used for Schema Enforcer."""
from pydantic import BaseModel
from schema_enforcer.schemas.manager import PydanticManager
class Hostname(BaseModel):
hostname: str = Field(pattern="^[a-z]{2}-[a-z]{3}-[a-z]{1,2}[0-9]{2}$")
# Prefix is optional and will default to a blank string, aka no prefix.
# Models is required to pass in custom Pydantic models.
manager = PydanticManager(prefix="custom", models=[Hostname])
[tool.schema_enforcer]
pydantic_validators = [
"my_custom_pydantic_models.manager"
]
An example YAML file schema correlation would look like:
# jsonschema: custom/Hostname
---
hostname: "az-phx-pe01"
Custom validators are run with schema-enforcer validate
and schema-enforcer ansible
commands.
You map validators to keys in your data with top_level_properties
in your subclass or with schema_enforcer_schema_ids
in your data. Schema-enforcer uses the same process to map custom validators and schemas. Refer to the "Mapping Schemas" documentation
for more details.
The CheckInterface validator has a top_level_properties of "interfaces":
class CheckInterface(JmesPathModelValidation): # pylint: disable=too-few-public-methods
top_level_properties = ["interfaces"]
With automapping enabled, this validator will apply to any host with a top-level interfaces
key in the Ansible host_vars data:
---
hostname: "az-phx-pe01"
pair_rtr: "az-phx-pe02"
interfaces:
MgmtEth0/0/CPU0/0:
ipv4: "172.16.1.1"
Loopback0:
ipv4: "192.168.1.1"
ipv6: "2001:db8:1::1"
GigabitEthernet0/0/0/0:
ipv4: "10.1.0.1"
ipv6: "2001:db8::"
peer: "az-phx-pe02"
peer_int: "GigabitEthernet0/0/0/0"
type: "core"
GigabitEthernet0/0/0/1:
ipv4: "10.1.0.37"
ipv6: "2001:db8::12"
peer: "co-den-p01"
peer_int: "GigabitEthernet0/0/0/2"
type: "core"
Alternatively, you can manually map a validator in your Ansible host vars or other data files.
schema_enforcer_automap_default: false
schema_enforcer_schema_ids:
- "CheckInterface"