Initial commit

This commit is contained in:
Pim van Pelt
2022-03-13 09:54:50 +00:00
commit 9862129ab0
10 changed files with 451 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
__pycache__

17
README.md Normal file
View File

@ -0,0 +1,17 @@
## Design
### Validators
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.
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.
Validators can be registered as follows:

53
example.yaml Normal file
View File

@ -0,0 +1,53 @@
bondethernets:
BondEthernet0:
description: "Infra: xsw0.lab.ipng.ch LACP"
interfaces: [ GigabitEthernet2/0/0, GigabitEthernet2/0/1 ]
interfaces:
GigabitEthernet1/0/0:
description: "Infra: nikhef-core-1.nl.switch.coloclue.net e1/34"
lcp: e0-0
addresses: [ 94.142.244.85/24, 2A02:898::146:1/64 ]
sub-interfaces:
100:
description: "Cust: hvn0.nlams0.ipng.ch"
addresses: [ 94.142.241.185/29, 2a02:898:146::1/64 ]
101:
description: "Infra: L2 for FrysIX AS112"
GigabitEthernet2/0/0:
description: "Infra: LAG to xsw0"
GigabitEthernet2/0/1:
description: "Infra: LAG to xsw1"
BondEthernet0:
description: "Bond, James Bond!"
mac: 00:01:02:03:04:05
sub-interfaces:
200:
encapsulation:
dot1q: 1000
201:
encapsulation:
dot1ad: 1000
202:
encapsulation:
dot1q: 1000
inner-dot1q: 1000
203:
encapsulation:
dot1ad: 1000
inner-dot1q: 1000
300:
encapsulation:
dot1ad: 1000
dot1q: 1000
301:
encapsulation:
inner-dot1q: 1000
302:
encapsulation:
dot1ad: 1000
dot1q: 1000
inner-dot1q: 1000

30
schema.yaml Normal file
View File

@ -0,0 +1,30 @@
interfaces: map(include('interface'),key=str(matches='.*GigabitEthernet[0-9]+/[0-9]+/[0-9]+|BondEthernet[0-9]+'))
bondethernets: map(include('bondethernet'),key=str(matches='BondEthernet[0-9]+'))
---
bondethernet:
description: str(exclude='\'"',required=False)
interfaces: list(str(matches='.*GigabitEthernet[0-9]+/[0-9]+/[0-9]+'))
---
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)
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)
encapsulation: include('encapsulation',required=False)
---
encapsulation:
dot1q: int(min=1,max=4095,required=False)
dot1ad: int(min=1,max=4095,required=False)
inner-dot1q: int(min=1,max=4095,required=False)
exact-match: bool(required=False)

77
validator/__init__.py Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import (
absolute_import,
division,
print_function,
)
import logging
try:
import yamale
except ImportError:
print("ERROR: install yamale manually: sudo pip install yamale")
sys.exit(-2)
from validator.loopback import loopback
from validator.bondethernet import bondethernet
from validator.interface import interface
from validator.bridgedomain import bridgedomain
class NullHandler(logging.Handler):
def emit(self, record):
pass
class Validator(object):
def __init__(self, args):
self.logger = logging.getLogger('vppcfg.validator')
self.logger.addHandler(NullHandler())
self.args = args
def validate(self, yaml):
ret_rv = True
ret_msgs = []
if self.args.schema:
try:
self.logger.info("Validating against schema %s" % self.args.schema)
schema = yamale.make_schema(self.args.schema)
data = yamale.make_data(content=str(yaml))
yamale.validate(schema, data)
self.logger.debug("Schema correctly validated by yamale")
except ValueError as e:
ret_rv = False
for result in e.results:
for error in result.errors:
ret_msgs.extend(['yamale: %s' % error])
return ret_rv, ret_msgs
else:
self.logger.warning("Schema validation disabled")
self.logger.debug("Validating Semantics...")
rv, msgs = bondethernet(self.args, yaml)
if msgs:
ret_msgs.extend(msgs)
if not rv:
ret_rv = False
rv, msgs = interface(self.args, yaml)
if msgs:
ret_msgs.extend(msgs)
if not rv:
ret_rv = False
rv, msgs = loopback(self.args, yaml)
if msgs:
ret_msgs.extend(msgs)
if not rv:
ret_rv = False
rv, msgs = bridgedomain(self.args, yaml)
if msgs:
ret_msgs.extend(msgs)
if not rv:
ret_rv = False
return ret_rv, ret_msgs

