Schema Enforcer Custom Validators

Blog Detail

We’re excited to introduce support for custom validators in Schema Enforcer. Schema Enforcer provides a framework for testing structured data against schema definitions using JSON Schema and now using custom Python validators. You can check out Introducing Schema Enforcer for more background and an introduction to Schema Enforcer.

What Is a Custom Validator?

A custom validator is a Python module that allows you to run any logic against your data on a per-host basis.

Let’s start with an example. What if you want to validate that every edge router has at least two core interfaces defined?

Here’s a possible way we could model our data in an Ansible host_var file:

---
hostname: "az-phx-pe01"
pair_rtr: "az-phx-pe02"
upstreams: []
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"

In this example, each physical interface has a type key, which we can evaluate in our custom validator. JSON Schema can be used to validate that this field exists and contains a desired value (e.g., “core”, “access”, etc.). However, it cannot check whether there are at least two interfaces with this key set to “core”.

JMESPath Custom Validators

As a shortcut for basic use cases, Schema Enforcer provides the JmesPathModelValidation class. This class supports using JMESPath queries against your data along with generic comparison operators. The logic is provided by the base class, so no Python is required beyond setting a few variables.

To solve the preceding example, we can use the following custom validator:

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"

The top_level_properties variable maps this validator to the interfaces object in our data. The real work is done by the leftright, and operator variables. Think of these as part of an expression:

{left} {operator} {right}

Or for our example:

"interfaces.*[@.type=='core'][] | length([?@])" gte 2

This custom validator uses the JMESPath expression to query the data. The query returns all interfaces that have type of “core”. The output is piped to a built-in JMESPath function that gives us the length of the return value. When applied to our example data, the value of the query is 2. When checked by our custom validator, this host will pass, as the value of the query is greater than or equal to 2.

root@b295daf33db5:/local/examples/ansible3# schema-enforcer ansible --show-checks
Found 2 hosts in the inventory
Ansible Host              Schema ID
--------------------------------------------------------------------------------
az_phx_pe01               ['CheckInterface']
az_phx_pe02               ['CheckInterface']

In the preceding output, we see the CheckInterface validator is applied to two hosts.

When Schema Enforcer is run against the inventory, the output shows if any hosts fail the validation. If a host fails, the error message defined in the CheckInterface class error variable will be shown.

root@b295daf33db5:/local/examples/ansible3# schema-enforcer ansible
Found 2 hosts in the inventory
FAIL | [ERROR] Less than two core interfaces [HOST] az_phx_pe02 [PROPERTY]
root@b295daf33db5:/local/examples/ansible3#

Advanced Use Cases

For more advanced use cases, Schema Enforcer provides the BaseValidation class which can be used to build your own complex validation classes. BaseValidation provides two helper functions for reporting pass/fail: add_validation_pass and add_validation_error. Schema Enforcer will automatically call the validate method of your custom class for all instances of your data. The logic as to whether a validator passes or fails is up to your implementation.

Since we can run arbitrary logic against the data using Python, one possible use case is to check data against some external service. In the example below, a simple BGP peer data file is checked against the ARIN database to validate that the name is correct.

Sample Data

---
bgp_peers:
  - asn: 6939
    name: "Hurricane Electric LLC"
  - asn: 701
    name: "VZW"
  - asn: 100000
    name: "Private"

Validator

"""Custom validator for BGP peer information."""
import requests

from schema_enforcer.schemas.validator import BaseValidation


class CheckARIN(BaseValidation):
    """Verify that BGP peer name matches ARIN ASN information."""

    def validate(self, data, strict):
        """Validate BGP peers for each host."""
        headers = {"Accept": "application/json"}
        for peer in data["bgp_peers"]:
            # pylint: disable=invalid-name
            r = requests.get(f"http://whois.arin.net/rest/asn/{peer['asn']}", headers=headers)
            if r.status_code != requests.codes.ok:  # pylint: disable=no-member
                self.add_validation_error(f"ARIN lookup failed for peer {peer['name']} with ASN {peer['asn']}")
                continue
            arin_info = r.json()
            arin_name = arin_info["asn"]["orgRef"]["@name"]
            if peer["name"] != arin_name:
                self.add_validation_error(
                    f"Peer name {peer['name']} for ASN {peer['asn']} does not match ARIN database: {arin_name}"
                )
            else:
                self.add_validation_pass()

If we run Schema Enforcer with this validator, we get the following output:

