From be102d5b6d91c10c2c4446880420cae4b683c622 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sun, 13 Mar 2022 09:55:01 +0000 Subject: [PATCH] Add ip_interface() validator --- README.md | 45 +++++++++++++++++++++++++++++++------------ schema.yaml | 8 ++------ validator/__init__.py | 31 ++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ce23922..42f810a 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,38 @@ ## Design -### Validators +### YAML Configuration -Validators are functions which are passed the fully formed YAML configuration and are meant to -check it for syntax and semantic validity. A validator has a unique name and takes the (yaml) -configuration as the only argument. Validators are expected to return a tuple of (bool,[string]) -where the boolean signals success (False meaning the validator rejected the configuration file, -True meaning it is known to be correct), and a list of zero or more strings which contain messages -meant for human consumption. They can contain INFO, WARN or ERROR messages, and are meant to help -the caller understand why the validator rejected the configuration. +The main file that is handled by this program is the **Configuration File**. -Validators can be disabled with the --skip-validator <name> [<name>] flag. It is not -advised to skip validators. The purpose of the validators is to ensure the configuration is sane -and semantically correct. +## Validation -Validators can be registered as follows: +There are three types of validation: _schema_ which ensures that the input YAML has the correct +fields of well known types, _semantic_ which ensures that the configuration doesn't violate +semantic constraints and _runtime_ which ensures that the configuration can be applied to the +VPP daemon. + +### Schema Validators + +First the configuration file is held against a structural validator, provided by [Yamale](https://github.com/23andMe/Yamale/). +Based on a validation schema in `schema.yaml`, the input file is checked for syntax correctness. +For example, a `dot1q` field must be an integer between 1 and 4095, wile an `lcp` string must +match a certain regular expression. After this first pass of syntax validation, I'm certain that +_if_ a field is set, it is of the right type (ie. string, int, enum). + +### Semantic Validators + +A set of semantic validators, each with a unique name, ensure that the _semantics_ of the YAML +are correct. For example, a physical interface cannot have an LCP, addresses or sub-interfaces, +if it is to be a member of a BondEthernet. + +Validators are expected to return a tuple of (bool,[string]) where the boolean signals success +(False meaning the validator rejected the configuration file, True meaning it is known to be +correct), and a list of zero or more strings which contain messages meant for human consumption. + +### Runtime Validators + +After the configuration file is considered syntax and semanticly valid, there is one more set of +checks to perform -- runtime validators ensure that the configuration elements such as physical +network devices (ie. `HundredGigabitEthernet12/0/0` or plugin `lcpng` are present on the system. +It does this by connecting to VPP and querying the runtime state to ensure that what is modeled +in the configuration file is able to be committed. diff --git a/schema.yaml b/schema.yaml index a0f8ced..a4d3aee 100644 --- a/schema.yaml +++ b/schema.yaml @@ -9,17 +9,13 @@ interface: description: str(exclude='\'"',required=False) lcp: str(max=8,matches='[a-z]+[a-z0-9-]{,7}',required=False) mac: mac(required=False) - addresses: list(include('v4'),include('v6'),min=1,max=6,required=False) + addresses: list(ip_interface(),min=1,max=6,required=False) sub-interfaces: map(include('sub-interface'),key=int(min=1,max=4294967295),required=False) --- -v4: str(matches='[0-9\.]+/[0-9]+') ---- -v6: str(matches='[0-9a-f:]+/[0-9]+',ignore_case=True) ---- sub-interface: description: str(exclude='\'"',required=False) lcp: str(max=8,matches='[a-z]+[a-z0-9-]{,7}',required=False) - addresses: list(ip(),required=False) + addresses: list(ip_interface(),required=False) encapsulation: include('encapsulation',required=False) --- encapsulation: diff --git a/validator/__init__.py b/validator/__init__.py index 6ceb744..9841510 100644 --- a/validator/__init__.py +++ b/validator/__init__.py @@ -17,6 +17,33 @@ from validator.bondethernet import bondethernet from validator.interface import interface from validator.bridgedomain import bridgedomain +from yamale.validators import DefaultValidators, Validator +import ipaddress + +class IPInterfaceWithPrefixLength(Validator): + """ Custom IPAddress validator - takes IP/prefixlen as input: + 192.0.2.1/29 or 2001:db8::1/64 are correct. The PrefixLength + is required, and must be a number (0-32 for IPv4 and 0-128 for + IPv6). + """ + tag = 'ip_interface' + + def _is_valid(self, value): + try: + network = ipaddress.ip_interface(value) + except: + return False + if not '/' in value: + return False + e = value.split('/') + if not len(e) == 2: + return False + if not e[1].isnumeric(): + return False + return True + + + class NullHandler(logging.Handler): def emit(self, record): pass @@ -35,7 +62,9 @@ class Validator(object): if self.args.schema: try: self.logger.info("Validating against schema %s" % self.args.schema) - schema = yamale.make_schema(self.args.schema) + validators = DefaultValidators.copy() + validators[IPInterfaceWithPrefixLength.tag] = IPInterfaceWithPrefixLength + schema = yamale.make_schema(self.args.schema, validators=validators) data = yamale.make_data(content=str(yaml)) yamale.validate(schema, data) self.logger.debug("Schema correctly validated by yamale")