44
validator/bondethernet.py Normal file
View File

@ -0,0 +1,44 @@
import logging
import validator.interface as interface
class NullHandler(logging.Handler):
def emit(self, record):
pass
def exists(yaml, ifname):
""" Return True if the BondEthernet exists """
try:
if ifname in yaml['bondethernets']:
return True
except:
pass
return False
def bondethernet(args, yaml):
result = True
msgs = []
logger = logging.getLogger('vppcfg.validator')
logger.addHandler(NullHandler())
if not 'bondethernets' in yaml:
return result, msgs
for ifname, iface in yaml['bondethernets'].items():
logger.debug("bondethernet %s: %s" % (ifname, iface))
for member in iface['interfaces']:
if not interface.exists(yaml, member):
msgs.append("bondethernet %s member %s doesn't exist" % (ifname, member))
result = False
continue
if interface.has_sub(yaml, member):
msgs.append("bondethernet %s member %s has sub-interface(s)" % (ifname, member))
result = False
if interface.has_lcp(yaml, member):
msgs.append("bondethernet %s member %s has an LCP" % (ifname, member))
result = False
if interface.has_address(yaml, member):
msgs.append("bondethernet %s member %s has address(es)" % (ifname, member))
result = False
return result, msgs

15
validator/bridgedomain.py Normal file
View File

@ -0,0 +1,15 @@
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
def bridgedomain(args, yaml):
result = True
msgs = []
logger = logging.getLogger('vppcfg.validator')
logger.addHandler(NullHandler())
logger.debug("Validating bridgedomains...")
return result, msgs

149
validator/interface.py Normal file
View File