root@da72aae39ede:/local/examples/example4# schema-enforcer validate --show-checks
Structured Data File                               Schema ID
--------------------------------------------------------------------------------
./bgp/peers.yml                                    ['CheckARIN']
root@da72aae39ede:/local/examples/example4# schema-enforcer validate
FAIL | [ERROR] Peer name VZW for ASN 701 does not match ARIN database: MCI Communications Services, Inc. d/b/a Verizon Business [FILE] ./bgp/peers.yml [PROPERTY]
FAIL | [ERROR] ARIN lookup failed for peer Private with ASN 100000 [FILE] ./bgp/peers.yml [PROPERTY]

You could expand this example to do other validation, such as checking that the ASN is valid before making the request to ARIN.

For more information on this Schema Enforcer feature, see the docs. And if you have any interesting use cases, please let us know!



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Using Schema Enforcer with Docker and CI

Blog Detail

Recently Network to Code open sourced schema-enforcer, and immediately my mind turned to integrating this tool in with CI pipelines. The goal is to have fast, repeatable, and reusable pipelines that ensure the integrity of the data stored in Git repositories. We will be accomplishing repeatability and reusability by packaging schema-enforcer with Docker and publishing to a common Docker registry.

Why integrate with CI pipelines?

By integrating repositories containing structured data with a CI pipeline that enforces schema you are better able to predict the repeatability of the downstream automation that consumes the structured data. This is critical when using the data as a source of truth for automation to consume. It also helps to react faster to an incorrect schema before this is used by a configuration tool, such as Ansible. Imagine being able to empower other teams to make chages to data repositories and trust the automation is performing the checks an engineer manually does today.

How containers can speed up CI execution.

Containers can be a catalyst to speeding up the process of CI execution for the following reasons:

  1. Having purpose built containers in CI allows for standardized pipelines with little setup time.
  2. Sourcing from a pre built image to execute a single test command removes the need to build an image or manage a virtual environment per repository.
  3. Reducing build times from using pre-built images allows for faster running pipeline and helps to shorten the feedback loop to the end user.

Example with privately hosted GitLab.

For today’s example I am using my locally hosted GitLab and Docker Registry. This was done to showcase the power of building internal resources that can be easily integrated with on-premise solutions. This example could easily be adapted to run in GitHub & Travis CI with the same level of effectiveness and speed of execution.

Building a container to use in CI.

Click Here for documentation on Dockerfile construction and docker build commands. The Dockerfile is starting with python:3.8 as a base. We then set the working directory, install schema-enforcer, and lastly setting the default entrypoint and command for the container image.

Dockerfile

FROM python:3.8

WORKDIR /usr/src/app

RUN python -m pip install schema-enforcer

ENTRYPOINT ["schema-enforcer"]

CMD ["validate", "--show-pass"]

Publishing schema-enforcer container to private registry.

Click Here for documentation on hosting a private docker registry. If using Docker Hub, the image tag would change to <namespace>/<container name>:tag. If I was to push this to my personal Docker Hub namespace, the image would be whitej6/schema-enforcer:latest.

docker build -t registry.whitej6.com/ntc/docker/schema-enforcer:latest .

docker push registry.whitej6.com/ntc/docker/schema-enforcer:latest

Integrating GitLab Runner with data repo.

For the first use case, we are starting with example1 in the schema-enforcer repository located here. We then add a docker-compose.yml, where we mount in the full project repo into the previously built container and create a pipeline with two stages in .gitlab-ci.yml, which is triggered on every commit.

➜  schema-example git:(master) ✗ tree -a -I '.git' 
.
├── .gitlab-ci.yml
├── chi-beijing-rt1
│   ├── dns.yml # This will be the offending file in the failing CI pipeline.
│   └── syslog.yml
├── docker-compose.yml
├── eng-london-rt1
│   ├── dns.yml
│   └── ntp.yml
└── schema
    └── schemas
        ├── dns.yml # This will be the schema definition that triggers in the failure.
        ├── ntp.yml
        └── syslog.yml

4 directories, 9 files

Click Here for documentation on docker-compose and structuring the docker-compose.yml file. We are defining a single service called schema that uses the image we just publish to the Docker registry and are mounting in the current working directory of the pipeline execution into the container at /usr/scr/app. We are using the default entrypoint and cmd specified in the Dockerfile as schema-enforcer validate --show-pass but this could be overwritten in the service definition. For instance, if we would like to enable the strict flag, we would add command: ['validate', '--show-pass', '--strict'] inside the schema service. Keep in mind the command attribute of a service overwrites the CMD directive in the Dockerfile.

