#
# Copyright (c) 2014, Arista Networks, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# Neither the name of Arista Networks nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
# pylint: disable=C0103,W0142
#
import collections
import logging
import os
import re
import string # pylint: disable=W0402
import ztpserver
import ztpserver.config
from ztpserver.validators import validate_neighbordb, validate_pattern
from ztpserver.resources import ResourcePool
from ztpserver.constants import CONTENT_TYPE_YAML
from ztpserver.serializers import load, SerializerError
from ztpserver.utils import expand_range, parse_interface
ANY_DEVICE_PARSER_RE = re.compile(r':(?=[any])')
NONE_DEVICE_PARSER_RE = re.compile(r':(?=[none])')
FUNC_RE = re.compile(r'(?P<function>\w+)(?=\(\S+\))\([\'|\"]'
r'(?P<arg>.+?)[\'|\"]\)')
ALL_CHARS = set([chr(c) for c in range(256)])
NON_HEX_CHARS = ALL_CHARS - set(string.hexdigits)
log = logging.getLogger(__name__)
Neighbor = collections.namedtuple('Neighbor', ['device', 'interface'])
[docs]def default_filename():
''' Returns the path for neighbordb based on the conf file
'''
filepath = ztpserver.config.runtime.default.data_root
filename = ztpserver.config.runtime.neighbordb.filename
return os.path.join(filepath, filename)
[docs]def load_file(filename, content_type, node_id):
''' Returns the contents of a file specified by filename.
The requred content_type argument is required and indicates the
text serialization format the contents are stored in.
If the serializer load function encounters errors, None is returned
'''
try:
return load(filename, content_type, node_id)
except SerializerError:
log.error('%s: failed to load file: %s' % (node_id, filename))
raise
[docs]def load_neighbordb(node_id, contents=None):
try:
if not contents:
contents = load_file(default_filename(), CONTENT_TYPE_YAML,
node_id)
# neighbordb is empty
if not contents:
contents = dict()
if not validate_neighbordb(contents, node_id):
log.error('%s: failed to validate neighbordb' % node_id)
return
neighbordb = Neighbordb(node_id)
if 'variables' in contents:
neighbordb.add_variables(contents['variables'])
if 'patterns' in contents:
neighbordb.add_patterns(contents['patterns'])
log.debug('%s: loaded neighbordb: %s' % (node_id, neighbordb))
return neighbordb
except (NeighbordbError, SerializerError):
log.error('%s: failed to load neighbordb' % node_id)
except Exception as err:
log.error('%s: failed to load neighbordb because of error: %s' %
(node_id, err))
[docs]def load_pattern(pattern, content_type=CONTENT_TYPE_YAML, node_id=None):
""" Returns an instance of Pattern """
try:
if not isinstance(pattern, collections.Mapping):
pattern = load_file(pattern, content_type,
node_id)
if not validate_pattern(pattern, node_id):
log.error('%s: failed to validate pattern attributes' % node_id)
return None
pattern['node_id'] = node_id
return Pattern(**pattern)
except TypeError:
log.error('%s: failed to load pattern \'%s\'' % (node_id, pattern))
[docs]def create_node(nodeattrs):
try:
if nodeattrs.get('systemmac') is not None:
_systemmac = nodeattrs['systemmac']
for symbol in [':', '.']:
_systemmac = str(_systemmac).replace(symbol, '')
nodeattrs['systemmac'] = _systemmac
node = Node(**nodeattrs)
log.debug('%s: created node object %r' % (node.identifier(), node))
return node
except KeyError as err:
log.error('Failed to create node - missing attribute: %s' % err)
[docs]def resources(attributes, node, node_id):
log.debug('%s: computing resources (attr=%s)' %
(node_id, attributes))
_attributes = dict()
_resources = ResourcePool(node_id)
for key, value in attributes.items():
if hasattr(value, 'items'):
value = resources(value, node, node_id)
elif hasattr(value, '__iter__'):
_value = list()
for item in value:
match = FUNC_RE.match(item)
if match:
method = getattr(_resources, match.group('function'))
_value.append(method(match.group('arg'), node))
else:
_value.append(item)
value = _value
else:
match = FUNC_RE.match(str(value))
if match:
method = getattr(_resources, match.group('function'))
value = method(match.group('arg'), node)
_attributes[key] = value
log.debug('%s: resources: %s' % (node_id, _attributes))
return _attributes
[docs]def replace_config_action(resource, filename=None):
''' Builds a definition with a single action replace_config '''
filename = filename or 'startup-config'
server_url = ztpserver.config.runtime.default.server_url
url = '%s/nodes/%s/%s' % (server_url, str(resource), filename)
action = dict(name='install static startup-config file',
action='replace_config',
always_execute=True,
attributes={'url': url})
return action
[docs]class NodeError(Exception):
''' Base exception class for :py:class:`Node` '''
pass
[docs]class PatternError(Exception):
''' Base exception class for :py:class:`Pattern` '''
pass
[docs]class InterfacePatternError(Exception):
''' Base exception class for :py:class:`InterfacePattern` '''
pass
[docs]class NeighbordbError(Exception):
''' Base exception class for :py:class:`Neighbordb` '''
pass
[docs]class OrderedCollection(collections.OrderedDict):
''' base object for using an ordered dictionary '''
def __call__(self, key=None):
#pylint: disable=W0221
return self.get(key) if key else self.keys()
[docs]class Function(object):
def __init__(self, value):
self.value = value
[docs] def match(self, arg):
raise NotImplementedError
[docs]class IncludesFunction(Function):
[docs] def match(self, arg):
return self.value in arg
[docs]class ExcludesFunction(Function):
[docs] def match(self, arg):
return self.value not in arg
[docs]class RegexFunction(Function):
[docs] def match(self, arg):
match = re.match(self.value, arg)
return match is not None
[docs]class ExactFunction(Function):
[docs] def match(self, arg):
return arg == self.value
[docs]class Node(object):
''' A Node object is maps the metadata from an EOS node. It provides
access to the node's meta data including interfaces and the
associated neighbors found on those interfaces.
'''
def __init__(self, **kwargs):
self.systemmac = kwargs.get('systemmac')
self.model = kwargs.get('model')
self.serialnumber = kwargs.get('serialnumber')
self.version = kwargs.get('version')
self.neighbors = OrderedCollection()
if 'neighbors' in kwargs:
self.add_neighbors(kwargs['neighbors'])
def __repr__(self):
return 'Node(serialnumber=%s, systemmac=%s, neighbors=%s)' % \
(self.serialnumber, self.systemmac, self.neighbors)
[docs] def identifier(self):
identifier = ztpserver.config.runtime.default.identifier
return getattr(self, identifier)
[docs] def add_neighbor(self, interface, peers):
try:
if self.neighbors.get(interface):
raise NodeError('%s: interface \'%s\' already added to node' %
(self.identifier(), interface))
_neighbors = list()
for peer in peers:
log.debug('%s: creating neighbor %s:%s for interface %s' %
( self.identifier(), peer['device'],
peer['port'], interface))
_neighbors.append(Neighbor(peer['device'],
peer['port']))
self.neighbors[interface] = _neighbors
except KeyError as err:
log.error('%s: failed to neighbor because of missing key (%s)' %
(self.identifier(), str(err)))
raise NodeError('%s: failed to neighbor because of KeyError (%s)' %
(self.identifier(), str(err)))
[docs] def add_neighbors(self, neighbors):
for interface, peers in neighbors.items():
self.add_neighbor(interface, peers)
[docs] def serialize(self):
result = {}
for prop in ['model', 'systemmac', 'serialnumber', 'version']:
if getattr(self, prop):
result[prop] = getattr(self, prop)
neighbors = {}
if self.neighbors:
for interface, neighbor_list in self.neighbors.items():
serialized_neighbor_list = []
for neighbor in neighbor_list:
serialized_neighbor_list.append(
dict(device=neighbor.device, port=neighbor.interface))
neighbors[interface] = serialized_neighbor_list
result['neighbors'] = neighbors
return result
[docs]class Neighbordb(object):
RESERVED_VARIABLES = ['any', 'none']
def __init__(self, node_id):
self.node_id = node_id
self.variables = dict()
self.patterns = {'globals': list(), 'nodes': dict()}
def __repr__(self):
return 'Neighbordb(variables=%d, globals=%d, nodes=%d)' % \
(len(self.variables),
len(self.patterns['globals']),
len(self.patterns['nodes']))
[docs] def add_variable(self, key, value, overwrite=False):
if key in self.RESERVED_VARIABLES:
log.error('%s: failed to add variable: %s (reserved keyword)' %
(self.node_id, key))
raise NeighbordbError('%s: failed to add variable: %s '
'(reserved keyword)' % (self.node_id, key))
elif key in self.variables and not overwrite:
log.error('%s: failed to add variable: %s (duplicate)' %
(self.node_id, key))
raise NeighbordbError('%s: failed to add variable %s '
'(duplicate)' % (self.node_id, key))
self.variables[key] = value
[docs] def add_variables(self, variables):
if not hasattr(variables, 'items'):
log.error('%s: failed to add variables: missing attribute '
'\'items\' (%s)' % (self.node_id, variables))
raise NeighbordbError('%s: failed to add variables: '
'missing attribute \'items\' (%s)' %
(self.node_id, variables))
for key, value in variables.items():
self.add_variable(key, value)
[docs] def add_pattern(self, name, **kwargs):
try:
kwargs['node_id'] = self.node_id
kwargs['name'] = name
kwargs['node'] = kwargs.get('node')
kwargs['definition'] = kwargs.get('definition')
kwargs['interfaces'] = kwargs.get('interfaces', list())
kwargs['variables'] = kwargs.get('variables', dict())
for key in set(self.variables).difference(kwargs['variables']):
kwargs['variables'][key] = self.variables[key]
pattern = Pattern(**kwargs)
log.debug('%s: pattern \'%r\' parsed successfully' %
(self.node_id, pattern))
# Add pattern to neighbordb
if kwargs['node']:
self.patterns['nodes'][pattern.node] = pattern
else:
self.patterns['globals'].append(pattern)
except KeyError as err:
log.error('%s: failed to add pattern \'%s\' because of '
'missing key (%s)' % (self.node_id, name, str(err)))
raise NeighbordbError('%s: failed to pattern \'%s\' because of '
'missing key (%s)' %
(self.node_id, name, str(err)))
except PatternError:
log.error('%s: failed to add pattern \'%s\'' %
(self.node_id, name))
raise NeighbordbError('%s: failed to add pattern \'%s\'' %
(self.node_id, name))
[docs] def add_patterns(self, patterns):
try:
for pattern in patterns:
self.add_pattern(**pattern)
except TypeError as err:
log.error('%s: failed to add patterns %s: %s' %
(self.node_id, patterns, str(err)))
raise NeighbordbError('%s: failed to add patterns %s: %s' %
(self.node_id, patterns, str(err)))
[docs] def is_node_pattern(self, pattern):
#pylint: disable=R0201
return pattern.node
[docs] def is_global_pattern(self, pattern):
#pylint: disable=R0201
return not pattern.node
[docs] def get_patterns(self):
return self.patterns['nodes'].values() + self.patterns['globals']
@staticmethod
[docs] def identifier(node):
identifier = ztpserver.config.runtime.default.identifier
return node[identifier]
[docs] def find_patterns(self, node):
identifier = node.identifier()
log.debug('%s: searching for eligible patterns' %
identifier)
pattern = self.patterns['nodes'].get(identifier, None)
if pattern:
log.debug('%s: eligible pattern: %s' % (identifier,
pattern.name))
return [pattern]
else:
if self.patterns['globals']:
log.debug('%s: all global patterns are eligible' %
identifier)
else:
log.debug('%s: no eligible patterns' %
identifier)
return self.patterns['globals']
[docs] def match_node(self, node):
identifier = node.identifier()
result = list()
for pattern in self.find_patterns(node):
log.debug('%s: attempting to match pattern %s' %
(identifier, pattern.name))
if pattern.match_node(node):
log.debug('%s: pattern %s matched' %
(identifier, pattern.name))
result.append(pattern)
else:
log.debug('%s: pattern %s match failed' %
(identifier, pattern.name))
return result
[docs]class Pattern(object):
def __init__(self, name=None, definition=None, interfaces=None,
node=None, variables=None, node_id=None):
self.name = name
self.definition = definition
self.node = node
self.node_id = node_id
self.variables = variables or dict()
self.interfaces = list()
if interfaces:
self.add_interfaces(interfaces)
self.variable_substitution()
def __repr__(self):
return 'Pattern(name=\'%s\')' % self.name
[docs] def variable_substitution(self):
try:
log.debug('%s: checking pattern \'%s\' entries for variable '
'substitution' % (self.node_id, self.name))
for entry in self.interfaces:
for item in entry['patterns']:
for attr in ['remote_device', 'remote_interface']:
value = getattr(item, attr)
if value.startswith('$'):
newvalue = self.variables[value[1:]]
setattr(item, attr, newvalue)
item.refresh()
log.debug('%s: pattern \'%s\' variable substitution complete' %
(self.node_id, self.name))
except KeyError as exc:
log.debug('%s: pattern \'%s\' variable substitution failed: %s' %
(self.node_id, self.name, str(exc)))
raise PatternError('%s: pattern \'%s\' variable substitution '
'failed: %s' %
(self.node_id, self.name, str(exc)))
[docs] def serialize(self):
data = dict(name=self.name, definition=self.definition)
data['variables'] = self.variables
data['node'] = self.node
interfaces = []
for item in self.interfaces:
_item = item['metadata']
interfaces.append({_item['interface']: _item['neighbors']})
data['interfaces'] = interfaces
return data
[docs] def parse_interface(self, neighbor):
try:
return parse_interface(neighbor, self.node_id)
except Exception as err:
raise PatternError(str(err))
[docs] def add_interface(self, interface):
try:
if not hasattr(interface, 'items'):
log.error('%s: pattern \'%s\' - failed to add interface %s: '
'missing attribute (items)' %
(self.node_id, self.name, interface))
raise PatternError('%s: pattern \'%s\' - failed to add '
'interface %s: missing attribute (items)' %
(self.node_id, self.name, interface))
for intf, neighbors in interface.items():
(remote_device, remote_interface) = \
self.parse_interface(neighbors)
metadata = dict(interface=intf, neighbors=neighbors)
patterns = list()
if intf in ['none', 'any']:
patterns.append(InterfacePattern(intf, remote_device,
remote_interface,
self.node_id))
else:
for item in expand_range(intf):
pattern = InterfacePattern(item, remote_device,
remote_interface,
self.node_id)
patterns.append(pattern)
self.interfaces.append(dict(metadata=metadata,
patterns=patterns))
except InterfacePatternError:
log.error('%s: pattern \'%s\' - failed to add interface %s' %
(self.node_id, self.name, interface))
raise PatternError('%s: pattern \'%s\' - failed to add '
'interface %s' %
(self.node_id, self.name, interface))
[docs] def add_interfaces(self, interfaces):
try:
for interface in interfaces:
self.add_interface(interface)
except TypeError as err:
log.error('%s: pattern \'%s\' - failed to add interfaces %s: %s' %
(self.node_id, self.name, interface, str(err)))
raise PatternError('%s: pattern \'%s\' - failed to add '
'interfaces %s: %s' %
(self.node_id, self.name, interface, str(err)))
[docs] def match_node(self, node):
log.debug('%s: pattern \'%s\' - attempting to match node (%r)' %
(self.node_id, self.name, str(node)))
patterns = list()
for entry in self.interfaces:
for pattern in entry['patterns']:
patterns.append(pattern)
for interface, neighbors in node.neighbors.items():
log.debug('%s: pattern \'%s\' - attempting to match '
'interface %s(%s)' %
(self.node_id, self.name, interface, str(neighbors)))
match = False
for index, pattern in enumerate(patterns):
log.debug('%s: pattern \'%s\' - checking interface pattern '
'for %s: %s' %
(self.node_id, self.name, interface, pattern))
result = pattern.match(interface, neighbors)
# True, False, None
if result is True:
log.debug('%s: pattern \'%s\' - interface pattern match '
'for %s: %s' %
(self.node_id, self.name, interface, pattern))
del patterns[index]
match = True
break
elif result is False:
log.debug('%s: pattern \'%s\' - interface pattern match '
'failure for %s: %s' %
(self.node_id, self.name, interface, pattern))
return False
if not match:
log.debug('%s: pattern \'%s\' - interface %s did not match '
'any interface patterns' %
(self.node_id, self.name, interface))
for pattern in patterns:
if pattern.is_positive_constraint():
log.debug('%s: pattern \'%s\' - interface pattern %s did '
'not match any interface' %
(self.node_id, self.name, pattern))
return False
return True
[docs]class InterfacePattern(object):
KEYWORDS = {
'any': RegexFunction('.*'),
'none': RegexFunction('[^a-zA-Z0-9]')
}
FUNCTIONS = {
'exact': ExactFunction,
'includes': IncludesFunction,
'excludes': ExcludesFunction,
'regex': RegexFunction
}
def __init__(self, interface, remote_device, remote_interface, node_id):
match = re.match(r'^[ehnrtE]+(\d.*)$', interface)
if match:
self.interface = 'Ethernet%s' % match.groups()[0]
else:
self.interface = interface
self.remote_device = remote_device
self.remote_interface = remote_interface
self.node_id = node_id
self.remote_device_re = self.compile(remote_device)
self.remote_interface_re = self.compile(remote_interface)
def __repr__(self):
return 'InterfacePattern(interface=%s, remote_device=%s, ' \
'remote_interface=%s)' % \
(self.interface, self.remote_device, self.remote_interface)
[docs] def refresh(self):
self.remote_device_re = self.compile(self.remote_device)
self.remote_interface_re = self.compile(self.remote_interface)
[docs] def compile(self, value):
if value in self.KEYWORDS:
return self.KEYWORDS[value]
try:
match = FUNC_RE.match(value)
if match:
function = match.group('function')
arg = match.group('arg')
return self.FUNCTIONS[function](arg)
else:
return ExactFunction(value)
except KeyError as exc:
log.error('%s: compile error: unknown function \'%s\' (%s)' %
(self.node_id, function, str(exc)))
raise InterfacePatternError
[docs] def match(self, interface, neighbors):
for neighbor in neighbors:
res = self.match_neighbor(interface, Neighbor(neighbor.device,
neighbor.interface))
if res is True:
return True
elif res is False:
return False
return None
[docs] def match_neighbor(self, interface, neighbor):
# pylint: disable=R0911,R0912
log.debug('%s: attempting to match %s(%s) against '
'interface pattern %r' %
(self.node_id, interface, neighbor, self))
if self.interface == 'any':
if self.remote_device == 'any':
if self.remote_interface == 'any':
return True
elif self.remote_interface == 'none':
# bogus
return False
else:
if self.match_remote_interface(neighbor.interface):
return True
elif self.remote_device == 'none':
if self.remote_interface == 'any':
# bogus
return False
elif self.remote_interface == 'none':
# bogus
return False
else:
return False
else:
if self.remote_interface == 'any':
if self.match_remote_device(neighbor.device):
return True
elif self.remote_interface == 'none':
if self.match_remote_device(neighbor.device):
return False
else:
if(self.match_remote_device(neighbor.device) and
self.match_remote_interface(neighbor.interface)):
return True
elif self.interface == 'none':
if self.remote_device == 'any':
if self.remote_interface == 'any':
# bogus
return False
elif self.remote_interface == 'none':
# bogus
return False
else:
if self.match_remote_interface(neighbor.interface):
return False
elif self.remote_device == 'none':
if self.remote_interface == 'any':
# bogus
return False
elif self.remote_interface == 'none':
# no LLDP capable neighbors
return False
else:
# bogus
return False
else:
if self.remote_interface == 'any':
if self.match_remote_device(neighbor.device):
return False
elif self.remote_interface == 'none':
if self.match_remote_device(neighbor.device):
return False
else:
if(self.match_remote_device(neighbor.device) and
self.match_remote_interface(neighbor.interface)):
return False
else:
if self.remote_device == 'any':
if self.remote_interface == 'any':
if self.match_interface(interface):
return True
elif self.remote_interface == 'none':
if self.match_interface(interface):
return False
else:
if(self.match_interface(interface) and
self.match_remote_interface(neighbor.interface)):
return True
elif self.remote_device == 'none':
if self.remote_interface == 'any':
if self.match_interface(interface):
return False
elif self.remote_interface == 'none':
if self.match_interface(interface):
return False
else:
if(self.match_interface(interface) and
self.match_remote_interface(neighbor.interface)):
return False
elif self.match_interface(interface):
if self.remote_interface == 'any':
if self.match_remote_device(neighbor.device):
return True
elif self.remote_interface == 'none':
if self.match_remote_device(neighbor.device):
return False
else:
if(self.match_interface(interface) and
self.match_remote_device(neighbor.device) and
self.match_remote_interface(neighbor.interface)):
return True
return None
[docs] def match_interface(self, interface):
if self.interface == 'any':
return True
elif self.interface is None:
return False
else:
return self.interface == interface
[docs] def match_remote_device(self, remote_device):
if self.remote_device == 'any':
return True
elif self.remote_device is None:
return False
else:
return self.remote_device_re.match(remote_device)
[docs] def match_remote_interface(self, remote_interface):
if self.interface == 'any':
return True
elif self.interface is None:
return False
else:
return self.remote_interface_re.match(remote_interface)
[docs] def is_positive_constraint(self):
if self.interface == 'any':
if self.remote_device == 'any':
return True
elif self.remote_device != 'none':
return self.interface != 'none'
elif self.interface != 'none':
if self.remote_device == 'any':
return True
elif self.remote_device != 'none':
return self.interface != 'none'