@ -0,0 +1,149 @@
import logging
import validator.bondethernet as bondethernet
class NullHandler(logging.Handler):
def emit(self, record):
pass
def has_sub(yaml, ifname):
""" Returns True if this interface has sub-interfaces """
if not 'interfaces' in yaml:
return False
if ifname in yaml['interfaces']:
iface = yaml['interfaces'][ifname]
if 'sub-interfaces' in iface and len(iface['sub-interfaces']) > 0:
return True
return False
def has_address(yaml, ifname):
""" Returns True if this interface or sub-interface has one or more addresses"""
if not 'interfaces' in yaml:
return False
if '.' in ifname:
ifname, subid = ifname.split('.')
subid = int(subid)
try:
if len(yaml['interfaces'][ifname]['sub-interfaces'][subid]['addresses']) > 0:
return True
except:
pass
return False
try:
if len(yaml['interfaces'][ifname]['addresses']) > 0:
return True
except:
pass
return False
def is_bond_member(yaml, ifname):
""" Returns True if this interface is a member of a BondEthernet """
if not 'bondethernets' in yaml:
return False
for bond, iface in yaml['bondethernets'].items():
if not 'interfaces' in iface:
continue
if ifname in iface['interfaces']:
return True
return False
def has_lcp(yaml, ifname):
""" Returns True if this interface or sub-interface has an LCP"""
if not 'interfaces' in yaml:
return False
if '.' in ifname:
ifname, subid = ifname.split('.')
subid = int(subid)
try:
if len(yaml['interfaces'][ifname]['sub-interfaces'][subid]['lcp']) > 0:
return True
except:
pass
return False
try:
if len(yaml['interfaces'][ifname]['lcp']) > 0:
return True
except:
pass
return False
def exists(yaml, ifname):
""" Returns true if ifname exists as a phy or sub-int """
try:
if ifname in yaml['interfaces']:
return True
if '.' in ifname:
ifname, subid = ifname.split('.')
subid = int(subid)
if subid in yaml['interfaces'][ifname]['sub-interfaces']:
return True
except:
pass
return False
def valid_encapsulation(yaml, sub_ifname):
try:
ifname, subid = sub_ifname.split('.')
subid = int(subid)
sub_iface = yaml['interfaces'][ifname]['sub-interfaces'][subid]
except:
return False
if not 'encapsulation' in sub_iface:
return True
encap = sub_iface['encapsulation']
if 'dot1ad' in encap and 'dot1q' in encap:
return False
if 'inner-dot1q' in encap and not ('dot1ad' in encap or 'dot1q' in encap):
return False
return True
def interface(args, yaml):
result = True
msgs = []
logger = logging.getLogger('vppcfg.validator')
logger.addHandler(NullHandler())
if not 'interfaces' in yaml:
return result, msgs
for ifname, iface in yaml['interfaces'].items():
logger.debug("interface %s" % iface)
if ifname.startswith("BondEthernet") and not bondethernet.exists(yaml, ifname):
msgs.append("interface %s does not exist in bondethernets" % ifname)
result = False
continue
iface_lcp = has_lcp(yaml, ifname)
iface_address = has_address(yaml, ifname)
if iface_address and not iface_lcp:
msgs.append("interface %s has adddress(es) but no LCP" % ifname)
result = False
if has_sub(yaml, ifname):
for sub_id, sub_iface in yaml['interfaces'][ifname]['sub-interfaces'].items():
sub_ifname = "%s.%d" % (ifname, sub_id)
logger.debug("sub-interface %s" % sub_iface)
if has_lcp(yaml, sub_ifname):
if not iface_lcp:
msgs.append("sub-interface %s has LCP but %s does not have LCP" % (sub_ifname, ifname))
result = False
if has_address(yaml, sub_ifname):
## The sub_iface lcp is not required: it can be derived from the iface_lcp, which has to be set
if not iface_lcp:
msgs.append("sub-interface %s has address(es) but %s does not have LCP" % (sub_ifname, ifname))
result = False
if not valid_encapsulation(yaml, sub_ifname):
msgs.append("sub-interface %s has invalid encapsulation" % (sub_ifname))
result = False
return result, msgs

15
validator/loopback.py Normal file
View File

@ -0,0 +1,15 @@
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
def loopback(args, yaml):
result = True
msgs = []
logger = logging.getLogger('vppcfg.validator')
logger.addHandler(NullHandler())
logger.debug("Validating loopbacks...")
return result, msgs

48
vppcfg Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import yaml
import logging
from validator import Validator
try:
import argparse
except ImportError:
print("ERROR: install argparse manually: sudo pip install argparse")
sys.exit(-2)
def main():
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-c', dest='config', type=str, help="""YAML configuration file for VPP""")
parser.add_argument('-s', dest='schema', type=str, default='./schema.yaml', help="""YAML schema validation""")
parser.add_argument('-d', dest='debug', action='store_true', help="""Enable debug, default False""")
args = parser.parse_args()
if args.debug:
print("Arguments:", args)
level = logging.DEBUG
else:
level = logging.INFO
logging.basicConfig(format='[%(levelname)-8s] %(name)s.%(funcName)s: %(message)s', level=level)
try:
with open(args.config, "r") as f:
logging.info("Loading configfile %s" % args.config)
cfg = yaml.load(f, Loader = yaml.FullLoader)
logging.debug("Config: %s" % cfg)
except:
logging.error("Couldn't read config from %s" % args.config)
sys.exit(-1)
v = Validator(args)
rv, msgs = v.validate(cfg)
if not rv:
for m in msgs:
logging.error(m)
else:
logging.info("Configuration validated successfully")
if __name__ == "__main__":
main()