---
version: "3.8"
services:
  schema:
    # Uncomment the next line to enable strict on schema-enforcer
    # command: ['validate', '--show-pass', '--strict']
    image: registry.whitej6.com/ntc/docker/schema-enforcer:latest
    volumes:
      - ./:/usr/src/app/

Click Here for documentation on structuring the .gitlab-ci.yml file. We are defining two stages in the pipeline, and each stage has one job. The first stage ensures we have the most up to date container image for schema-enforcer and next we run schema service from the docker-compose.yml file. By specifiying --exit-code-from schema we are passing the exit code from the schema to the docker-compose command. The commands specified in the script are used to determine whether the job runs successfully. If the schema service returns a non-zero exit code, the job and pipeline will be marked as failed. The second stage ensures we are good tenants of docker and clean up after ourselves, docker-compose down will ensure we remove any containers or networks associated with this project.

---
stages:
  - test
  - clean

test:
  stage: test
  script:
    - docker-compose pull
    - docker-compose up --exit-code-from schema schema

clean:
  stage: clean
  script:
    - docker-compose down || true
  when: always

Failing.

In this example chi-beijing-rt1/dns.yml has a boolean value instead of an IPv4 address as specified in the schema/schemas/dns.yml. As you can see, the container returned a non-zero exit code, failing the pipeline and blocking the merge into a protected branch.

chi-beijing-rt1/dns.yml

# jsonschema: schemas/dns_servers
---
dns_servers:
  - address: true # This is a boolean value and we are expecting a string value in an IPv4 format
  - address: "10.2.2.2"

schema/schemas/dns.yml

---
$schema: "http://json-schema.org/draft-07/schema#"
$id: "schemas/dns_servers"
description: "DNS Server Configuration schema."
type: "object"
properties:
  dns_servers:
    type: "array"
    items:
      type: "object"
      properties:
        name:
          type: "string"
        address: # This is the specific property that will be used in the failed example.
          type: "string"
          format: "ipv4"
        vrf:
          type: "string"
      required:
        - "address"
      uniqueItems: true
required:
  - "dns_servers"

Runner output.

We see exactly which file and attribute fails the pipeline along with the runtime of the pipeline in seconds. 

Blocked Merge Request.

When sourcing from a branch with a failing pipeline, GitLab has the ability to block merging until the pipeline succeeds. By having the pipeline triggered on each commit we can resolve the issue on the next commit, which then triggers a new pipeline. Once the issue has been resolved, we will see the Merge button is no longer greyed out and can be merged into the target branch.

Passing.

Now the previous error has been corrected and a new commit has been made on the same branch. GitLab has then rerun the same pipeline with the new commit and upon passing the branch can be merged into the protected branch.

chi-beijing-rt1/dns.yml

# jsonschema: schemas/dns_servers
---
dns_servers:
  - address: "10.2.2.3" # This is the value that has been updated to align with the schema definition.
  - address: "10.2.2.2"

Runner output,

With the issue resolved and committed, we now see the previously offending file is passing the pipeline. 

Fixed Merge Request.

The merge request is now able to be merged into the target branch.

As a network engineer by trade that has come into automation, it at times has been difficult to trust the machine that was building the machine let alone trusting others eager to collaborate. Building safe guards for schema into my early pipelines would have saved me a tremendous amount of time and headache.

Friends don't let friends merge bad data.



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Introducing Schema Enforcer

Blog Detail

These days, most organizations heavily leverage YAML and JSON to store and organize all sorts of data. This is done in order to define variables, be provided as input for generating device configurations, define inventory, and for many other use cases. Both YAML and JSON are very popular because both languages are very flexible and are easy to use. It is relatively easy for users who have little to no experience working with structured data (as well as for very experienced programmers) to use JSON and YAML because the formats do not require users to define a schema in order to define data.

As the use of structured data increases, the flexibility provided because these languages don’t require data to adhere to a schema create complexity and risk. If a user accidentally defines the data for ntp_servers in two different structures (e.g. one is a list, and one is a dictionary), automation tooling must be written to handle the differences in inputs in some way. Often times, the automation tooling just bombs out with a cryptic message in such cases. This is because the tool consuming this data rightfully expects to have a contract with it, that the data will adhere to a clearly defined form and thus the tool can interact with the data in a standard way. It is for this reason that APIs, when updated, should never change the format in which they provide data unless there is some way to delineate the new format (e.g. an API version increment). By ensuring data is defined in a standard way, complexity and risk can be mitigated.

