Source code for ztpserver.topology

#
# 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'