With structured data languages like YAML and JSON which do not inherently define a schema (contract) for the data they define, a schema definition language can be used to provide this contract, thereby mitigating complexity and risk. Schema definition languages come with their own added maintenance though as the burden of writing the logic to ensure structured data is schema valid falls on the user. The user doesn’t just need to maintain structured data and schemas, they also have to build and maintain the tooling that checks if data is schema valid. To allow users to simply write schemas and structured data and worry less about writing and maintaining the code that bolts them together, Network to Code has developed a tool called Schema Enforcer. Today we are happy to announce that we are making Schema Enforcer available to the community.

Check it out on Github!

What is Schema Enforcer

Schema Enforcer is a framework for allowing users to define schemas for their structured data and assert that the defined structured data adheres to their schemas. This structured data can (currently) come in the the form of a data file in JSON or YAML format, or an Ansible inventory. The schema definition is defined by using the JSONSchema language in YAML or JSON format.

Why use Schema Enforcer?

If you’re familiar with JSONSchema already, you may be thinking “wait, doesn’t JSONSchema do all of this?”. JSONSchema does allow you to validate that structured data adheres to a schema definition, but it requires for you to write your own code to interact with and manage the data’s adherence to defined schema. Schema Enforcer is meant to provide a wrapper which makes it easy for users to manage structured data without needing to write their own code to check their structured data for adherence to a schema. It provides the following advantages over just using JSONSchema:

  • Provides a framework for mapping data files to the schema definitions against which they should be checked for adherence
  • Provides a framework for validating that Ansible inventory adheres to a schema definition or multiple schema definitions
  • Prints clear log messages indicating each data object examined which is not adherent to schema, and the specific way in which these data objects are not adherent
  • Allows a user to define unit tests asserting that their schema definitions are written correctly (e.g. that non-adherent data fails validation in a specific way, and adherent data passes validation)
  • Exits with an exit code of 1 in the event that data is not adherent to schema. This makes it fit for use in a CI pipeline along-side linters and unit tests

An Example

I’ve created the following directories and files in a repository called new_example.

schema-enforcer-intro2

The directory includes

  • structured data (in YAML format) defining ntp servers for the host chi-beijing-rt01 inside of the file at hostvars/chi-beijing-rt01/ntp.yml
  • a schema definition inside of the file at schema/schemas/ntp.yml

If we examine the file at hostvars/chi-being-rt01/ntp.yml we can see the following data defined in YAML format.

# jsonschema: schemas/ntp
---
ntp_servers:
  - address: 192.2.0.1
  - address: 192.2.0.2

Note the comment # jsonschema: schemas/ntp at the top of the YAML file. This comment is used to declare the schema that the data in this file should be checked for adherence to, as well as the language being used to define the schema (JSONSchema here). Multiple schemas can be declared by comma separating them in the comment. For instance, the comment # jsonschema: schemas/ntp,schemas/syslog would declare that the data should be checked for adherence to two schema, one schema with the ID schemas/ntp and another with the id schemas/syslog. We can validate that this mapping is being inferred correctly by running the command schema-enforcer validate --show-checks

The --show-checks flag shows each data file along with a list of every schema IDs it will be checked for adherence to.

schema-enforcer-intro3

Other mechanisms for mapping data files to schemas against which they should be validated. See docs/mapping_schemas.md in the Schema Enforcer git repository. for more details.
YAML supports the addition of comments using an octothorp. JSON does not support the addition of comments. To this end, only data defined in YAML format can declare the schema to which it should adhere with a comment. Another mechanism for mapping needs to be used if your data is defined in JSON format.

If we examine the file at schema/schemas/ntp.yml we can see the following schema definition. This is written in the JSONSchema language and formatted in YAML.

---
$schema: "http://json-schema.org/draft-07/schema#"
$id: "schemas/ntp"
description: "NTP Configuration schema."
type: "object"
properties:
  ntp_servers:
    type: "array"
    items:
      type: "object"
      properties:
        name:
          type: "string"
        address:
          type: "string"
          format: "ipv4"
        vrf:
          type: "string"
      required:
          - "address"
    uniqueItems: true
additionalProperties: false
required:
  - "ntp_servers"

The schema definition above is used to ensure that:

  • The ntp_servers property is of type hash/dictionary (object in JSONSchema parlance)
  • No top level keys can be defined in the data file besides ntp_servers
  • It’s value is of type array/list
  • Each item in this array must be unique
  • Each element of this array/list is a dictionary with the possible keys nameaddress and vrf
    • Of these keys, address is required, name and vrf can optionally be defined, but it is not necessary to define them.
    • address must be of type “string” and it must be a valid IP address
    • name must be of type “string” if it is defined
    • vrf must be of type “string” if it is defined

Here is an example of the structured data being checked for adherence to the schema definition.

schema-enforcer-intro1

We can see that when schema-enforcer runs, it shows that all files containing structured data are schema valid. Also note that Schema Enforcer exits with a code of 0.

What happens if we modify the data such that the first ntp server defined has a value of the boolean true and add a syslog_servers dictionary/hash type object at the top level of the YAML file.

# jsonschema: schemas/ntp
---
ntp_servers:
  - address: true
  - address: 192.2.0.2
syslog_servers:
  - address: 192.0.5.3
schema-enforcer-intro4

We can see that two errors are flagged. The first informs us that the first element in the array which is the value of the ntp_servers top level key is a boolean and a string was expected. The second informs us that the additional top level property syslog_servers is a property that is additional to (is not specified in) the properties defined by the schema, and that additional properties are not allowed per the schema definition. Note that schema-enforcer exits with a code of 1 indicating a failure. If Schema Enforcer were to be used before structured data is ingested into automation tools as part of a pipeline, the pipeline would never have the automation tools consume the malformed data.

Validating Ansible Inventory

Schema Enforcer supports validating that variables defined in an Ansible inventory adhere to a schema definition (or multiple schema definitions).

To do this, Schema Enforcer first constructs a dictionary containing key/value pairs for each attribute defined in the inventory. It does this by flattening the varibles from the groups the host is a part of. After doing this, schema-enforcer maps which schemas it should use to validate the hosts variables in one of two ways:

  • By using a list of schema ids defined by the schema_enforcer_schema_ids attribute (defined at the host or group level).
  • By automatically mapping a schema’s top level properties to the Ansible host’s keys.

That may have been gibberish on first pass, but the examples in the following sections will hopefully make things clearer.

schema-enforcer-ansible

An Example of Validating Ansible Variables

In the following example, we have an inventory file which defines three groups, nycspine, and leafspine and leaf are children of nyc.

[nyc:children]
spine
leaf

[spine]
spine1
spine2

[leaf]
leaf1
leaf2

The group spine.yaml has two top level keys; dns_servers and interfaces.

cat group_vars/spine.yaml
---
dns_servers:
  - address: true
  - address: "10.2.2.2"
interfaces:
  swp1:
    role: "uplink"
  swp2:
    role: "uplink"

schema_enforcer_schema_ids:
  - "schemas/dns_servers"
  - "schemas/interfaces"

Note the schema_enforcer_schema_ids variable. This declaratively tells Schema Enforcer which schemas to use when running tests to ensure that the Ansible host vars for every host in the spine group are schema valid.

Here is the interfaces schema which is declared above:

bash$ cat schema/schemas/interfaces.yml
---
$schema: "http://json-schema.org/draft-07/schema#"
$id: "schemas/interfaces"
description: "Interfaces configuration schema."
type: "object"
properties:
  interfaces:
    type: "object"
    patternProperties:
      ^swp.*$:
        properties:
          type:
            type: "string"
          description:
            type: "string"
          role:
            type: "string"

Note that the $id property is what is being declared by the schema_enforcer_schema_ids variable.

schema-enforcer-ansible2

When we run the schema-enforcer ansible command with the --show-pass flag, we can see that the spine1 and spine2’s defined dns_servers attribute did not adhere to schema.

By default, schema-enforcer prints a “FAIL” message to stdout for each object in the data file which does not adhere to schema. If no objects fail to adhere to schema definitions, a single line is printed indicating that all data files are schema valid. The --show-pass flag modifies this behavior such that, in addition the the default behavior, a line is printed to stdout for every file that is schema valid indicating it passed the schema adherence check.

In looking at the group_vars/spine.yaml group above. This is because the first dns server in the list which is the value of dns_servers has a value of the boolean true.

bash$ cat schema/schemas/dns.yml
---
$schema: "http://json-schema.org/draft-07/schema#"
$id: "schemas/dns_servers"
description: "DNS Server Configuration schema."
type: "object"
properties:
  dns_servers:
    type: "array"
    items:
      type: "object"
      properties:
        name:
          type: "string"
        address:
          type: "string"
          format: "ipv4"
        vrf:
          type: "string"
        required:
            - "address"
    uniqueItems: true
required:
  - "dns_servers"

In looking at the schema for dns servers, we see that DNS servers address field must be of type string and format ipv4 (e.g. IPv4 address). Because the first element in the list of DNS servers has an address of the boolean true it is not schema valid.

Another Example of Validating Ansible Vars

Similar to the way that schema-enforcer validate --show-checks can be used to show which data files will be checked by which schema definitions, the schema-enforcer ansible --show-checks command can be used to show which Ansible hosts will be checked for adherence to which schema IDs.

schema-enforcer-ansible3

From the execution of the command, we can see that 4 hosts were loaded from inventory. This is just what we expect from our earlier examination of the .ini file which defines Ansible inventory. We just saw how spine1 and spine2 were checked for adherence to both the schemas/dns_servers and schemas/interfaces schema definitions, and how the schema_enforcer_schema_ids var was configured to declare that devices belonging to the spine group should adhere to those schemas. Lets now examine the leaf group a little more closely.

cat ansible/group_vars/leaf.yml
---
dns_servers:
  - address: "10.1.1.1"
  - address: "10.2.2.2"

In the leaf.yml file, no schema_enforcer_schema_ids var is configured. There is also no individual data defined at the host level for leaf1 and leaf2 which belong to the leaf group. This brings up the question, how does schema-enforcer know to check the leaf switches for adherence to the schemas/dns_servers schema definition?

The default behavior of schema-enforcer is to map the top level property in a schema definition to vars associated with each Ansible host that have the same name.

bash$ cat schema/schemas/dns.yml
---
$schema: "http://json-schema.org/draft-07/schema#"
$id: "schemas/dns_servers"
description: "DNS Server Configuration schema."
type: "object"
properties:
  dns_servers:
    type: "array"
    items:
      type: "object"
      properties:
        name:
          type: "string"
        address:
          type: "string"
          format: "ipv4"
        vrf:
          type: "string"
        required:
            - "address"
    uniqueItems: true
required:
  - "dns_servers"

Because the property defined in the schema definition above is dns_servers, the matching Ansible host var dns_servers will be checked for adherence against it.

In fact, if we make the following changes to the leaf group var definition then run schema-enforcer --show-checks, we can see that devices belonging to the leaf group are now slated to be checked for adherence to both the schemas/dns_servers and schemas/interfaces schema definitions.

cat ansible/group_vars/leaf.yml
---
dns_servers:
  - address: "10.1.1.1"
  - address: "10.2.2.2"
interfaces:
  swp01:
    role: uplink
schema-enforcer-ansible3

Using Schema Enforcer

O.K. so you’ve defined schemas for your data, now what? Here are a couple of use cases for Schema Enforcer we’ve found to be “juice worth the squeeze.”

1) Use Schema Enforcer in your CI system to validate defined structured data before merging code. Virtually all git version control systems (GitHub, GitLab…etc) allow the ability to configure tests which must pass before code can be merged from a feature branch into the code base. Schema Enforcer can be turned on along side your other tests (unit tests, linters…etc). If your data is not schema valid, the exact reason why the data is not schema valid will be printed to the output of the CI system when the tool is run and the tool will exit with a code of 1 causing the CI system to register a failure. When the CI system sees a failure, it will not allow the merge of data which is not adherent to schema.

2) Use it in a pipeline. Say you have YAML structured data which defines the configuration for network devices. You can run schema enforcer as part of a pipeline and run it before automation tooling (Ansible, Python…etc) consumes this data in order to render configurations for devices. If the data isn’t schema valid, the pipeline will fail before rendering configurations and pushing them to devices (or exploding with a stack trace that takes you 30 minutes and lots of googling to troubleshoot).

3) Run it after your tooling generates structured data and prints it to a file. In this case, Schema Enforcer can act as a sanity check to ensure that your tooling is dumping correctly structured output.

Where Are We Going Next

We plan to add the support for the following features to Schema Enforcer in the future:

  • Validation of Nornir inventory attributes
  • Business logic validation

Conclusion

Have a use case for Schema Enforcer? Try it out and let us know what you think! Do you want the ability to write schema definitions in YANG or have another cool idea? We are iterating on the tool and we are open to feedback!

-Phillip Simonds



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!