From 814d19e2da410196f04f7917c26ace113236cefd Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 16:01:24 +0000 Subject: [PATCH 01/20] PR#2 Added additional caches to allow faster operationsx --- ncdiff/src/yang/ncdiff/config.py | 2 ++ ncdiff/src/yang/ncdiff/model.py | 33 +++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index 018937f..e3457f9 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -700,6 +700,8 @@ def check_models(models): @property def namespaces_compatible(self): + if self.config1.device == self.config2.device: + return True def check_models(models): for device in [self.config1.device, self.config2.device]: diff --git a/ncdiff/src/yang/ncdiff/model.py b/ncdiff/src/yang/ncdiff/model.py index 49e139e..6f45589 100755 --- a/ncdiff/src/yang/ncdiff/model.py +++ b/ncdiff/src/yang/ncdiff/model.py @@ -601,6 +601,20 @@ def __init__(self, folder): self.dir_yang = os.path.abspath(folder) self.build_dependencies() + def _xml_from_cache(self, name): + cached_name = os.path.join(self.dir_yang, f"{name}.xml") + if os.path.exists(cached_name): + with(open(cached_name, "r", encoding="utf-8")) as fh: + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.XML(fh.read(), parser) + return tree + return None + + def _to_cache(self, name, value): + cached_name = os.path.join(self.dir_yang, f"{name}.xml") + with open(cached_name, "wb") as fh: + fh.write(value) + def build_dependencies(self): '''build_dependencies @@ -614,6 +628,11 @@ def build_dependencies(self): Nothing returns. ''' + from_cache = self._xml_from_cache("$dependencies") + if from_cache is not None: + self.dependencies = from_cache + return + cmd_list = ['pyang', '--plugindir', self.pyang_plugins] cmd_list += ['-p', self.dir_yang] cmd_list += ['-f', 'pyimport'] @@ -624,6 +643,9 @@ def build_dependencies(self): logger.info('pyang return code is {}'.format(p.returncode)) logger.debug(stderr.decode()) parser = etree.XMLParser(remove_blank_text=True) + + self._to_cache("$dependencies",stdout) + self.dependencies = etree.XML(stdout.decode(), parser) def get_dependencies(self, module): @@ -676,6 +698,11 @@ def compile(self, module): Model A Model object. ''' + cached_tree = self._xml_from_cache(module) + + if cached_tree is not None: + m = Model(cached_tree) + return m imports, depends = self.get_dependencies(module) file_list = list(imports | depends) + [module] @@ -692,7 +719,11 @@ def compile(self, module): else: logger.error(stderr.decode()) parser = etree.XMLParser(remove_blank_text=True) - tree = etree.XML(stdout.decode(), parser) + + self._to_cache(module, stdout) + + out = stdout.decode() + tree = etree.XML(out, parser) return Model(tree) From 2a9ef5bc9c5b7a5f852e5377f4cada5144fe8314 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 16:40:48 +0000 Subject: [PATCH 02/20] PR#3 Improve performance --- ncdiff/src/yang/ncdiff/calculator.py | 304 ++++++++++++++++---- ncdiff/src/yang/ncdiff/config.py | 17 +- ncdiff/src/yang/ncdiff/device.py | 73 ++++- ncdiff/src/yang/ncdiff/netconf.py | 7 +- ncdiff/src/yang/ncdiff/ref.py | 398 +++++++++++++++++++++++++++ 5 files changed, 717 insertions(+), 82 deletions(-) create mode 100644 ncdiff/src/yang/ncdiff/ref.py diff --git a/ncdiff/src/yang/ncdiff/calculator.py b/ncdiff/src/yang/ncdiff/calculator.py index b1de400..2f21211 100755 --- a/ncdiff/src/yang/ncdiff/calculator.py +++ b/ncdiff/src/yang/ncdiff/calculator.py @@ -1,6 +1,9 @@ import logging +from functools import lru_cache + from ncclient import xml_ +from .ref import IdentityRef, InstanceIdentifier from .errors import ConfigError # create a logger for this module @@ -107,6 +110,103 @@ def _get_sequence(scope, tag, parent): return [child for child in parent.iterchildren(tag=tag) if child in new_scope] + def _pair_children(self, node_one, node_two): + """_pair_children + pair all children with their peers, resulting in a list of Tuples + Parameters + ---------- + node_one : `Element` + An Element node in one Config instance. + node_two : `Element` + An Element node in the other Config instance. + Returns + ------- + list + List of matching pairs, one of both items in the pair can be None + """ + # Hash based approach, build two hashtables and match + # keys are (tag, self-key) + # self-key is + # text for leaf-list + # key tuple for list + # none for others + + def find_one_child(node, tag): + """ Find exactly one child with the given tag""" + s = list(node.iterchildren(tag=tag)) + if len(s) < 1: + raise ConfigError("cannot find key '{}' in node {}" \ + .format(tag, + self.device.get_xpath(node))) + if len(s) > 1: + raise ConfigError("not unique key '{}' in node {}" \ + .format(tag, + self.device.get_xpath(node))) + return s[0] + + def build_index_tuple(node, keys, s_node): + """ + build a tuple containing the text of all fields listed in keys, taken from node + s_node is passed to prevent it being looked up twice. + """ + out = [self._parse_text(find_one_child(node, k), s_node) for k in keys] + return tuple(out) + + type_for_tag = {} + def get_type_for_tag(tag, child): + """ + Given a child node with a given tag, find the type + caches based on tag + :return: a tuple, containing the s_node and node type + """ + node_type = type_for_tag.get(tag, None) + if node_type is not None: + return node_type + s_node = self.device.get_schema_node(child) + node_type = s_node.get('type') + + result = (s_node, node_type) + type_for_tag[tag] = result + return result + + def build_unique_id(child): + """ + Build the hash key for a node + """ + tag = child.tag + key = None + s_node, node_type = get_type_for_tag(tag, child) + if node_type == 'leaf-list': + key = self._parse_text(child, s_node) + elif node_type == 'list': + keys = self._get_list_keys(s_node) + key = build_index_tuple(child, keys, s_node) + return (tag, key) + + # build the hashmap for node_one + ones = {} + for child in node_one.getchildren(): + key = build_unique_id(child) + if key in ones: + raise ConfigError('not unique peer of node {} {}' \ + .format(child, ones[key])) + ones[key] = child + + # build the hashmap for node_two + twos = {} + for child in node_two.getchildren(): + key = build_unique_id(child) + if key in twos: + raise ConfigError('not unique peer of node {} {}' \ + .format(child, twos[key])) + twos[key] = child + + # make pairs, in order + one_lookup = set(ones.keys()) + keys_in_order = list(ones.keys()) + keys_in_order.extend([two for two in twos.keys() if two not in one_lookup]) + return [(ones.get(uid, None), twos.get(uid, None)) for uid in keys_in_order] + def _group_kids(self, node_one, node_two): '''_group_kids @@ -139,30 +239,15 @@ def _group_kids(self, node_one, node_two): in_1_not_in_2 = [] in_2_not_in_1 = [] in_1_and_in_2 = [] - for child in node_one.getchildren(): - peers = self._get_peers(child, node_two) - if len(peers) < 1: - # child in self but not in other - in_1_not_in_2.append(child) - elif len(peers) > 1: - # one child in self matches multiple children in other - raise ConfigError('not unique peer of node {}' \ - .format(self.device.get_xpath(child))) - else: - # child matches one peer in other - in_1_and_in_2.append((child, peers[0])) - for child in node_two.getchildren(): - peers = self._get_peers(child, node_one) - if len(peers) < 1: - # child in other but not in self - in_2_not_in_1.append(child) - elif len(peers) > 1: - # one child in other matches multiple children in self - raise ConfigError('not unique peer of node {}' \ - .format(self.device.get_xpath(child))) + + for one, two in self._pair_children(node_one, node_two): + if one is None: + in_2_not_in_1.append(two) + elif two is None: + in_1_not_in_2.append(one) else: - # child in self matches one peer in self - pass + in_1_and_in_2.append((one, two)) + return (in_1_not_in_2, in_2_not_in_1, in_1_and_in_2) @staticmethod @@ -187,7 +272,7 @@ def _get_list_keys(schema_node): nodes = list(filter(lambda x: x.get('is_key'), schema_node.getchildren())) - return [n.tag for n in nodes] + return sorted([n.tag for n in nodes]) def _get_peers(self, child_self, parent_other): '''_get_peers @@ -264,6 +349,109 @@ def _is_peer(self, keys, node_self, node_other): return False return True + @lru_cache(maxsize=1024) + # cache because this is an expensive call, often called multiple times on the same node in rapid succession + def _parse_text(self, node, schema_node=None): + '''_parse_text + + Low-level api: Return text if a node. Pharsing is required if the node + data type is identityref or instance-identifier. + + Parameters + ---------- + + node : `Element` + An Element node in data tree. + + Returns + ------- + + None or str + None if the node does not have text, otherwise, text string of the + node. + ''' + + if node.text is None: + return None + + if schema_node is None: + schema_node = self.device.get_schema_node(node) + if schema_node.get('datatype') is not None and \ + (schema_node.get('datatype')[:11] == 'identityref' or schema_node.get('datatype')[:3] == '-> '): + idref = IdentityRef(self.device, node) + return idref.default + elif schema_node.get('datatype') is not None and \ + schema_node.get('datatype') == 'instance-identifier': + instanceid = InstanceIdentifier(self.device, node) + return instanceid.default + else: + return node.text + + + def _same_text(self, node1, node2): + '''_same_text + + Low-level api: Compare text values of two nodes. + + Parameters + ---------- + + node1 : `Element` + An Element node in data tree. + + node2 : `Element` + An Element node in data tree. + + Returns + ------- + + bool + True if text values of two nodes are same, otherwise, False. + ''' + + if node1.text is None and node2.text is None: + return True + return self._parse_text(node1) == self._parse_text(node2) + + def _merge_text(self, from_node, to_node): + '''_merge_text + + Low-level api: Set text value of to_node according to the text value of + from_node. + + Parameters + ---------- + + from_node : `Element` + An Element node in data tree. + + to_node : `Element` + An Element node in data tree. + + Returns + ------- + + None + There is no return of this method. + ''' + + if from_node.text is None: + to_node.text = None + return + schema_node = self.device.get_schema_node(from_node) + if schema_node.get('datatype') is not None and \ + schema_node.get('datatype')[:11] == 'identityref': + idref = IdentityRef(self.device, + from_node, to_node=to_node) + to_node.text = idref.converted + elif schema_node.get('datatype') is not None and \ + schema_node.get('datatype') == 'instance-identifier': + instanceid = InstanceIdentifier(self.device, + from_node, to_node=to_node) + to_node.text = instanceid.converted + else: + to_node.text = from_node.text + @property def le(self): return self._node_le(self.etree1, self.etree2) @@ -322,38 +510,40 @@ def _node_le(self, node_self, node_other): if a not in node_other.attrib or \ node_self.attrib[a] != node_other.attrib[a]: return False - for child in node_self.getchildren(): - peers = self._get_peers(child, node_other) - if len(peers) < 1: + for child, child_other in self._pair_children(node_self, node_other): + if child is None: + # only in other, meaningless + continue + if child_other is None: + # only in other, false return False - elif len(peers) > 1: - raise ConfigError('not unique peer of node {}' \ - .format(self.device.get_xpath(child))) - else: - schma_node = self.device.get_schema_node(child) - if schma_node.get('ordered-by') == 'user' and \ - schma_node.get('type') == 'leaf-list' or \ - schma_node.get('ordered-by') == 'user' and \ - schma_node.get('type') == 'list': - elder_siblings = list(child.itersiblings(tag=child.tag, - preceding=True)) - if elder_siblings: - immediate_elder_sibling = elder_siblings[0] - peers_of_immediate_elder_sibling = \ - self._get_peers(immediate_elder_sibling, - node_other) - if len(peers_of_immediate_elder_sibling) < 1: - return False - elif len(peers_of_immediate_elder_sibling) > 1: - p = self.device.get_xpath(immediate_elder_sibling) - raise ConfigError('not unique peer of node {}' \ - .format(p)) - elder_siblings_of_peer = \ - list(peers[0].itersiblings(tag=child.tag, - preceding=True)) - if peers_of_immediate_elder_sibling[0] not in \ - elder_siblings_of_peer: - return False - if not self._node_le(child, peers[0]): - return False + # both are present + schma_node = self.device.get_schema_node(child) + ordered_by = schma_node.get('ordered-by') + child_type = schma_node.get('type') + if ordered_by == 'user' and ( + child_type == 'leaf-list' or + child_type == 'list'): + elder_siblings = list(child.itersiblings(tag=child.tag, + preceding=True)) + if elder_siblings: + immediate_elder_sibling = elder_siblings[0] + peers_of_immediate_elder_sibling = \ + self._get_peers(immediate_elder_sibling, + node_other) + if len(peers_of_immediate_elder_sibling) < 1: + return False + elif len(peers_of_immediate_elder_sibling) > 1: + p = self.device.get_xpath(immediate_elder_sibling) + raise ConfigError('not unique peer of node {}' \ + .format(p)) + elder_siblings_of_peer = \ + list(child_other.itersiblings(tag=child.tag, + preceding=True)) + if peers_of_immediate_elder_sibling[0] not in \ + elder_siblings_of_peer: + return False + if not self._node_le(child, child_other): + return False + return True diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index e3457f9..4f02f84 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -62,7 +62,7 @@ class Config(object): `{url}tagname` notation, and values are corresponding model names. ''' - def __init__(self, ncdevice, config=None): + def __init__(self, ncdevice, config=None, validate=True): ''' __init__ instantiates a Config instance. ''' @@ -86,7 +86,8 @@ def __init__(self, ncdevice, config=None): raise TypeError("argument 'config' must be None, XML string, " \ "or Element, but not '{}'" \ .format(type(config))) - self.validate_config() + if validate: + self.validate_config() def __repr__(self): return '<{}.{} {} at {}>'.format(self.__class__.__module__, @@ -100,7 +101,7 @@ def __str__(self): pretty_print=True) def __bool__(self): - d = Config(self.device, None) + d = Config(self.device, None, False) if self == d: return False else: @@ -111,21 +112,21 @@ def __add__(self, other): if ConfigCompatibility(self, other).is_compatible: return Config(self.device, NetconfCalculator(self.device, - self.ele, other.ele).add) + self.ele, other.ele).add, False) elif isinstance(other, ConfigDelta): if ConfigCompatibility(self, other).is_compatible: return Config(self.device, NetconfCalculator(self.device, - self.ele, other.nc).add) + self.ele, other.nc).add, False) elif etree.iselement(other): return Config(self.device, - NetconfCalculator(self.device, self.ele, other).add) + NetconfCalculator(self.device, self.ele, other).add, False) elif isinstance(other, Request): return Config(self.device, - RestconfCalculator(self.device, self.ele, other).add) + RestconfCalculator(self.device, self.ele, other).add, False) elif isinstance(other, SetRequest): return Config(self.device, - gNMICalculator(self.device, self.ele, other).add) + gNMICalculator(self.device, self.ele, other).add, False) else: return NotImplemented diff --git a/ncdiff/src/yang/ncdiff/device.py b/ncdiff/src/yang/ncdiff/device.py index 7b4b313..30beacd 100755 --- a/ncdiff/src/yang/ncdiff/device.py +++ b/ncdiff/src/yang/ncdiff/device.py @@ -1,5 +1,7 @@ import os import re +from functools import lru_cache + import logging from lxml import etree from ncclient import manager, operations, transport, xml_ @@ -7,7 +9,7 @@ from .model import Model, ModelDownloader, ModelCompiler from .config import Config -from .errors import ModelError, ModelMissing +from .errors import ModelError, ModelMissing, ConfigError from .composer import Tag, Composer # create a logger for this module @@ -78,6 +80,8 @@ def __repr__(self): hex(id(self))) @property + @lru_cache(maxsize=1) + # extremely expensive call, cache def namespaces(self): if self.compiler is None: raise ValueError('please first call scan_models() to build ' @@ -623,23 +627,28 @@ def is_parent(node1, node2): return True n = Composer(self, config_node) - config_path = n.path - if ' '.join(config_path) in self.nodes: - return self.nodes[' '.join(config_path)] - if len(config_path) > 1: + path = n.path + config_path_str = ' '.join(path) + if config_path_str in self.nodes: + return self.nodes[config_path_str] + if len(path) > 1: parent = self.get_schema_node(config_node.getparent()) - if parent is None: - return None - else: - child = get_child(parent, config_node.tag) - if child is not None: - self.nodes[' '.join(config_path)] = child - return child + child = get_child(parent, config_node.tag) + if child is None: + raise ConfigError("unable to locate a child '{}' of {} in " \ + "schema tree" \ + .format(config_node.tag, + self.get_xpath(parent))) + self.nodes[config_path_str] = child + return child else: tree = self.models[n.model_name].tree child = get_child(tree, config_node.tag) - if child is not None: - self.nodes[' '.join(config_path)] = child + if child is None: + raise ConfigError("unable to locate a root '{}' in {} schema " \ + "tree" \ + .format(config_node.tag, n.model_name)) + self.nodes[config_path_str] = child return child def get_model_name(self, node): @@ -826,6 +835,42 @@ def convert(ns): else: raise ValueError("unknown value '{}' in class Tag".format(dst[1])) + def convert_ns(self, ns, src=Tag.NAMESPACE, dst=Tag.NAME): + '''convert_ns + + High-level api: Convert from one namespace format, model name, model + prefix or model URL, to another namespace format. + + Parameters + ---------- + + ns : `str` + A namespace, which can be model name, model prefix or model URL. + + src : `int` + An int constant defined in class Tag, specifying the namespace + format of ns. + + dst : `int` + An int constant defined in class Tag, specifying the namespace + format of return value. + + Returns + ------- + + str + Converted namespace in a format specified by dst. + ''' + + matches = [t for t in self.namespaces if t[src] == ns] + if len(matches) == 0: + raise ValueError("{} '{}' is not claimed by this device" \ + .format(Tag.STR[src], ns)) + if len(matches) > 1: + raise ValueError("more than one {} '{}' are claimed by this " \ + "device".format(Tag.STR[src], ns)) + return matches[0][dst] + def _get_ns(self, reply): '''_get_ns diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index 0ba664c..25587a9 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -710,13 +710,14 @@ def same_leaf_list(tag): list_other = [c for c in list(node_other) if c.tag == tag] s_node = self.device.get_schema_node((list_self + list_other)[0]) if s_node.get('ordered-by') == 'user': - if [i.text for i in list_self] == [i.text for i in list_other]: + if [self._parse_text(i, s_node) for i in list_self] == \ + [self._parse_text(i, s_node) for i in list_other]: return True else: return False else: - if set([i.text for i in list_self]) == \ - set([i.text for i in list_other]): + if set([self._parse_text(i, s_node) for i in list_self]) == \ + set([self._parse_text(i, s_node) for i in list_other]): return True else: return False diff --git a/ncdiff/src/yang/ncdiff/ref.py b/ncdiff/src/yang/ncdiff/ref.py new file mode 100644 index 0000000..e7b03cf --- /dev/null +++ b/ncdiff/src/yang/ncdiff/ref.py @@ -0,0 +1,398 @@ +import re + +from .errors import ConfigError +from .composer import Tag + + +class IdentityRef(object): + '''IdentityRef + + A class to process YANG built-in type identityref. + + Attributes + ---------- + device : `object` + An instance of yang.ncdiff.ModelDevice, which represents a modeled + device. + + node : `Element` + A data node of type identityref. + + to_node : `Element` + Another data node of type identityref. + + converted : `str` + Converted text value of node, based on to_node.nsmap. + ''' + + def __init__(self, device, node, to_node=None): + ''' + __init__ instantiates a IdentityRef instance. + ''' + + self.device = device + self.node = node + self.to_node = to_node + self.node_url = self.device.convert_tag('', self.node.tag)[0] + self._default_ns = {} + + @property + def converted(self): + if self.node.text is None: + raise ConfigError("the node with tag {} is an identityref but no " \ + "value" \ + .format(self.node.tag)) + return self.convert(self.node.text, to_node=self.to_node) + + @property + def default(self): + if self.node.text is None: + raise ConfigError("the node with tag {} is an identityref but no " \ + "value" \ + .format(self.node.tag)) + return self.convert(self.node.text, to_node=None) + + @property + def default_ns(self): + self._default_ns = {None: self.node_url} + self.default + return self._default_ns + + def parse_prefixed_id(self, id, node): + match = re.search(Tag.COLON[0], id) + if match: + if match.group(1) in node.nsmap: + return node.nsmap[match.group(1)], match.group(2) + elif match.group(1) in [ns[0] for ns in self.device.namespaces]: + name_to_url = {ns[0]: ns[2] for ns in self.device.namespaces} + return name_to_url[match.group(1)], match.group(2) + else: + raise ConfigError("unknown prefix '{}' in the node with tag " \ + "{}" \ + .format(match.group(1), node.tag)) + else: + # RFC7950 section 9.10.3 and RFC7951 section 6.8 + if None in node.nsmap: + return node.nsmap[None], id + else: + raise ConfigError("fail to find default namespace of " \ + "the node with tag {}" \ + .format(node.tag)) + + def compose_prefixed_id(self, url, id, to_node=None): + def url_to_name(url, ns): + model_name = self.device.convert_ns(url, + src=Tag.NAMESPACE, + dst=Tag.NAME) + if url not in ns.values(): + ns[model_name] = url + return model_name + + if to_node is None: + # RFC7951 section 6.8 + if url == self.node_url: + return id + else: + return '{}:{}'.format(url_to_name(url, self._default_ns), id) + else: + # RFC7950 section 9.10.3 + url_to_prefix = {v: k for k, v in to_node.nsmap.items()} + if url in url_to_prefix: + if url_to_prefix[url] is None: + return '{}'.format(id) + else: + return '{}:{}'.format(url_to_prefix[url], id) + else: + raise ConfigError("URL '{}' is not found in to_node.nsmap " \ + "{}, where the to_node has tag {}" \ + .format(url, to_node.nsmap, to_node.tag)) + + def convert(self, tag, to_node=None): + url, id = self.parse_prefixed_id(tag, self.node) + return self.compose_prefixed_id(url, id, to_node=to_node) + + def parse_instanceid(self, node, to_node=None): + if node.text is None: + raise ConfigError("the node with tag {} is an " \ + "instance-identifier but no value" \ + .format(node.tag)) + tag = '' + new_instanceid = '' + expecting = '*./' + for char in node.text: + if char in '/.[]=*': + new_instanceid += char + if tag: + url, id = self.parse_prefixed_id(tag, node) + tag = '' + new_instanceid += self.compose_prefixed_id(url, id, + to_node=to_node) + else: + tag += char + if tag: + url, id = self.parse_prefixed_id(tag, node) + new_instanceid += self.compose_prefixed_id(url, id, + to_node=to_node) + return new_instanceid + + +class InstanceIdentifier(IdentityRef): + '''InstanceIdentifier + + A class to process YANG built-in type instance-identifier. + + Attributes + ---------- + device : `object` + An instance of yang.ncdiff.ModelDevice, which represents a modeled + device. + + node : `Element` + A data node of type instance-identifier. + + to_node : `Element` + Another data node of type instance-identifier. + + converted : `str` + Converted text value of node, based on to_node.nsmap. + ''' + + def __init__(self, device, node, to_node=None): + ''' + __init__ instantiates a InstanceIdentifier instance. + ''' + + IdentityRef.__init__(self, device, node, to_node=to_node) + self.str_list = [(self.node.text, 0, self.node.text)] + + @property + def converted(self): + self.str_list = [(self.node.text, 0, self.node.text)] + self.parse_quote() + self.parse_square_bracket(to_node=self.to_node) + self.parse_element(to_node=self.to_node) + self.convert_str_list(to_node=self.to_node) + return ''.join([p[2] for p in self.str_list]) + + @property + def default(self): + self.str_list = [(self.node.text, 0, self.node.text)] + self.parse_quote() + self.parse_square_bracket() + self.parse_element() + self.convert_str_list() + return ''.join([p[2] for p in self.str_list]) + + def string(self, phase_num): + return ''.join([p[0] for p in self.str_list + if p[1] == 0 or p[1] >= phase_num]) + + def parse_prefixed_id(self, id, node): + match = re.search(Tag.COLON[0], id) + if match: + if match.group(1) in node.nsmap: + return node.nsmap[match.group(1)], match.group(2) + elif match.group(1) in [ns[0] for ns in self.device.namespaces]: + name_to_url = {ns[0]: ns[2] for ns in self.device.namespaces} + return name_to_url[match.group(1)], match.group(2) + else: + raise ConfigError("unknown prefix '{}' in the node with tag " \ + "{}" \ + .format(match.group(1), node.tag)) + else: + # RFC7950 section 9.13.2 and RFC7951 section 6.11 + return None, id + + def compose_prefixed_id(self, url, id, to_node=None): + def url_to_name(url, ns): + model_name = self.device.convert_ns(url, + src=Tag.NAMESPACE, + dst=Tag.NAME) + if url not in ns.values(): + ns[model_name] = url + return model_name + + # RFC7950 section 9.13.2 + if to_node is None: + return '{}:{}'.format(url_to_name(url, self._default_ns), id) + else: + url_to_prefix = {v: k for k, v in to_node.nsmap.items() + if k is not None} + if url in url_to_prefix: + return '{}:{}'.format(url_to_prefix[url], id) + else: + raise ConfigError("URL '{}' is not found in to_node.nsmap {} " \ + "(default namespace cannot be used here), " \ + "where the to_node has tag {}" \ + .format(url, to_node.nsmap, to_node.tag)) + + def convert_str_list(self, to_node=None): + default_url = None + new_str_list = [] + for piece in self.str_list: + if piece[1] <= 1: + new_str_list.append(piece) + elif piece[1] == 2: + if piece[0] == '[' or \ + piece[0] == ']' or \ + piece[0] == '=' or \ + piece[0] == '.': + new_str_list.append((piece[0], piece[1], piece[0])) + else: + url, id = self.parse_prefixed_id(piece[0], self.node) + if url is None: + url = default_url + if url is None: + raise ConfigError("in the instance-identifier node " \ + "with tag {}, the leftmost data " \ + "node name '{}' is not in " \ + "namespace-qualified form" \ + .format(self.node.tag, piece[0])) + else: + converted_id = self.compose_prefixed_id(url, id, + to_node=to_node) + new_str_list.append((piece[0], piece[1], converted_id)) + elif piece[1] == 3: + url, id = self.parse_prefixed_id(piece[0], self.node) + if url is not None: + default_url = url + if default_url is None: + raise ConfigError("in the instance-identifier node " \ + "with tag {}, the leftmost data " \ + "node name '{}' is not in " \ + "namespace-qualified form" \ + .format(self.node.tag, piece[0])) + else: + converted_id = self.compose_prefixed_id(default_url, id, + to_node=to_node) + new_str_list.append((piece[0], piece[1], converted_id)) + self.str_list = new_str_list + + def cut(self, start_idx, end_idx, converted_str, phase_num): + def this_piece(idx, start_idx, end_idx, converted_str, phase_num): + string = self.str_list[idx][0] + original_phase_num = self.str_list[idx][1] + ret = [(string[:start_idx], original_phase_num, string[:start_idx]), + (string[start_idx:end_idx], phase_num, converted_str), + (string[end_idx:], original_phase_num, string[end_idx:])] + return [piece for piece in ret if piece[0]] + + def all_pieces(idx, start_idx, end_idx, converted_str, phase_num): + return self.str_list[:idx] + \ + this_piece(idx, start_idx, end_idx, + converted_str, phase_num) + \ + self.str_list[idx+1:] + + str_len = end_idx - start_idx + position = 0 + for idx, piece in enumerate(self.str_list): + if piece[1] == 0 or piece[1] == phase_num: + end_position = position + len(piece[0]) + if start_idx >= position and end_idx <= end_position: + self.str_list = all_pieces(idx, + start_idx-position, + end_idx-position, + converted_str, + phase_num) + return + position += len(piece[0]) + + @staticmethod + def convert_literal(literal): + converted_literal = literal[1:-1] + converted_literal = re.sub("'", "''", converted_literal) + return "'" + re.sub('""', '"', converted_literal) + "'" + + def parse_quote(self): + string = self.string(1) + start_idx = None + start_escape = None + for idx, char in enumerate(string): + if start_idx is None and char == '=': + start_idx = idx + 1 + elif start_idx is not None and idx == start_idx: + if char != "'" and char != '"': + raise ConfigError("do not see a apostrophe or double " \ + "quote after '=' in the node with tag " \ + "{}" \ + .format(self.node.tag)) + else: + opening_quote = char + elif start_idx is not None and idx > start_idx: + if char == opening_quote: + if idx < len(string)-1 and string[idx+1] == opening_quote: + start_escape = idx + continue + end_escape = idx + 1 + if start_escape is not None and \ + (end_escape-start_escape)%2 == 0: + start_escape = None + continue + end_idx = idx + 1 + if opening_quote == '"': + default_literal = \ + self.convert_literal(string[start_idx:end_idx]) + else: + default_literal = string[start_idx:end_idx] + self.cut(start_idx, end_idx, default_literal, 1) + start_idx = None + if start_idx is not None: + raise ConfigError('found opening apostrophe or double quote, but ' \ + 'not the closing one in the node with tag {}' \ + .format(self.node.tag)) + + def parse_square_bracket(self, to_node=None): + string = self.string(2) + start_idx = None + for idx, char in enumerate(string): + if start_idx is None and char == '[': + start_idx = idx + elif start_idx is not None and idx > start_idx: + if char == "]": + end_idx = idx + 1 + substring = string[start_idx:end_idx] + self.cut(start_idx, start_idx+1, '[', 2) + self.cut(end_idx-1, end_idx, ']', 2) + if substring[-2] == '=': + tag = substring[1:-2] + self.cut(end_idx-2, end_idx-1, '=', 2) + if tag == '.': + self.cut(start_idx+1, end_idx-2, '.', 2) + else: + self.cut(start_idx+1, end_idx-2, tag, 2) + start_idx = None + else: + if re.search('^\[[1-9][0-9]*\]$', substring): + numbers = substring[1:-1] + self.cut(start_idx+1, end_idx-1, numbers, 2) + else: + tag = substring[1:-1] + self.cut(start_idx+1, end_idx-1, tag, 2) + start_idx = None + if start_idx is not None: + raise ConfigError('found opening square bracket, but not the ' \ + 'closing bracket in the node with tag {}' \ + .format(self.node)) + + def parse_element(self, to_node=None): + SEPARATORS = "./" + string = self.string(3) + start_idx = None + for idx, char in enumerate(string): + if start_idx is None and char not in SEPARATORS: + start_idx = idx + elif start_idx is not None and char in SEPARATORS: + end_idx = idx + tag = string[start_idx:end_idx] + if tag == '*': + self.cut(start_idx, end_idx, '*', 3) + else: + self.cut(start_idx, end_idx, tag, 3) + start_idx = None + if start_idx is not None: + end_idx = idx + 1 + tag = string[start_idx:end_idx] + if tag == '*': + self.cut(start_idx, end_idx, '*', 3) + else: + self.cut(start_idx, end_idx, tag, 3) \ No newline at end of file From 145dc32f1927386658709bcd2398a47240c71625 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 16:48:38 +0000 Subject: [PATCH 03/20] PR#5 Remove whitespace from containers --- ncdiff/src/yang/ncdiff/calculator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ncdiff/src/yang/ncdiff/calculator.py b/ncdiff/src/yang/ncdiff/calculator.py index 2f21211..fa62ad6 100755 --- a/ncdiff/src/yang/ncdiff/calculator.py +++ b/ncdiff/src/yang/ncdiff/calculator.py @@ -385,6 +385,9 @@ def _parse_text(self, node, schema_node=None): instanceid = InstanceIdentifier(self.device, node) return instanceid.default else: + if schema_node.get("type") == "container": + # prevent whitespace in container to cause problems + return None return node.text From a3a490c0833fc0e6ecc10f696deecb1bc21d98d9 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 16:51:45 +0000 Subject: [PATCH 04/20] #PR#8 compare leafrefs by value --- ncdiff/src/yang/ncdiff/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ncdiff/src/yang/ncdiff/calculator.py b/ncdiff/src/yang/ncdiff/calculator.py index fa62ad6..fec254b 100755 --- a/ncdiff/src/yang/ncdiff/calculator.py +++ b/ncdiff/src/yang/ncdiff/calculator.py @@ -377,7 +377,7 @@ def _parse_text(self, node, schema_node=None): if schema_node is None: schema_node = self.device.get_schema_node(node) if schema_node.get('datatype') is not None and \ - (schema_node.get('datatype')[:11] == 'identityref' or schema_node.get('datatype')[:3] == '-> '): + (schema_node.get('datatype')[:11] == 'identityref'): idref = IdentityRef(self.device, node) return idref.default elif schema_node.get('datatype') is not None and \ From 8e87c0b3c31b837c53c13eaf4f37e4946920ecef Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 17:05:10 +0000 Subject: [PATCH 05/20] PR#13 Remove memory leak introduced in #2" --- ncdiff/src/yang/ncdiff/calculator.py | 16 +++++++++++++++- ncdiff/src/yang/ncdiff/device.py | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/calculator.py b/ncdiff/src/yang/ncdiff/calculator.py index fec254b..a84f4d2 100755 --- a/ncdiff/src/yang/ncdiff/calculator.py +++ b/ncdiff/src/yang/ncdiff/calculator.py @@ -45,6 +45,7 @@ def __init__(self, device, etree1, etree2): self.device = device self.etree1 = etree1 self.etree2 = etree2 + self.__attach_per_instance_cache() @staticmethod def _del_attrib(element): @@ -349,7 +350,20 @@ def _is_peer(self, keys, node_self, node_other): return False return True - @lru_cache(maxsize=1024) + def __attach_per_instance_cache(self): + """ + We want to cache _parse_text, as it is an expensive call + Python functools conveniently provides the @lru_cache annotation + (and no other way of having a pre-made lru_cache) + The lru_cache annotation however locks all arguments in memory, including self + This causes both trees for this calculator to be locked in memory + We want to use the @lru_cache annotation, but release the cache when this calculator is garbage collected + For this, we use the following construction: + https://stackoverflow.com/questions/14946264/python-lru-cache-decorator-per-instance + """ + + self._parse_text = lru_cache(maxsize=128)(self._parse_text) + # cache because this is an expensive call, often called multiple times on the same node in rapid succession def _parse_text(self, node, schema_node=None): '''_parse_text diff --git a/ncdiff/src/yang/ncdiff/device.py b/ncdiff/src/yang/ncdiff/device.py index 30beacd..4fe6ffa 100755 --- a/ncdiff/src/yang/ncdiff/device.py +++ b/ncdiff/src/yang/ncdiff/device.py @@ -1,6 +1,5 @@ import os import re -from functools import lru_cache import logging from lxml import etree @@ -73,6 +72,7 @@ def __init__(self, *args, **kwargs): self.nodes = {} self.compiler = None self._models_loadable = None + self.namespaces_cache = None def __repr__(self): return '<{}.{} object at {}>'.format(self.__class__.__module__, @@ -80,9 +80,10 @@ def __repr__(self): hex(id(self))) @property - @lru_cache(maxsize=1) # extremely expensive call, cache def namespaces(self): + if self.namespaces_cache is not None: + return self.namespaces_cache if self.compiler is None: raise ValueError('please first call scan_models() to build ' 'up supported namespaces of a device') @@ -92,6 +93,7 @@ def namespaces(self): device_namespaces.append((m.get('id'), m.get('prefix'), m.findtext('namespace'))) + self.namespaces_cache = device_namespaces return device_namespaces @property From 39135cf02db211d88cb7c722bc21cbde1fac6c02 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 17:07:48 +0000 Subject: [PATCH 06/20] PR#14 Improved handling of cache corruption --- ncdiff/src/yang/ncdiff/model.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/model.py b/ncdiff/src/yang/ncdiff/model.py index 6f45589..cf82601 100755 --- a/ncdiff/src/yang/ncdiff/model.py +++ b/ncdiff/src/yang/ncdiff/model.py @@ -602,12 +602,16 @@ def __init__(self, folder): self.build_dependencies() def _xml_from_cache(self, name): - cached_name = os.path.join(self.dir_yang, f"{name}.xml") - if os.path.exists(cached_name): - with(open(cached_name, "r", encoding="utf-8")) as fh: - parser = etree.XMLParser(remove_blank_text=True) - tree = etree.XML(fh.read(), parser) - return tree + try: + cached_name = os.path.join(self.dir_yang, f"{name}.xml") + if os.path.exists(cached_name): + with(open(cached_name, "r", encoding="utf-8")) as fh: + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.XML(fh.read(), parser) + return tree + except Exception: + # make the cache safe: any failure will just bypass the cache + logger.info(f"Unexpected failure during cache read of {name}, refreshing cache", exc_info=True) return None def _to_cache(self, name, value): From ad5510a53db516969002e2cd330645449f31ac5b Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 17:25:25 +0000 Subject: [PATCH 07/20] PR#19 fixed undefined variable --- ncdiff/src/yang/ncdiff/model.py | 225 +++++++++++++++++++++++++++++++- 1 file changed, 224 insertions(+), 1 deletion(-) diff --git a/ncdiff/src/yang/ncdiff/model.py b/ncdiff/src/yang/ncdiff/model.py index cf82601..867198e 100755 --- a/ncdiff/src/yang/ncdiff/model.py +++ b/ncdiff/src/yang/ncdiff/model.py @@ -1,12 +1,22 @@ import math import os import re +import queue import logging from lxml import etree from copy import deepcopy from ncclient import operations +from threading import Thread, current_thread +from pyang import statements from subprocess import PIPE, Popen - +try: + from pyang.repository import FileRepository +except ImportError: + from pyang import FileRepository +try: + from pyang.context import Context +except ImportError: + from pyang import Context from .errors import ModelError # create a logger for this module @@ -431,6 +441,215 @@ def convert_tree(self, element1, element2=None): self.convert_tree(e1, e2) return element2 +class DownloadWorker(Thread): + + def __init__(self, downloader): + Thread.__init__(self) + self.downloader = downloader + + def run(self): + while not self.downloader.download_queue.empty(): + try: + module = self.downloader.download_queue.get(timeout=0.01) + except queue.Empty: + pass + else: + self.downloader.download(module) + self.downloader.download_queue.task_done() + logger.debug('Thread {} exits'.format(current_thread().name)) + +class ContextWorker(Thread): + + def __init__(self, context): + Thread.__init__(self) + self.context = context + + def run(self): + varnames = Context.add_module.__code__.co_varnames + while not self.context.modulefile_queue.empty(): + try: + modulefile = self.context.modulefile_queue.get(timeout=0.01) + except queue.Empty: + pass + else: + with open(modulefile, 'r', encoding='utf-8') as f: + text = f.read() + kwargs = { + 'ref': modulefile, + 'text': text, + } + if 'primary_module' in varnames: + kwargs['primary_module'] = True + if 'format' in varnames: + kwargs['format'] = 'yang' + if 'in_format' in varnames: + kwargs['in_format'] = 'yang' + module_statement = self.context.add_module(**kwargs) + self.context.update_dependencies(module_statement) + self.context.modulefile_queue.task_done() + logger.debug('Thread {} exits'.format(current_thread().name)) + +class CompilerContext(Context): + + def __init__(self, repository): + Context.__init__(self, repository) + self.dependencies = None + self.modulefile_queue = None + if 'prune' in dir(statements.Statement): + self.num_threads = 2 + else: + self.num_threads = 1 + + def _get_latest_revision(self, modulename): + latest = None + for module_name, module_revision in self.modules: + if module_name == modulename and ( + latest is None or module_revision > latest + ): + latest = module_revision + return latest + + def get_statement(self, modulename, xpath=None): + revision = self._get_latest_revision(modulename) + if revision is None: + return None + if xpath is None: + return self.modules[(modulename, revision)] + + # in order to follow the Xpath, the module is required to be validated + node_statement = self.modules[(modulename, revision)] + if node_statement.i_is_validated is not True: + return None + + # xpath is given, so find the node statement + xpath_list = xpath.split('/') + + # only absolute Xpaths are supported + if len(xpath_list) < 2: + return None + if ( + xpath_list[0] == '' and xpath_list[1] == '' or + xpath_list[0] != '' + ): + return None + + # find the node statement + root_prefix = node_statement.i_prefix + for n in xpath_list[1:]: + node_statement = self.get_child(root_prefix, node_statement, n) + if node_statement is None: + return None + return node_statement + + def get_child(self, root_prefix, parent, child_id): + child_id_list = child_id.split(':') + if len(child_id_list) > 1: + children = [ + c for c in parent.i_children + if c.arg == child_id_list[1] and + c.i_module.i_prefix == child_id_list[0] + ] + elif len(child_id_list) == 1: + children = [ + c for c in parent.i_children + if c.arg == child_id_list[0] and + c.i_module.i_prefix == root_prefix + ] + return children[0] if children else None + + def update_dependencies(self, module_statement): + if self.dependencies is None: + self.dependencies = etree.Element('modules') + for m in [ + m for m in self.dependencies + if m.attrib.get('id') == module_statement.arg + ]: + self.dependencies.remove(m) + module_node = etree.SubElement(self.dependencies, 'module') + module_node.set('id', module_statement.arg) + module_node.set('type', module_statement.keyword) + if module_statement.keyword == 'module': + statement = module_statement.search_one('prefix') + if statement is not None: + module_node.set('prefix', statement.arg) + statement = module_statement.search_one("namespace") + if statement is not None: + namespace = etree.SubElement(module_node, 'namespace') + namespace.text = statement.arg + if module_statement.keyword == 'submodule': + statement = module_statement.search_one("belongs-to") + if statement is not None: + belongs_to = etree.SubElement(module_node, 'belongs-to') + belongs_to.set('module', statement.arg) + + dependencies = set() + for parent_node_name, child_node_name, attr_name in [ + ('includes', 'include', 'module'), + ('imports', 'import', 'module'), + ('revisions', 'revision', 'date'), + ]: + parent = etree.SubElement(module_node, parent_node_name) + statements = module_statement.search(child_node_name) + if statements: + for statement in statements: + child = etree.SubElement(parent, child_node_name) + child.set(attr_name, statement.arg) + if child_node_name in ['include', 'import']: + dependencies.add(statement.arg) + return dependencies + + def write_dependencies(self): + dependencies_file = os.path.join( + self.repository.dirs[0], + 'dependencies.xml', + ) + write_xml(dependencies_file, self.dependencies) + + def read_dependencies(self): + dependencies_file = os.path.join( + self.repository.dirs[0], + 'dependencies.xml', + ) + self.dependencies = read_xml(dependencies_file) + + def load_context(self): + self.modulefile_queue = queue.Queue() + for filename in os.listdir(self.repository.dirs[0]): + if filename.lower().endswith('.yang'): + filepath = os.path.join(self.repository.dirs[0], filename) + self.modulefile_queue.put(filepath) + for x in range(self.num_threads): + worker = ContextWorker(self) + worker.daemon = True + worker.name = 'context_worker_{}'.format(x) + worker.start() + self.modulefile_queue.join() + self.write_dependencies() + + def validate_context(self): + revisions = {} + for mudule_name, module_revision in self.modules: + if mudule_name not in revisions or ( + mudule_name in revisions and + revisions[mudule_name] < module_revision + ): + revisions[mudule_name] = module_revision + self.validate() + if 'prune' in dir(statements.Statement): + for mudule_name, module_revision in revisions.items(): + self.modules[(mudule_name, module_revision)].prune() + + def internal_reset(self): + self.modules = {} + self.revs = {} + self.errors = [] + for mod, rev, handle in self.repository.get_modules_and_revisions( + self): + if mod not in self.revs: + self.revs[mod] = [] + revs = self.revs[mod] + revs.append((rev, handle)) + class ModelDownloader(object): '''ModelDownloader @@ -467,6 +686,10 @@ def __init__(self, nc_device, folder): if not os.path.isdir(self.dir_yang): os.makedirs(self.dir_yang) self.yang_capabilities = self.dir_yang + '/capabilities.txt' + repo = FileRepository(path=self.dir_yang) + self.context = CompilerContext(repository=repo) + self.download_queue = queue.Queue() + self.num_threads = 2 @property def need_download(self): From fb12d78f480cadeb66bd3c3ad535fe4a1b9d9996 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 17:34:35 +0000 Subject: [PATCH 08/20] PR#21 fow monitor order does not matter --- ncdiff/src/yang/ncdiff/runningconfig.py | 541 +++++++++++++--- .../yang/ncdiff/tests/test_running_config.py | 597 ++++++++++++++++++ 2 files changed, 1058 insertions(+), 80 deletions(-) create mode 100644 ncdiff/src/yang/ncdiff/tests/test_running_config.py diff --git a/ncdiff/src/yang/ncdiff/runningconfig.py b/ncdiff/src/yang/ncdiff/runningconfig.py index 227fc29..a8aa3d7 100755 --- a/ncdiff/src/yang/ncdiff/runningconfig.py +++ b/ncdiff/src/yang/ncdiff/runningconfig.py @@ -1,58 +1,182 @@ import re import logging -from copy import deepcopy -from collections import OrderedDict + # create a logger for this module logger = logging.getLogger(__name__) +# Some no commands are not forgiving, e.g., "no license boot level +# network-advantage addon dna-advantage" is rejected, but "no license boot +# level" is acceptable. These cases are listed in SHORT_NO_COMMANDS. +SHORT_NO_COMMANDS = [ + 'no license boot level ', + 'no transport input ', + 'no transport output ', + 'no ip address ', + 'no ipv6 address ', +] + +# Some commands are orderless, e.g., "show running-config" output could be: +# aaa authentication login admin-con group tacacs+ local +# aaa authentication login admin-vty group tacacs+ local +# Or in other times it is displayed in a different order: +# aaa authentication login admin-vty group tacacs+ local +# aaa authentication login admin-con group tacacs+ local +ORDERLESS_COMMANDS = [ + r'^ *aaa authentication login ', + r'^ *logging host ', + r'^ *flow monitor ', +] + +# Some commands can be overwritten without a no command. For example, changing +# from: +# username admin privilege 15 password 0 admin +# to: +# username admin privilege 15 password 7 15130F010D24 +# There is no need to send a no command before sending the second line. +OVERWRITABLE_COMMANDS = [ + r'^ *username \S+ privilege [0-9]+ password ', + r'^ *password ', + r'^ *description ', + r'^ *ip address( |$)', + r'^ *ipv6 address( |$)', +] + +# Some commands look like a parent-child relation but actually they are +# siblings. One example is two lines of client config below: +# aaa server radius proxy +# client 10.0.0.0 255.0.0.0 +# ! +# client 11.0.0.0 255.0.0.0 +# ! +SIBLING_CAMMANDS = [ + r'^ *client ', +] + +# As for the client command above, its children does not have indentation: +# aaa server radius proxy +# client 10.0.0.0 255.0.0.0 +# timer disconnect acct-stop 23 +# ! +# client 11.0.0.0 255.0.0.0 +# accounting port 34 +# timer disconnect acct-stop 22 +# ! +# client 12.0.0.0 255.0.0.0 +# accounting port 56 +# ! +# Logically, "accounting port 34" and "timer disconnect acct-stop 22" are +# children of "client 11.0.0.0 255.0.0.0", but there is no indentation in the +# "show running-config" output. The sub-section is indicated by the expression +# mark. +MISSING_INDENT_COMMANDS = [ + r'^ *client ', +] -class DictDiff(object): - '''DictDiff - Abstraction of diff between two dictionaries. If all keys and their values - are same, two dictionaries are considered to be same. This class is intended - to be used by class RunningConfigDiff. +class ListDiff(object): + '''ListDiff + + Abstraction of diff between two lists. Each item in the list is a tuple. + If all tuples are same, two lists are considered to be same. This class is + intended to be used by class RunningConfigDiff. Attributes ---------- - diff : `tuple` - A tuple with two elements. First one is a dictionary that contains - content in dict1 but not in dict2. Second one is a dictionary that - contains content in dict2 but not in dict1. + diff : `list` + A list whose items are tuples. The first item in the tuple is + the config line and the second item is its next level config. If a + config line does not have next level config, the second item value is + None. Otherwise, the value is a list of tuples. The third item in the + tuple has three possible values: '+' means the config is added; '-' + means it is deleted; '' means it remiains unchnaged but for some reason + it is needed (e.g., as a position reference). ''' - def __init__(self, dict1, dict2): - self.dict1 = dict1 - self.dict2 = dict2 + def __init__(self, list1, list2): + self.list1 = list1 + self.list2 = list2 @property def diff(self): - diff1 = deepcopy(self.dict1) - diff2 = deepcopy(self.dict2) - self.simplify_single_dict(diff1, diff2) - return (diff1, diff2) + return self.compare(self.list1, self.list2) @staticmethod - def simplify_single_dict(dict1, dict2): - common_keys = set(dict1.keys()) & set(dict2.keys()) - for key in common_keys: - if dict1[key] == dict2[key]: - del dict1[key] - del dict2[key] + def compare(list1, list2): + diff_1 = [] + key_list_1 = [i[0] for i in list1] + key_list_2 = [i[0] for i in list2] + common_key_list = [] + previous_index_1 = previous_index_2 = 0 + for k2 in key_list_2: + if k2 in key_list_1[previous_index_1:]: + common_key_list.append(k2) + previous_index_1 += key_list_1[previous_index_1:].index(k2) + 1 + previous_index_2 += key_list_2[previous_index_2:].index(k2) + 1 + previous_index_1 = previous_index_2 = 0 + for key in common_key_list: + current_index_1 = previous_index_1 + \ + key_list_1[previous_index_1:].index(key) + current_index_2 = previous_index_2 + \ + key_list_2[previous_index_2:].index(key) + + # Stuff in list1 before a common key but not in list2 + for k, v, i in list1[previous_index_1:current_index_1]: + diff_1.append((k, v, '-')) + + # Stuff in list2 before a common key but not in list1 + for k, v, i in list2[previous_index_2:current_index_2]: + diff_1.append((k, v, '+')) + + # Stuff of the common key itself + if list1[current_index_1][1] == list2[current_index_2][1]: + if list1[current_index_1][1] is None: + diff_1.append((key, None, '?')) + else: + diff_1.append((key, [ + (k, v, '') + for k, v, i in list1[current_index_1][1]], '?')) else: - child1_is_dict = isinstance(dict1[key], dict) - child2_is_dict = isinstance(dict2[key], dict) - if child1_is_dict and child2_is_dict: - DictDiff.simplify_single_dict(dict1[key], dict2[key]) - if not dict1[key]: - del dict1[key] - if not dict2[key]: - del dict2[key] - elif child1_is_dict and not child2_is_dict: - del dict2[key] - elif child2_is_dict and not child1_is_dict: - del dict1[key] + if ( + list1[current_index_1][1] is not None and + list2[current_index_2][1] is not None + ): + diff_1.append((key, ListDiff.compare( + list1[current_index_1][1], + list2[current_index_2][1], + ), '')) + elif list1[current_index_1][1] is not None: + diff_1.append((key, [ + (k, v, '-') + for k, v, i in list1[current_index_1][1]], '')) + elif list2[current_index_2][1] is not None: + diff_1.append((key, [ + (k, v, '+') + for k, v, i in list2[current_index_2][1]], '')) + + previous_index_1 = current_index_1 + 1 + previous_index_2 = current_index_2 + 1 + + # Stuff after all common keys + for k, v, i in list1[previous_index_1:]: + diff_1.append((k, v, '-')) + for k, v, i in list2[previous_index_2:]: + diff_1.append((k, v, '+')) + + # Cleanup + diff_2 = [] + keys_before = set() + for index, item in enumerate(diff_1): + key, value, info = item + if info == '?': + keys_after = set([k for k, v, i in diff_1[index+1:]]) + if keys_before & keys_after: + diff_2.append((key, value, '')) + else: + diff_2.append((key, value, info)) + keys_before.add(key) + + return diff_2 class RunningConfigDiff(object): @@ -70,10 +194,19 @@ class RunningConfigDiff(object): running2 : `str` Second Cisco running-config. - diff : `tuple` - A tuple with two elements. First one is the running-config in running1 - but not in running2. Second one is the running-config in running2 but - not in running1. + diff : `list` + A list from class ListDiff attribute diff, representing changes from + running1 to running2. + + diff_reverse : `list` + A list from class ListDiff attribute diff, representing changes from + running2 to running1. + + cli : `str` + CLIs that transits from running1 to running2. + + cli_reverse : `str` + CLIs that transits from running2 to running1. ''' def __init__(self, running1, running2): @@ -83,18 +216,14 @@ def __init__(self, running1, running2): self.running1 = running1 self.running2 = running2 + self._diff_list = None + self._diff_list_reverse = None def __bool__(self): - diff1, diff2 = self.diff - if diff1 or diff2: - return True - else: - return False + return bool(self.diff) def __str__(self): - diff1, diff2 = self.diff - return '\n'.join(['- ' + l for l in diff1.splitlines()] + - ['+ ' + l for l in diff2.splitlines()]) + return self.list2config(self.diff, diff_type='') def __eq__(self, other): if str(self) == str(other): @@ -104,29 +233,55 @@ def __eq__(self, other): @property def diff(self): - dict1 = self.running2dict(self.running1) - dict2 = self.running2dict(self.running2) - diff_dict = DictDiff(dict1, dict2) - diff1, diff2 = diff_dict.diff - return (self.dict2running(diff1), self.dict2running(diff2)) + return self.get_diff(reverse=False) - def running2dict(self, str_in): - str_in = str_in.replace('exit-address-family', ' exit-address-family') - dict_ret = OrderedDict() - dict_ret['running-config'] = self.config2dict(str_in) - return dict_ret + @property + def diff_reverse(self): + return self.get_diff(reverse=True) + + @property + def cli(self): + return self.get_cli(reverse=False) + + @property + def cli_reverse(self): + return self.get_cli(reverse=True) + + def get_diff(self, reverse=False): + if self._diff_list is None: + list1 = self.running2list(self.running1) + list2 = self.running2list(self.running2) + self.handle_sibling_cammands(list1) + self.handle_sibling_cammands(list2) + self._diff_list = ListDiff(list1, list2).diff + self._diff_list_reverse = ListDiff(list2, list1).diff + diff_list = self._diff_list_reverse if reverse else self._diff_list + return diff_list if diff_list else None - def dict2running(self, dict_in): - if dict_in: - return self.dict2config(dict_in['running-config']) + def get_cli(self, reverse=False): + diff_list = self.get_diff(reverse=reverse) + if diff_list: + positive_str, negative_str = self.list2cli(diff_list) + if positive_str: + if negative_str: + return negative_str + '\n!\n' + positive_str + else: + return positive_str + else: + return negative_str else: return '' - def config2dict(self, str_in): - dict_ret = OrderedDict() + def running2list(self, str_in): + str_in = str_in.replace('exit-address-family', ' exit-address-family') + return self.config2list(self.handle_orderless(str_in)) + + def config2list(self, str_in): + list_ret = [] last_line = '' last_section = '' last_indentation = 0 + missing_indent = False for line in str_in.splitlines(): if len(line.strip()) > 22 and \ line[:22] == 'Building configuration': @@ -136,37 +291,263 @@ def config2dict(self, str_in): continue if len(line.strip()) == 0: continue - if re.search('^ *!', line): + if missing_indent and line.rstrip() == '!': + if last_line: + if last_indentation > 0: + list_ret.append(( + last_line, self.config2list(last_section), '')) + else: + list_ret.append((last_line, None, '')) + last_line = '' + last_section = '' + last_indentation = 0 + missing_indent = False + continue + if re.search('^ *!', line) or re.search('^ *%', line): continue if line[0] == ' ': + current_indentation = len(re.search('^ *', line).group(0)) if last_indentation == 0: - last_indentation = len(re.search('^ *', line).group(0)) - last_section += line[last_indentation:] + '\n' + last_indentation = current_indentation + + # There might be special cases. For example, the following + # running-config: + # ip dhcp class CLASS1 + # relay agent information + # relay-information hex 01040101030402020102 + # should be considered as: + # ip dhcp class CLASS1 + # relay agent information + # relay-information hex 01040101030402020102 + if current_indentation < last_indentation: + last_section += ' ' * (last_indentation + 1) + \ + line[current_indentation:] + '\n' + else: + last_section += line[last_indentation:] + '\n' else: + if any( + [re.search(regx, line) for regx in MISSING_INDENT_COMMANDS] + ): + missing_indent = True + elif missing_indent: + current_indentation = 1 + if last_indentation == 0: + last_indentation = current_indentation + last_section += line + '\n' + continue if last_line: if last_indentation > 0: - dict_ret[last_line] = self.config2dict(last_section) + list_ret.append(( + last_line, self.config2list(last_section), '')) else: - dict_ret[last_line] = '' + list_ret.append((last_line, None, '')) last_line = line last_section = '' last_indentation = 0 - if last_indentation > 0: - dict_ret[last_line] = self.config2dict(last_section) - else: - dict_ret[last_line] = '' - return dict_ret + if last_line: + if last_indentation > 0: + list_ret.append(( + last_line, self.config2list(last_section), '')) + else: + list_ret.append((last_line, None, '')) + return list_ret - def dict2config(self, dict_in): + def list2config(self, list_in, diff_type=None): str_ret = '' - for k, v in dict_in.items(): - str_ret += k + '\n' - if type(v) is OrderedDict: - str_ret += self.indent(self.dict2config(v)) + if list_in is None: + return str_ret + for k, v, i in list_in: + if k == '': + continue + if diff_type == '': + local_diff_type = i + else: + local_diff_type = diff_type + if diff_type is None: + str_ret += ' ' + k + '\n' + else: + prefix = local_diff_type if local_diff_type != '' else ' ' + str_ret += prefix + ' ' + k + '\n' + if v is not None: + str_ret += self.indent( + self.list2config(v, diff_type=local_diff_type), + ) return str_ret + def list2cli(self, list_in): + if list_in is None: + return '' + positive_list = [] + negative_list = [] + positive_keys = [k for k, v, i in list_in if i == '+' and v is None] + for k, v, i in list_in: + if k == '': + continue + if v is None: + if i == '+': + self.append_command(k, negative_list, positive_list) + elif i == '-': + # In a case that a CLI is updated, no command is not + # needed: + # - service timestamps debug datetime msec + # + service timestamps debug datetime msec localtime + # show-timezone + key_len = len(k) + matching_positive_keys = [ + key for key in positive_keys if key[:key_len] == k] + if not matching_positive_keys: + self.append_command('no ' + k, + negative_list, positive_list) + else: + if i == '': + positive_str, negative_str = self.list2cli(v) + if positive_str: + positive_list.append( + k + '\n' + self.indent(positive_str).rstrip()) + if negative_str: + negative_list.append( + k + '\n' + self.indent(negative_str).rstrip()) + if i == '+': + positive_str = self.list2config(v, diff_type=None).rstrip() + if positive_str: + positive_list.append(k + '\n' + positive_str) + else: + positive_list.append(k) + elif i == '-': + self.append_command('no ' + k, + negative_list, positive_list) + + # Handle overwritable commands + idx_positive_dict = {} + idx_positive_list = [] + for regx in OVERWRITABLE_COMMANDS: + for idx_positive, cmd in enumerate(positive_list): + m = re.search(regx, cmd) + if m: + + # Remove matching negative CLIs + exact_cmd = 'no ' + m.group(0).lstrip() + len_exact_cmd = len(exact_cmd) + for idx_negative, line in enumerate(negative_list): + cmd_line = line.lstrip() + if ( + len(cmd_line) >= len_exact_cmd and + cmd_line[:len_exact_cmd] == exact_cmd + ): + break + else: + idx_negative = None + if idx_negative is not None: + del negative_list[idx_negative] + + # Overwrite previous matching positive CLIs + exact_cmd = m.group(0).strip() + if exact_cmd in idx_positive_dict: + idx_positive_list.append(idx_positive_dict[exact_cmd]) + idx_positive_dict[exact_cmd] = idx_positive + + if idx_positive_list: + idx_positive_list.sort() + for idx in reversed(idx_positive_list): + del positive_list[idx] + + # Handle duplicate commands + # Some commands their positive and negative lines are both appeared in + # the "show running-config" output, e.g., "platform punt-keepalive + # disable-kernel-core" and "no platform punt-keepalive + # disable-kernel-core" are both visible. This treatment is required to + # avoid two lines of the same CLI. + for cmd_list in [positive_list, negative_list]: + self.remove_duplicate_cammands(cmd_list) + + return '\n'.join(positive_list), '\n'.join(reversed(negative_list)) + + @staticmethod + def append_command(cmd, negative_list, positive_list): + cmd, is_no_cmd = RunningConfigDiff.handle_command(cmd) + if is_no_cmd: + negative_list.append(cmd) + else: + positive_list.append(cmd) + + @staticmethod + def handle_command(cmd): + if cmd[:6] == 'no no ': + return cmd[6:].strip(), False + for short_cmd in SHORT_NO_COMMANDS: + if cmd[:len(short_cmd)] == short_cmd: + return short_cmd.strip(), True + if cmd[:3] == 'no ': + return cmd.strip(), True + else: + return cmd.strip(), False + + @staticmethod + def handle_orderless(running): + lines = running.splitlines() + for regx in ORDERLESS_COMMANDS: + indexes = [] + for idx, line in enumerate(lines): + if re.search(regx, line): + indexes.append(idx) + if len(indexes) < 2: + continue + matches_dict = {hash(lines[i]): lines[i] for i in indexes} + matches_list = [ + matches_dict[i] for i in sorted(list(matches_dict.keys()))] + for i in reversed(indexes): + del lines[i] + lines = lines[:indexes[0]] + matches_list + lines[indexes[0]:] + return '\n'.join(lines) + + @staticmethod + def remove_duplicate_cammands(config_list): + indexes = [] + commands = set() + for idx, line in enumerate(config_list): + if line in commands: + indexes.append(idx) + else: + commands.add(line) + if indexes: + for idx in reversed(indexes): + del config_list[idx] + + @staticmethod + def handle_sibling_cammands(config_list): + length = len(config_list) + lines_inserted = 0 + for i in range(length): + idx = i + lines_inserted + tup = config_list[idx] + for regx in SIBLING_CAMMANDS: + if re.search(regx, tup[0]) and tup[1] is not None: + siblings = [] + indexes = [] + for idx_c, tup_c in enumerate(tup[1]): + if re.search(regx, tup_c[0]): + indexes.append(idx_c) + for idx_c in reversed(indexes): + siblings.append(tup[1][idx_c]) + del tup[1][idx_c] + if not tup[1]: + tup = config_list[idx] = (tup[0], None, tup[2]) + j = 1 + for sibling in reversed(siblings): + config_list.insert(i+j, sibling) + j += 1 + break + if tup[1] is not None: + RunningConfigDiff.handle_sibling_cammands(tup[1]) + def indent(self, str_in): str_ret = '' for line in str_in.splitlines(): - str_ret += ' ' + line + '\n' - return str_ret + if line: + if line[0] in '-+': + diff_type = line[0] + line = line[1:] + str_ret += diff_type + ' ' + line + '\n' + else: + str_ret += ' ' + line + '\n' + return str_ret \ No newline at end of file diff --git a/ncdiff/src/yang/ncdiff/tests/test_running_config.py b/ncdiff/src/yang/ncdiff/tests/test_running_config.py new file mode 100644 index 0000000..e993206 --- /dev/null +++ b/ncdiff/src/yang/ncdiff/tests/test_running_config.py @@ -0,0 +1,597 @@ +#!/bin/env python +""" Unit tests for the ncdiff cisco-shared package. """ + +import unittest +from ncdiff import RunningConfigDiff + + +class TestRunningConfig(unittest.TestCase): + + def setUp(self): + pass + + def test_diff_1(self): + config_1 = """ +ip dhcp pool ABC + network 10.0.20.0 255.255.255.0 + class CLASS1 + address range 10.0.20.1 10.0.20.100 + class CLASS2 + address range 10.0.20.101 10.0.20.200 + class CLASS3 + address range 10.0.20.201 10.0.20.254 +! +ip dhcp pool dhcp_1 + network 172.16.1.0 255.255.255.0 + network 172.16.2.0 255.255.255.0 secondary + network 172.16.3.0 255.255.255.0 secondary + network 172.16.4.0 255.255.255.0 secondary +! +! +ip dhcp class CLASS1 + relay agent information +! +ip dhcp class CLASS2 + relay agent information +! +ip dhcp class CLASS3 + relay agent information +! +! +login on-success log + """ + config_2 = """ +ip dhcp pool dhcp_1 + network 172.16.1.0 255.255.255.0 + network 172.16.2.0 255.255.255.0 secondary + network 172.16.3.0 255.255.255.0 secondary + network 172.16.4.0 255.255.255.0 secondary +! +ip dhcp pool ABC + network 10.0.20.0 255.255.255.0 + class CLASS1 + address range 10.0.20.1 10.0.20.100 + class CLASS2 + address range 10.0.20.101 10.0.20.200 + class CLASS3 + address range 10.0.20.201 10.0.20.254 +! +! +ip dhcp class CLASS2 + relay agent information + relay-information hex 01040101030402020102 + relay-information hex 01040102030402020102 +! +ip dhcp class CLASS1 + relay agent information + relay-information hex 01030a0b0c02* + relay-information hex 01030a0b0c02050000000123 +! +ip dhcp class CLASS3 + relay agent information +! +! +login on-success log +! + """ + expected_diff = """ +- ip dhcp pool ABC +- network 10.0.20.0 255.255.255.0 +- class CLASS1 +- address range 10.0.20.1 10.0.20.100 +- class CLASS2 +- address range 10.0.20.101 10.0.20.200 +- class CLASS3 +- address range 10.0.20.201 10.0.20.254 + ip dhcp pool dhcp_1 + network 172.16.1.0 255.255.255.0 + network 172.16.2.0 255.255.255.0 secondary + network 172.16.3.0 255.255.255.0 secondary + network 172.16.4.0 255.255.255.0 secondary +- ip dhcp class CLASS1 +- relay agent information ++ ip dhcp pool ABC ++ network 10.0.20.0 255.255.255.0 ++ class CLASS1 ++ address range 10.0.20.1 10.0.20.100 ++ class CLASS2 ++ address range 10.0.20.101 10.0.20.200 ++ class CLASS3 ++ address range 10.0.20.201 10.0.20.254 + ip dhcp class CLASS2 + relay agent information ++ relay-information hex 01040101030402020102 ++ relay-information hex 01040102030402020102 ++ ip dhcp class CLASS1 ++ relay agent information ++ relay-information hex 01030a0b0c02* ++ relay-information hex 01030a0b0c02050000000123 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line, expected_line) + + def test_diff_2(self): + config_1 = """ +aa bb cc + """ + config_2 = """ +aa bb cc + ee ff rr +! + """ + expected_diff = """ + aa bb cc ++ ee ff rr + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line, expected_line) + + def test_diff_3(self): + config_1 = """ +aa bb cc + ee ff rr +! + """ + config_2 = """ +aa bb cc + """ + expected_diff = """ + aa bb cc +- ee ff rr + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line, expected_line) + + def test_diff_4(self): + config_1 = """ +vrf definition genericstring + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! + """ + config_2 = """ +vrf definition genericstring + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! + """ + expected_diff = "" + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line, expected_line) + + def test_diff_5(self): + config_1 = """ +vrf definition genericstring + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! + """ + config_2 = """ +vrf definition generic + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! + """ + expected_diff = """ +- vrf definition genericstring +- address-family ipv4 +- exit-address-family +- address-family ipv6 +- exit-address-family ++ vrf definition generic ++ address-family ipv4 ++ exit-address-family ++ address-family ipv6 ++ exit-address-family + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line, expected_line) + + def test_cli_short_no_commands(self): + config_1 = """ +vrf definition genericstring + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! +license boot level network-advantage addon dna-advantage + """ + config_2 = """ +vrf definition generic + ! + address-family ipv4 + exit-address-family + ! + address-family ipv6 + exit-address-family +! + """ + expected_cli = """ +no license boot level +no vrf definition genericstring +! +vrf definition generic + address-family ipv4 + exit-address-family + address-family ipv6 + exit-address-family + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_orderless_commands(self): + config_1 = """ +license boot level network-advantage addon dna-advantage +! +aaa authentication login admin-con group tacacs+ local +aaa authentication login admin-vty group tacacs+ local + """ + config_2 = """ +aaa authentication login admin-vty group tacacs+ local +aaa authentication login admin-con group tacacs+ local + """ + expected_cli = """ +no license boot level + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_overwritable_commands(self): + config_1 = """ +line vty 0 4 + exec-timeout 0 0 + password 0 lab + transport input all +! + """ + config_2 = """ +line vty 0 4 + exec-timeout 0 0 + password 7 082D4D4C + transport input all +! + """ + expected_cli = """ +line vty 0 4 + password 7 082D4D4C + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_doubleface_commands(self): + config_1 = """ +no platform punt-keepalive disable-kernel-core +! + """ + config_2 = """ +platform punt-keepalive disable-kernel-core + """ + expected_cli = """ +platform punt-keepalive disable-kernel-core + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_sibling_commands(self): + config_1 = """ +aaa server radius proxy + client 10.0.0.0 255.0.0.0 + timer disconnect acct-stop 23 + ! + client 11.0.0.0 255.0.0.0 + accounting port 34 + ! + client 12.0.0.0 255.0.0.0 + accounting port 56 + ! +abc def + aaa server radius proxy + client 10.0.0.0 255.0.0.0 + ert tyt + client 11.0.0.0 255.0.0.0 + kdjfg kjkfdg + client 12.0.0.0 255.0.0.0 + gdfh lijdf + """ + config_2 = """ +aaa server radius proxy + client 11.0.0.0 255.0.0.0 + accounting port 34 + ! + client 13.0.0.0 255.0.0.0 + accounting port 77 + ! +abc def + aaa server radius proxy + client 11.0.0.0 255.0.0.0 + kdjfg ttttt + client 13.0.0.0 255.0.0.0 + gdfh lijdf + """ + expected_cli = """ +abc def + aaa server radius proxy + no client 12.0.0.0 255.0.0.0 + client 11.0.0.0 255.0.0.0 + no kdjfg kjkfdg + no client 10.0.0.0 255.0.0.0 +aaa server radius proxy + no client 12.0.0.0 255.0.0.0 + no client 10.0.0.0 255.0.0.0 +! +aaa server radius proxy + client 13.0.0.0 255.0.0.0 + accounting port 77 +abc def + aaa server radius proxy + client 11.0.0.0 255.0.0.0 + kdjfg ttttt + client 13.0.0.0 255.0.0.0 + gdfh lijdf + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_ip_address(self): + config_1 = """ +interface GigabitEthernet8/0/1 +! +interface Vlan69 + ip address 192.168.100.1 255.255.255.0 +! +interface Vlan70 + ip address 192.168.101.1 255.255.255.0 + """ + config_2 = """ +ip dhcp pool Test + network 1.1.0.0 255.255.0.0 +! +interface GigabitEthernet8/0/1 + no switchport + ip address dhcp + shutdown +! +interface Vlan69 + no ip address + ipv6 address 2001:FACE::1/64 + ipv6 enable + ipv6 dhcp relay destination 2001:1234::2 +! +interface Vlan70 + no ip address + ipv6 address 2001:1234::1/64 + ipv6 enable + """ + expected_cli = """ +interface Vlan70 + no ip address +interface Vlan69 + no ip address +interface GigabitEthernet8/0/1 + no switchport +! +ip dhcp pool Test + network 1.1.0.0 255.255.0.0 +interface GigabitEthernet8/0/1 + ip address dhcp + shutdown +interface Vlan69 + ipv6 address 2001:FACE::1/64 + ipv6 enable + ipv6 dhcp relay destination 2001:1234::2 +interface Vlan70 + ipv6 address 2001:1234::1/64 + ipv6 enable + """ + expected_reverse_cli = """ +interface Vlan70 + no ipv6 enable + no ipv6 address +interface Vlan69 + no ipv6 dhcp relay destination 2001:1234::2 + no ipv6 enable + no ipv6 address +interface GigabitEthernet8/0/1 + no shutdown + no ip address +no ip dhcp pool Test +! +interface GigabitEthernet8/0/1 + switchport +interface Vlan69 + ip address 192.168.100.1 255.255.255.0 +interface Vlan70 + ip address 192.168.101.1 255.255.255.0 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_cli = running_diff.cli.strip() + expected_cli = expected_cli.strip() + actual_lines = actual_cli.split('\n') + expected_lines = expected_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + actual_reverse_cli = running_diff.cli_reverse.strip() + expected_reverse_cli = expected_reverse_cli.strip() + actual_lines = actual_reverse_cli.split('\n') + expected_lines = expected_reverse_cli.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_logging_host(self): + config_1 = """ +logging host 10.15.118.120 +logging host 10.200.159.168 +logging host 10.200.209.215 + """ + config_2 = """ +logging host 10.200.209.215 +logging host 10.15.118.120 +logging host 10.200.159.168 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_cli_flow_mon(self): + config_1 = """ +flow monitor eta-mon +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 + """ + config_2 = """ +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +flow monitor eta-mon + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') \ No newline at end of file From e0647205ea582b5f9145ebda3d11930cd5f2ea89 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 17:37:35 +0000 Subject: [PATCH 09/20] PR#24 Added more orderless clis and tests for them --- ncdiff/src/yang/ncdiff/runningconfig.py | 142 ++++-- .../yang/ncdiff/tests/test_running_config.py | 451 ++++++++++++++++++ 2 files changed, 563 insertions(+), 30 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/runningconfig.py b/ncdiff/src/yang/ncdiff/runningconfig.py index a8aa3d7..5e09c09 100755 --- a/ncdiff/src/yang/ncdiff/runningconfig.py +++ b/ncdiff/src/yang/ncdiff/runningconfig.py @@ -19,13 +19,37 @@ # Some commands are orderless, e.g., "show running-config" output could be: # aaa authentication login admin-con group tacacs+ local # aaa authentication login admin-vty group tacacs+ local +# # Or in other times it is displayed in a different order: # aaa authentication login admin-vty group tacacs+ local # aaa authentication login admin-con group tacacs+ local +# +# Since "aaa authentication login" is orderless at the the global config +# level, regexp and depth are defined as "^ *aaa authentication login " and 0. +# +# In another example, "show running-config" output could be: +# flow monitor meraki_monitor +# exporter meraki_exporter +# exporter customer_exporter +# record meraki_record +# +# Or in other times it is displayed in a different order: +# flow monitor meraki_monitor +# exporter customer_exporter +# exporter meraki_exporter +# record meraki_record +# +# Here the config "exporter" at the second level is orderless. so regexp and +# depth are defined as "^ *exporter " and 1. ORDERLESS_COMMANDS = [ - r'^ *aaa authentication login ', - r'^ *logging host ', - r'^ *flow monitor ', + (re.compile(r'^ *aaa authentication login '), 0), + (re.compile(r'^ *logging host '), 0), + (re.compile(r'^ *flow monitor '), 0), + (re.compile(r'^ *service-template '), 0), + (re.compile(r'^ *aaa group server radius '), 0), + (re.compile(r'^ *flow exporter '), 0), + (re.compile(r'^ *exporter '), 1), + (re.compile(r'^ *username '), 0), ] # Some commands can be overwritten without a no command. For example, changing @@ -35,11 +59,11 @@ # username admin privilege 15 password 7 15130F010D24 # There is no need to send a no command before sending the second line. OVERWRITABLE_COMMANDS = [ - r'^ *username \S+ privilege [0-9]+ password ', - r'^ *password ', - r'^ *description ', - r'^ *ip address( |$)', - r'^ *ipv6 address( |$)', + re.compile(r'^ *username \S+ privilege [0-9]+ password '), + re.compile(r'^ *password '), + re.compile(r'^ *description '), + re.compile(r'^ *ip address( |$)'), + re.compile(r'^ *ipv6 address( |$)'), ] # Some commands look like a parent-child relation but actually they are @@ -50,7 +74,7 @@ # client 11.0.0.0 255.0.0.0 # ! SIBLING_CAMMANDS = [ - r'^ *client ', + re.compile(r'^ *client '), ] # As for the client command above, its children does not have indentation: @@ -249,8 +273,7 @@ def cli_reverse(self): def get_diff(self, reverse=False): if self._diff_list is None: - list1 = self.running2list(self.running1) - list2 = self.running2list(self.running2) + list1, list2 = self.running2list(self.running1, self.running2) self.handle_sibling_cammands(list1) self.handle_sibling_cammands(list2) self._diff_list = ListDiff(list1, list2).diff @@ -272,9 +295,14 @@ def get_cli(self, reverse=False): else: return '' - def running2list(self, str_in): - str_in = str_in.replace('exit-address-family', ' exit-address-family') - return self.config2list(self.handle_orderless(str_in)) + def running2list(self, str_in_1, str_in_2): + str_in_1 = str_in_1.replace('exit-address-family', + ' exit-address-family') + str_in_2 = str_in_2.replace('exit-address-family', + ' exit-address-family') + list_1 = self.config2list(str_in_1) + list_2 = self.config2list(str_in_2) + return self.handle_orderless(list_1, list_2, 0) def config2list(self, str_in): list_ret = [] @@ -483,22 +511,76 @@ def handle_command(cmd): return cmd.strip(), False @staticmethod - def handle_orderless(running): - lines = running.splitlines() - for regx in ORDERLESS_COMMANDS: - indexes = [] - for idx, line in enumerate(lines): - if re.search(regx, line): - indexes.append(idx) - if len(indexes) < 2: - continue - matches_dict = {hash(lines[i]): lines[i] for i in indexes} - matches_list = [ - matches_dict[i] for i in sorted(list(matches_dict.keys()))] - for i in reversed(indexes): - del lines[i] - lines = lines[:indexes[0]] + matches_list + lines[indexes[0]:] - return '\n'.join(lines) + def handle_orderless(list1, list2, depth): + # Find common lines that are orderless + lines1 = [i[0] for i in list1] + matches = {} + for idx, item in enumerate(list2): + result, match_type = RunningConfigDiff.match_orderless(item[0], + depth) + if result and item[0] in lines1: + matches[item[0]] = match_type, idx + + # Romove common lines from list1 and save them in removed_items + type_start_idx = {} + to_be_removed = [] + for idx, line in enumerate(lines1): + if line in matches: + if matches[line][0] not in type_start_idx: + type_start_idx[matches[line][0]] = idx + to_be_removed.append(idx) + removed_items = {} + for idx in reversed(to_be_removed): + removed_items[list1[idx][0]] = list1[idx] + del list1[idx] + + # Find common lines + lines1 = [i[0] for i in list1] + lines2 = [i[0] for i in list2] + common_lines = [] + previous_idx_1 = previous_idx_2 = 0 + for line2 in lines2: + if line2 in lines1[previous_idx_1:]: + previous_idx_1 += lines1[previous_idx_1:].index(line2) + 1 + previous_idx_2 += lines2[previous_idx_2:].index(line2) + 1 + common_lines.append((previous_idx_1 - 1, previous_idx_2 - 1)) + + # Insert common lines back to list1 + offset_idx_1 = 0 + previous_idx_2 = 0 + for line, (line_type, list2_idx) in matches.items(): + common_idx_1 = 0 + if len(common_lines) > 0 and list2_idx > common_lines[0][1]: + for i, j in common_lines: + # if previous_idx_2 <= j and j < list2_idx: + if j < list2_idx: + common_idx_1 = i + 1 + previous_idx_2 = j + if j > list2_idx: + break + start_idx = max(common_idx_1 + offset_idx_1, + type_start_idx[line_type]) + list1.insert(start_idx, removed_items[line]) + offset_idx_1 = start_idx - common_idx_1 + 1 + + # Find common lines that have children + lines1 = {item[0]: idx for idx, item in enumerate(list1) + if isinstance(item[1], list) and len(item[1]) > 0} + lines2 = {item[0]: idx for idx, item in enumerate(list2) + if isinstance(item[1], list) and len(item[1]) > 0} + for line in set(lines1.keys()) & set(lines2.keys()): + RunningConfigDiff.handle_orderless(list1[lines1[line]][1], + list2[lines2[line]][1], + depth + 1) + + return list1, list2 + + @staticmethod + def match_orderless(line, current_depth): + for idx, (regx, depth) in enumerate(ORDERLESS_COMMANDS): + if depth == current_depth and re.search(regx, line): + return True, idx + return False, None @staticmethod def remove_duplicate_cammands(config_list): diff --git a/ncdiff/src/yang/ncdiff/tests/test_running_config.py b/ncdiff/src/yang/ncdiff/tests/test_running_config.py index e993206..b286a4e 100644 --- a/ncdiff/src/yang/ncdiff/tests/test_running_config.py +++ b/ncdiff/src/yang/ncdiff/tests/test_running_config.py @@ -585,6 +585,457 @@ def test_cli_flow_mon(self): exporter customer_exporter record meraki_record_ipv6 flow monitor eta-mon + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_cli_service_template_1(self): + config_1 = """ +login on-success log +! +flow monitor eta-mon +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +! +access-session mac-move deny +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template webauth-global-inactive + inactivity-timer 3600 +service-template eap-seen +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag + """ + config_2 = """ +login on-success log +! +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +flow monitor eta-mon +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan + aaa bbb ccc +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template eap-seen +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag +service-template webauth-global-inactive + inactivity-timer 3600 + """ + expected_diff = """ +- access-session mac-move deny + service-template DEFAULT_CRITICAL_VOICE_TEMPLATE ++ aaa bbb ccc + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_service_template_2(self): + config_1 = """ +login on-success log +! +flow monitor eta-mon +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template webauth-global-inactive + inactivity-timer 3600 +service-template eap-seen +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag + """ + config_2 = """ +login on-success log +! +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +flow monitor eta-mon +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template eap-seen +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag +service-template webauth-global-inactive + inactivity-timer 3600 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_cli_service_template_3(self): + config_1 = """ +login on-success log +! +flow monitor eta-mon +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +! +access-session mac-move deny +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template webauth-global-inactive + inactivity-timer 3600 +service-template eap-seen +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag + """ + config_2 = """ +login on-success log +! +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + record meraki_record +flow monitor meraki_monitor_ipv6 + exporter meraki_exporter + exporter customer_exporter + record meraki_record_ipv6 +flow monitor eta-mon +! +crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 +! +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE_NEW + voice vlan +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template eap-seen +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag +service-template webauth-global-inactive + inactivity-timer 3600 + """ + expected_diff = """ +- access-session mac-move deny ++ service-template DEFAULT_CRITICAL_VOICE_TEMPLATE_NEW ++ voice vlan +- service-template DEFAULT_CRITICAL_VOICE_TEMPLATE +- voice vlan + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertTrue(running_diff) + actual_diff = str(running_diff).strip() + expected_diff = expected_diff.strip() + actual_lines = actual_diff.split('\n') + expected_lines = expected_diff.split('\n') + self.assertEqual(len(actual_lines), len(expected_lines)) + for actual_line, expected_line in zip(actual_lines, expected_lines): + self.assertEqual(actual_line.strip(), expected_line.strip()) + + def test_cli_aaa_group_server_radius(self): + config_1 = """ +aaa group server radius testing-10.5.0.110-1812 + server name 10.5.0.110_1812_0 +aaa group server radius testing-6.0.0.2-1812 + server name 6.0.0.2_1812_0 +aaa group server radius radius-group-0 + server name 6.0.0.2_1812_0 +aaa group server radius radius-group-1 + server name 10.5.0.110_1812_0 +aaa group server radius radius-group-2 + server name 10.5.0.110_1812_0 +aaa group server radius radius-group-3 + server name 10.5.0.110_1812_0 +aaa group server radius testing-10.5.0.110-0 + server name 10.5.0.110_0_1813 +aaa group server radius radius-group-4 + server name 10.5.0.110_1812_0 + """ + config_2 = """ +aaa group server radius radius-group-0 + server name 6.0.0.2_1812_0 +aaa group server radius radius-group-1 + server name 10.5.0.110_1812_0 +aaa group server radius radius-group-2 + server name 10.5.0.110_1812_0 +aaa group server radius radius-group-3 + server name 10.5.0.110_1812_0 +aaa group server radius radius-group-4 + server name 10.5.0.110_1812_0 +aaa group server radius testing-10.5.0.110-0 + server name 10.5.0.110_0_1813 +aaa group server radius testing-10.5.0.110-1812 + server name 10.5.0.110_1812_0 +aaa group server radius testing-6.0.0.2-1812 + server name 6.0.0.2_1812_0 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_service_template_2(self): + config_1 = """ +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template webauth-global-inactive + inactivity-timer 3600 +service-template eap-seen +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag +service-template failed-vlan-2 + vlan 98 +service-template guest-vlan-0 + vlan 900 + """ + config_2 = """ +service-template DEFAULT_CRITICAL_DATA_TEMPLATE +service-template DEFAULT_CRITICAL_VOICE_TEMPLATE + voice vlan +service-template DEFAULT_LINKSEC_POLICY_MUST_SECURE + linksec policy must-secure +service-template DEFAULT_LINKSEC_POLICY_SHOULD_SECURE + linksec policy should-secure +service-template aaa-unreachable-flag +service-template bounce-port-flag +service-template eap-seen +service-template failed-vlan-2 + vlan 98 +service-template guest-vlan-0 + vlan 900 +service-template secure-connect-ap-aaa-down +service-template secure-connect-break-flag +service-template secure-connect-failed-flag +service-template secure-connect-in-progress-flag +service-template secure-connect-success-flag +service-template webauth-global-inactive + inactivity-timer 3600 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_flow_exporter(self): + config_1 = """ +flow exporter meraki_exporter + destination 1.1.3.2 + transport udp 9995 +flow exporter customer_exporter + destination 10.7.253.22 + transport udp 2055 + export-protocol ipfix + option interface-table timeout 3600 + option exporter-stats timeout 300 + option application-table timeout 3600 + """ + config_2 = """ +flow exporter customer_exporter + destination 10.7.253.22 + transport udp 2055 + export-protocol ipfix + option interface-table timeout 3600 + option exporter-stats timeout 300 + option application-table timeout 3600 +flow exporter meraki_exporter + destination 1.1.3.2 + transport udp 9995 + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_exporter(self): + config_1 = """ +flow monitor meraki_monitor + exporter meraki_exporter + exporter customer_exporter + """ + config_2 = """ +flow monitor meraki_monitor + exporter customer_exporter + exporter meraki_exporter + """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_username(self): + config_1 = """ +username guestshell privilege 0 password 7 03034E0E151B32444B0515 +username priv01 password 7 1209171E045B5D + """ + config_2 = """ +username priv01 password 7 1209171E045B5D +username guestshell privilege 0 password 7 03034E0E151B32444B0515 """ running_diff = RunningConfigDiff( running1=config_1, From 70958735e28e6bce7d58c22c28031ab9e0e1a0a6 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 19:25:29 +0000 Subject: [PATCH 10/20] PR#25 Add two more tests to test replace_depth --- ncdiff/src/yang/ncdiff/config.py | 96 +- ncdiff/src/yang/ncdiff/netconf.py | 573 ++++++--- ncdiff/src/yang/ncdiff/tests/test_ncdiff.py | 1267 ++++++++++++++++++- 3 files changed, 1701 insertions(+), 235 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index 4f02f84..516b82a 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -455,26 +455,17 @@ class ConfigDelta(object): a transition. config_dst : `Config` - An instance of yang.ncdiff.Config, which is the destination config state - of a transition. + An instance of yang.ncdiff.Config, which is the destination config + state of a transition. nc : `Element` A lxml Element which contains the delta. This attribute can be used by ncclient edit_config() directly. It is the Netconf presentation of a ConfigDelta instance. - rc : `list` - A list of requests.models.Request instances. Each Request instance can - be used by prepare_request() in requests package. It is the Restconf - presentation of a ConfigDelta instance. - - gnmi : `dict` - A gnmi_pb2.SetRequest instance. It is the gNMI presentation of a - ConfigDelta instance. - ns : `dict` - A dictionary of namespaces used by the attribute 'nc'. Keys are prefixes - and values are URLs. + A dictionary of namespaces used by the attribute 'nc'. Keys are + prefixes and values are URLs. models : `list` A list of model names that self.roots belong to. @@ -494,56 +485,71 @@ class ConfigDelta(object): preferred_delete : `str` Preferred operation of deleting an existing element. Choice of 'delete' or 'remove'. + + diff_type : `str` + Choice of 'minimum' or 'replace'. This value has impact on attribute + nc. In general, there are two options to construct nc. The first + option is to find out minimal changes between config_src and + config_dst. Then attribute nc will reflect what needs to be modified. + The second option is to use 'replace' operation in Netconf. More + customers prefer 'replace' operation as it is more deterministic. + + replace_depth : `int` + Specify the deepest level of replace operation when diff_type is + 'replace'. Replace operation might be needed earlier before we reach + the specified level, depending on situations. Consider roots in a YANG + module are level 0, their children are level 1, and so on so forth. + The default value of replace_depth is 0. ''' def __init__(self, config_src, config_dst=None, delta=None, preferred_create='merge', preferred_replace='merge', - preferred_delete='delete'): + preferred_delete='delete', + diff_type='minimum', replace_depth=0): ''' __init__ instantiates a ConfigDelta instance. ''' + self.diff_type = diff_type + self.replace_depth = replace_depth if not isinstance(config_src, Config): - raise TypeError("argument 'config_src' must be " \ - "yang.ncdiff.Config, but not '{}'" \ + raise TypeError("argument 'config_src' must be " + "yang.ncdiff.Config, but not '{}'" .format(type(config_src))) if preferred_create in ['merge', 'create', 'replace']: self.preferred_create = preferred_create else: - raise ValueError("only 'merge', 'create' or 'replace' are valid " \ + raise ValueError("only 'merge', 'create' or 'replace' are valid " "values of 'preferred_create'") if preferred_replace in ['merge', 'replace']: self.preferred_replace = preferred_replace else: - raise ValueError("only 'merge' or 'replace' are valid " \ + raise ValueError("only 'merge' or 'replace' are valid " "values of 'preferred_replace'") if preferred_delete in ['delete', 'remove']: self.preferred_delete = preferred_delete else: - raise ValueError("only 'delete' or 'remove' are valid " \ + raise ValueError("only 'delete' or 'remove' are valid " "values of 'preferred_delete'") self.config_src = config_src if delta is not None: if isinstance(delta, str) or etree.iselement(delta): delta = NetconfParser(self.device, delta).ele - elif isinstance(delta, Request) or isinstance(delta, SetRequest): - delta = delta else: - raise TypeError("argument 'delta' must be XML string, " \ - "Element, requests.Request, or " \ - "gnmi_pb2.SetRequest, but not " \ - "'{}'".format(type(delta))) + raise TypeError("argument 'delta' must be XML string, " + "Element, but not '{}'" + .format(type(delta))) if not isinstance(config_dst, Config) and config_dst is not None: - raise TypeError("argument 'config_dst' must be " \ - "yang.ncdiff.Config or None, but not '{}'" \ + raise TypeError("argument 'config_dst' must be " + "yang.ncdiff.Config or None, but not '{}'" .format(type(config_dst))) self.config_dst = config_dst if self.config_dst is None and delta is None: - raise ValueError("either 'config_dst' or 'delta' must present") + self.config_dst = self.config_src if delta is not None: if self.config_dst is not None: - logger.warning("argument 'config_dst' is ignored as 'delta' " \ + logger.warning("argument 'config_dst' is ignored as 'delta' " "is provided") self.config_dst = self.config_src + delta else: @@ -555,21 +561,15 @@ def device(self): @property def nc(self): - return NetconfCalculator(self.device, - self.config_dst.ele, self.config_src.ele, - preferred_create=self.preferred_create, - preferred_replace=self.preferred_replace, - preferred_delete=self.preferred_delete).sub - - @property - def rc(self): - return RestconfCalculator(self.device, - self.config_dst.ele, self.config_src.ele).sub - - @property - def gnmi(self): - return gNMICalculator(self.device, - self.config_dst.ele, self.config_src.ele).sub + return NetconfCalculator( + self.device, + self.config_dst.ele, self.config_src.ele, + preferred_create=self.preferred_create, + preferred_replace=self.preferred_replace, + preferred_delete=self.preferred_delete, + diff_type=self.diff_type, + replace_depth=self.replace_depth, + ).sub @property def ns(self): @@ -577,7 +577,7 @@ def ns(self): @property def models(self): - return sorted(list(set(self.config_src.models + \ + return sorted(list(set(self.config_src.models + self.config_dst.models))) @property @@ -592,10 +592,7 @@ def __str__(self): def __neg__(self): return ConfigDelta(config_src=self.config_dst, - config_dst=self.config_src, - preferred_create=self.preferred_create, - preferred_replace=self.preferred_replace, - preferred_delete=self.preferred_delete) + config_dst=self.config_src) def __pos__(self): return self @@ -632,6 +629,7 @@ def __ne__(self, other): _cmperror(self, other) + class ConfigCompatibility(object): '''ConfigCompatibility diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index 25587a9..c3918bd 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -108,8 +108,8 @@ class NetconfCalculator(BaseCalculator): Attributes ---------- sub : `Element` - Content of a Netconf edit-config which can achieve a transition from one - config, i.e., self.etree2, to another config, i.e., self.etree1. + Content of a Netconf edit-config which can achieve a transition from + one config, i.e., self.etree2, to another config, i.e., self.etree1. add : `Element` Content of a Config instance. @@ -125,31 +125,49 @@ class NetconfCalculator(BaseCalculator): preferred_delete : `str` Preferred operation of deleting an existing element. Choice of 'delete' or 'remove'. + + diff_type : `str` + Choice of 'minimum' or 'replace'. This value has impact on attribute + nc. In general, there are two options to construct nc. The first + option is to find out minimal changes between config_src and + config_dst. Then attribute nc will reflect what needs to be modified. + The second option is to use 'replace' operation in Netconf. More + customers prefer 'replace' operation as it is more deterministic. + + replace_depth : `int` + Specify the deepest level of replace operation when diff_type is + 'replace'. Replace operation might be needed earlier before we reach + the specified level, depending on situations. Consider roots in a YANG + module are level 0, their children are level 1, and so on so forth. + The default value of replace_depth is 0. ''' def __init__(self, device, etree1, etree2, preferred_create='merge', preferred_replace='merge', - preferred_delete='delete'): + preferred_delete='delete', + diff_type='minimum', replace_depth=0): ''' __init__ instantiates a NetconfCalculator instance. ''' BaseCalculator.__init__(self, device, etree1, etree2) + self.diff_type = diff_type + self.replace_depth = replace_depth if preferred_create in ['merge', 'create', 'replace']: self.preferred_create = preferred_create else: - raise ValueError("only 'merge', 'create' or 'replace' are valid " \ + raise ValueError("only 'merge', 'create' or 'replace' are valid " "values of 'preferred_create'") if preferred_replace in ['merge', 'replace']: self.preferred_replace = preferred_replace else: - raise ValueError("only 'merge' or 'replace' are valid " \ + raise ValueError("only 'merge' or 'replace' are valid " "values of 'preferred_replace'") if preferred_delete in ['delete', 'remove']: self.preferred_delete = preferred_delete else: - raise ValueError("only 'delete' or 'remove' are valid " \ + raise ValueError("only 'delete' or 'remove' are valid " "values of 'preferred_delete'") @property @@ -163,9 +181,138 @@ def add(self): def sub(self): ele1 = deepcopy(self.etree1) ele2 = deepcopy(self.etree2) - self.node_sub(ele1, ele2) + if self.diff_type == 'replace' and self.replace_depth == 0: + self.get_config_replace(ele1, ele2) + else: + self.node_sub(ele1, ele2, depth=0) return ele1 + def get_config_replace(self, node_self, node_other): + '''get_config_replace + + High-level api: Build an edit-config using operation='replace'. It + will be mostly from self.etree1. + + Parameters + ---------- + + None + + Returns + ------- + + Element + An element represnting an edit-config. + ''' + + in_s_not_in_o, in_o_not_in_s, in_s_and_in_o = \ + self._group_kids(node_self, node_other) + ordered_by_user = {} + + for child_self in in_s_not_in_o: + child_self.set(operation_tag, 'replace') + s_node = self.device.get_schema_node(child_self) + if s_node.get('type') == 'leaf-list': + if ( + s_node.get('ordered-by') == 'user' and + s_node.tag not in ordered_by_user + ): + ordered_by_user[s_node.tag] = 'leaf-list' + elif s_node.get('type') == 'list': + keys = self._get_list_keys(s_node) + if ( + s_node.get('ordered-by') == 'user' and + s_node.tag not in ordered_by_user + ): + ordered_by_user[s_node.tag] = keys + + for child_other in in_o_not_in_s: + child_self = etree.Element(child_other.tag, + {operation_tag: self.preferred_delete}, + nsmap=child_other.nsmap) + siblings = list(node_self.iterchildren(tag=child_other.tag)) + if siblings: + siblings[-1].addnext(child_self) + else: + node_self.append(child_self) + s_node = self.device.get_schema_node(child_other) + if s_node.get('type') == 'leaf-list': + self._merge_text(child_other, child_self) + elif s_node.get('type') == 'list': + keys = self._get_list_keys(s_node) + for key in keys: + key_node = child_other.find(key) + e = etree.SubElement( + child_self, key, nsmap=key_node.nsmap) + e.text = key_node.text + + for child_self, child_other in in_s_and_in_o: + child_self.set(operation_tag, 'replace') + child_other.set(operation_tag, 'replace') + s_node = self.device.get_schema_node(child_self) + if s_node.get('type') == 'leaf': + if self._same_text(child_self, child_other): + if s_node.get('is_key'): + child_self.attrib.pop(operation_tag) + else: + node_self.remove(child_self) + elif s_node.get('type') == 'leaf-list': + if s_node.get('ordered-by') == 'user': + if s_node.tag not in ordered_by_user: + ordered_by_user[s_node.tag] = 'leaf-list' + else: + node_self.remove(child_self) + elif s_node.get('type') == 'container': + if ( + self._node_le(child_self, child_other) and + self._node_le(child_other, child_self) + ): + node_self.remove(child_self) + elif s_node.get('type') == 'list': + if ( + s_node.get('ordered-by') == 'user' and + s_node.tag not in ordered_by_user + ): + ordered_by_user[s_node.tag] = self._get_list_keys(s_node) + if ( + self._node_le(child_self, child_other) and + self._node_le(child_other, child_self) + ): + if s_node.get('ordered-by') != 'user': + node_self.remove(child_self) + else: + path = self.device.get_xpath(s_node) + raise ModelError("unknown schema node type: type of node {}" + "is '{}'".format(path, s_node.get('type'))) + + for tag in ordered_by_user: + scope_o = in_s_not_in_o + in_s_and_in_o + sequence = self._get_sequence(scope_o, tag, node_self) + for i, item in enumerate(sequence): + # modifying the namespace mapping of a node is not possible + # in lxml. See https://bugs.launchpad.net/lxml/+bug/555602 + # if 'yang' not in item.nsmap: + # item.nsmap['yang'] = yang_url + if i == 0: + item.set(insert_tag, 'first') + else: + item.set(insert_tag, 'after') + precursor = sequence[i - 1] + if ordered_by_user[tag] == 'leaf-list': + item.set(value_tag, precursor.text) + else: + keys = ordered_by_user[tag] + key_nodes = {k: precursor.find(k) for k in keys} + ids = { + k: self._url_to_prefix(n, k) + for k, n in key_nodes.items() + } + id_list = [ + "[{}='{}']".format(ids[k], key_nodes[k].text) + for k in keys + ] + item.set(key_tag, ''.join(id_list)) + def node_add(self, node_sum, node_other): '''node_add @@ -199,38 +346,70 @@ def node_add(self, node_sum, node_other): self._group_kids(node_sum, node_other) for child_self in in_s_not_in_o: pass + for child_other in in_o_not_in_s: + this_operation = child_other.get(operation_tag, default='merge') + # delete - if child_other.get(operation_tag) == 'delete': - raise ConfigDeltaError('data-missing: try to delete node {} ' \ - 'but it does not exist in config' \ - .format(self.device \ - .get_xpath(child_other))) + if this_operation == 'delete': + raise ConfigDeltaError( + 'data-missing: try to delete node {} but it does not ' + 'exist in config'.format( + self.device.get_xpath(child_other) + ) + ) + # remove - elif child_other.get(operation_tag) == 'remove': + elif this_operation == 'remove': pass - # merge, replace, create or none - else: + + # merge, create, replace or none + elif ( + this_operation == 'merge' or + this_operation == 'replace' or + this_operation == 'create' + ): s_node = self.device.get_schema_node(child_other) if s_node.get('type') in supported_node_type: - getattr(self, - '_node_add_without_peer_{}' \ - .format(s_node.get('type').replace('-', ''))) \ - (node_sum, child_other) + getattr( + self, + '_node_add_without_peer_{}'.format( + s_node.get('type').replace('-', '') + ) + )(node_sum, child_other) + + else: + raise ConfigDeltaError( + "unknown operation: node {} contains operation " + "'{}'".format( + self.device.get_xpath(child_other), + this_operation + ) + ) + for child_self, child_other in in_s_and_in_o: s_node = self.device.get_schema_node(child_self) if s_node.get('type') in supported_node_type: - getattr(self, - '_node_add_with_peer_{}' \ - .format(s_node.get('type').replace('-', ''))) \ - (child_self, child_other) + getattr( + self, + '_node_add_with_peer_{}'.format( + s_node.get('type').replace('-', '') + ) + )(child_self, child_other) + + if not list(child_self): + if ( + s_node.get('type') == 'container' and + s_node.get('presence') != 'true' + ): + node_sum.remove(child_self) def _node_add_without_peer_leaf(self, node_sum, child_other): '''_node_add_without_peer_leaf - Low-level api: Apply delta child_other to node_sum when there is no peer - of child_other can be found under node_sum. child_other is a leaf node. - Element node_sum will be modified during the process. + Low-level api: Apply delta child_other to node_sum when there is no + peer of child_other can be found under node_sum. child_other is a leaf + node. Element node_sum will be modified during the process. Parameters ---------- @@ -255,9 +434,9 @@ def _node_add_without_peer_leaf(self, node_sum, child_other): def _node_add_without_peer_leaflist(self, node_sum, child_other): '''_node_add_without_peer_leaflist - Low-level api: Apply delta child_other to node_sum when there is no peer - of child_other can be found under node_sum. child_other is a leaf-list - node. Element node_sum will be modified during the process. + Low-level api: Apply delta child_other to node_sum when there is no + peer of child_other can be found under node_sum. child_other is a + leaf-list node. Element node_sum will be modified during the process. Parameters ---------- @@ -325,9 +504,9 @@ def _node_add_without_peer_leaflist(self, node_sum, child_other): def _node_add_without_peer_container(self, node_sum, child_other): '''_node_add_without_peer_container - Low-level api: Apply delta child_other to node_sum when there is no peer - of child_other can be found under node_sum. child_other is a container - node. Element node_sum will be modified during the process. + Low-level api: Apply delta child_other to node_sum when there is no + peer of child_other can be found under node_sum. child_other is a + container node. Element node_sum will be modified during the process. Parameters ---------- @@ -346,15 +525,23 @@ def _node_add_without_peer_container(self, node_sum, child_other): There is no return of this method. ''' - e = deepcopy(child_other) - node_sum.append(self._del_attrib(e)) + this_operation = child_other.get(operation_tag, default='merge') + if this_operation == 'merge': + e = etree.SubElement(node_sum, child_other.tag, + nsmap=child_other.nsmap) + self.node_add(e, child_other) + elif ( + this_operation == 'replace' or + this_operation == 'create' + ): + node_sum.append(self._del_attrib(deepcopy(child_other))) def _node_add_without_peer_list(self, node_sum, child_other): '''_node_add_without_peer_list - Low-level api: Apply delta child_other to node_sum when there is no peer - of child_other can be found under node_sum. child_other is a list node. - Element node_sum will be modified during the process. + Low-level api: Apply delta child_other to node_sum when there is no + peer of child_other can be found under node_sum. child_other is a list + node. Element node_sum will be modified during the process. Parameters ---------- @@ -374,21 +561,32 @@ def _node_add_without_peer_list(self, node_sum, child_other): ''' s_node = self.device.get_schema_node(child_other) - e = deepcopy(child_other) + this_operation = child_other.get(operation_tag, default='merge') + if this_operation == 'merge': + e = etree.Element(child_other.tag, nsmap=child_other.nsmap) + for list_key_tag in self._get_list_keys(s_node): + key_ele_other = child_other.find(list_key_tag) + key_ele_self = deepcopy(key_ele_other) + e.append(self._del_attrib(key_ele_self)) + elif ( + this_operation == 'replace' or + this_operation == 'create' + ): + e = self._del_attrib(deepcopy(child_other)) scope = node_sum.getchildren() siblings = self._get_sequence(scope, child_other.tag, node_sum) if s_node.get('ordered-by') == 'user' and \ child_other.get(insert_tag) is not None: if child_other.get(insert_tag) == 'first': if siblings: - siblings[0].addprevious(self._del_attrib(e)) + siblings[0].addprevious(e) else: - node_sum.append(self._del_attrib(e)) + node_sum.append(e) elif child_other.get(insert_tag) == 'last': if siblings: - siblings[-1].addnext(self._del_attrib(e)) + siblings[-1].addnext(e) else: - node_sum.append(self._del_attrib(e)) + node_sum.append(e) elif child_other.get(insert_tag) == 'before': if child_other.get(key_tag) is None: _inserterror('before', self.device.get_xpath(child_other), @@ -400,7 +598,7 @@ def _node_add_without_peer_list(self, node_sum, child_other): path = self.device.get_xpath(child_other) key = child_other.get(key_tag) _inserterror('before', path, 'key', key) - sibling.addprevious(self._del_attrib(e)) + sibling.addprevious(e) elif child_other.get(insert_tag) == 'after': if child_other.get(key_tag) is None: _inserterror('after', self.device.get_xpath(child_other), @@ -412,12 +610,14 @@ def _node_add_without_peer_list(self, node_sum, child_other): path = self.device.get_xpath(child_other) key = child_other.get(key_tag) _inserterror('after', path, 'key', key) - sibling.addnext(self._del_attrib(e)) + sibling.addnext(e) else: if siblings: - siblings[-1].addnext(self._del_attrib(e)) + siblings[-1].addnext(e) else: - node_sum.append(self._del_attrib(e)) + node_sum.append(e) + if this_operation == 'merge': + self.node_add(e, child_other) def _node_add_with_peer_leaf(self, child_self, child_other): '''_node_add_with_peer_leaf @@ -444,33 +644,34 @@ def _node_add_with_peer_leaf(self, child_self, child_other): There is no return of this method. ''' - if child_other.get(operation_tag) is None: - child_self.text = child_other.text - elif child_other.get(operation_tag) == 'merge': - child_self.text = child_other.text - elif child_other.get(operation_tag) == 'replace': - child_self.text = child_other.text - elif child_other.get(operation_tag) == 'create': - raise ConfigDeltaError('data-exists: try to create node {} but ' \ - 'it already exists' \ + this_operation = child_other.get(operation_tag, default='merge') + if this_operation == 'merge': + self._merge_text(child_other, child_self) + elif this_operation == 'replace': + self._merge_text(child_other, child_self) + elif this_operation == 'create': + raise ConfigDeltaError('data-exists: try to create node {} but ' + 'it already exists' .format(self.device.get_xpath(child_other))) - elif child_other.get(operation_tag) == 'delete' or \ - child_other.get(operation_tag) == 'remove': + elif ( + this_operation == 'delete' or + this_operation == 'remove' + ): parent_self = child_self.getparent() parent_self.remove(child_self) else: - raise ConfigDeltaError("unknown operation: node {} contains " \ - "operation '{}'" \ + raise ConfigDeltaError("unknown operation: node {} contains " + "operation '{}'" .format(self.device.get_xpath(child_other), - child_other.get(operation_tag))) + this_operation)) def _node_add_with_peer_leaflist(self, child_self, child_other): '''_node_add_with_peer_leaflist Low-level api: Apply delta child_other to child_self when child_self is the peer of child_other. Element child_self and child_other are - leaf-list nodes. Element child_self will be modified during the process. - RFC6020 section 7.7.7 is a reference of this method. + leaf-list nodes. Element child_self will be modified during the + process. RFC6020 section 7.7.7 is a reference of this method. Parameters ---------- @@ -491,9 +692,9 @@ def _node_add_with_peer_leaflist(self, child_self, child_other): parent_self = child_self.getparent() s_node = self.device.get_schema_node(child_self) - if child_other.get(operation_tag) is None or \ - child_other.get(operation_tag) == 'merge' or \ - child_other.get(operation_tag) == 'replace': + this_operation = child_other.get(operation_tag, default='merge') + if this_operation == 'merge' or \ + this_operation == 'replace': if s_node.get('ordered-by') == 'user' and \ child_other.get(insert_tag) is not None: if child_other.get(insert_tag) == 'first': @@ -536,26 +737,28 @@ def _node_add_with_peer_leaflist(self, child_self, child_other): _inserterror('after', path, 'value', value) if sibling[0] != child_self: sibling[0].addnext(child_self) - elif child_other.get(operation_tag) == 'create': - raise ConfigDeltaError('data-exists: try to create node {} but ' \ - 'it already exists' \ + elif this_operation == 'create': + raise ConfigDeltaError('data-exists: try to create node {} but ' + 'it already exists' .format(self.device.get_xpath(child_other))) - elif child_other.get(operation_tag) == 'delete' or \ - child_other.get(operation_tag) == 'remove': + elif ( + this_operation == 'delete' or + this_operation == 'remove' + ): parent_self.remove(child_self) else: - raise ConfigDeltaError("unknown operation: node {} contains " \ - "operation '{}'" \ + raise ConfigDeltaError("unknown operation: node {} contains " + "operation '{}'" .format(self.device.get_xpath(child_other), - child_other.get(operation_tag))) + this_operation)) def _node_add_with_peer_container(self, child_self, child_other): '''_node_add_with_peer_container Low-level api: Apply delta child_other to child_self when child_self is the peer of child_other. Element child_self and child_other are - container nodes. Element child_self will be modified during the process. - RFC6020 section 7.5.8 is a reference of this method. + container nodes. Element child_self will be modified during the + process. RFC6020 section 7.5.8 is a reference of this method. Parameters ---------- @@ -575,24 +778,26 @@ def _node_add_with_peer_container(self, child_self, child_other): ''' parent_self = child_self.getparent() - if child_other.get(operation_tag) is None or \ - child_other.get(operation_tag) == 'merge': + this_operation = child_other.get(operation_tag, default='merge') + if this_operation == 'merge': self.node_add(child_self, child_other) - elif child_other.get(operation_tag) == 'replace': - e = deepcopy(child_other) - parent_self.replace(child_self, self._del_attrib(e)) - elif child_other.get(operation_tag) == 'create': - raise ConfigDeltaError('data-exists: try to create node {} but ' \ - 'it already exists' \ + elif this_operation == 'replace': + parent_self.replace(child_self, + self._del_attrib(deepcopy(child_other))) + elif this_operation == 'create': + raise ConfigDeltaError('data-exists: try to create node {} but ' + 'it already exists' .format(self.device.get_xpath(child_other))) - elif child_other.get(operation_tag) == 'delete' or \ - child_other.get(operation_tag) == 'remove': + elif ( + this_operation == 'delete' or + this_operation == 'remove' + ): parent_self.remove(child_self) else: - raise ConfigDeltaError("unknown operation: node {} contains " \ - "operation '{}'" \ + raise ConfigDeltaError("unknown operation: node {} contains " + "operation '{}'" .format(self.device.get_xpath(child_other), - child_other.get(operation_tag))) + this_operation)) def _node_add_with_peer_list(self, child_self, child_other): '''_node_add_with_peer_list @@ -621,8 +826,9 @@ def _node_add_with_peer_list(self, child_self, child_other): parent_self = child_self.getparent() s_node = self.device.get_schema_node(child_self) - if child_other.get(operation_tag) != 'delete' and \ - child_other.get(operation_tag) != 'remove' and \ + this_operation = child_other.get(operation_tag, default='merge') + if this_operation != 'delete' and \ + this_operation != 'remove' and \ s_node.get('ordered-by') == 'user' and \ child_other.get(insert_tag) is not None: if child_other.get(insert_tag) == 'first': @@ -663,26 +869,27 @@ def _node_add_with_peer_list(self, child_self, child_other): _inserterror('after', path, 'key', key) if sibling != child_self: sibling.addnext(child_self) - if child_other.get(operation_tag) is None or \ - child_other.get(operation_tag) == 'merge': + if this_operation == 'merge': self.node_add(child_self, child_other) - elif child_other.get(operation_tag) == 'replace': - e = deepcopy(child_other) - parent_self.replace(child_self, self._del_attrib(e)) - elif child_other.get(operation_tag) == 'create': - raise ConfigDeltaError('data-exists: try to create node {} but ' \ - 'it already exists' \ + elif this_operation == 'replace': + parent_self.replace(child_self, + self._del_attrib(deepcopy(child_other))) + elif this_operation == 'create': + raise ConfigDeltaError('data-exists: try to create node {} but ' + 'it already exists' .format(self.device.get_xpath(child_other))) - elif child_other.get(operation_tag) == 'delete' or \ - child_other.get(operation_tag) == 'remove': + elif ( + this_operation == 'delete' or + this_operation == 'remove' + ): parent_self.remove(child_self) else: - raise ConfigDeltaError("unknown operation: node {} contains " \ - "operation '{}'" \ + raise ConfigDeltaError("unknown operation: node {} contains " + "operation '{}'" .format(self.device.get_xpath(child_other), - child_other.get(operation_tag))) + this_operation)) - def node_sub(self, node_self, node_other): + def node_sub(self, node_self, node_other, depth=0): '''node_sub Low-level api: Compute the delta of two configs. This method is @@ -698,6 +905,12 @@ def node_sub(self, node_self, node_other): node_other : `Element` A config node in another config tree that is being processed. + depth : `int` + Specify the current level of processing. In other words, how many + hops from node_self to roots. Consider roots in a YANG module are + level 0, their children are level 1, and so on so forth. The + default value of depth is 0. + Returns ------- @@ -705,33 +918,20 @@ def node_sub(self, node_self, node_other): There is no return of this method. ''' - def same_leaf_list(tag): - list_self = [c for c in list(node_self) if c.tag == tag] - list_other = [c for c in list(node_other) if c.tag == tag] - s_node = self.device.get_schema_node((list_self + list_other)[0]) - if s_node.get('ordered-by') == 'user': - if [self._parse_text(i, s_node) for i in list_self] == \ - [self._parse_text(i, s_node) for i in list_other]: - return True - else: - return False - else: - if set([self._parse_text(i, s_node) for i in list_self]) == \ - set([self._parse_text(i, s_node) for i in list_other]): - return True - else: - return False - if self.preferred_replace != 'merge': - t_self = [c.tag for c in list(node_self) \ - if self.device.get_schema_node(c).get('type') == \ - 'leaf-list'] - t_other = [c.tag for c in list(node_other) \ - if self.device.get_schema_node(c).get('type') == \ - 'leaf-list'] + t_self = [ + c.tag for c in list(node_self) + if self.device.get_schema_node(c).get('type') == 'leaf-list' + ] + t_other = [ + c.tag for c in list(node_other) + if self.device.get_schema_node(c).get('type') == 'leaf-list' + ] commonalities = set(t_self) & set(t_other) for commonality in commonalities: - if not same_leaf_list(commonality): + if not self._same_leaf_list(commonality, + node_self, + node_other): node_self.set(operation_tag, 'replace') node_other.set(operation_tag, 'replace') return @@ -739,13 +939,14 @@ def same_leaf_list(tag): in_s_not_in_o, in_o_not_in_s, in_s_and_in_o = \ self._group_kids(node_self, node_other) ordered_by_user = {} - choice_nodes = {} for child_self in in_s_not_in_o: child_other = etree.Element(child_self.tag, {operation_tag: self.preferred_delete}, nsmap=child_self.nsmap) if self.preferred_create != 'merge': child_self.set(operation_tag, self.preferred_create) + if self.diff_type == 'replace': + child_self.set(operation_tag, 'replace') siblings = list(node_other.iterchildren(tag=child_self.tag)) if siblings: siblings[-1].addnext(child_other) @@ -756,7 +957,7 @@ def same_leaf_list(tag): if s_node.get('ordered-by') == 'user' and \ s_node.tag not in ordered_by_user: ordered_by_user[s_node.tag] = 'leaf-list' - child_other.text = child_self.text + self._merge_text(child_self, child_other) elif s_node.get('type') == 'list': keys = self._get_list_keys(s_node) if s_node.get('ordered-by') == 'user' and \ @@ -764,11 +965,9 @@ def same_leaf_list(tag): ordered_by_user[s_node.tag] = keys for key in keys: key_node = child_self.find(key) - e = etree.SubElement(child_other, key, nsmap=key_node.nsmap) + e = etree.SubElement( + child_other, key, nsmap=key_node.nsmap) e.text = key_node.text - if s_node.getparent().get('type') == 'case': - # key: choice node, value: case node - choice_nodes[s_node.getparent().getparent()] = s_node.getparent() for child_other in in_o_not_in_s: child_self = etree.Element(child_other.tag, {operation_tag: self.preferred_delete}, @@ -776,27 +975,16 @@ def same_leaf_list(tag): if self.preferred_create != 'merge': child_other.set(operation_tag, self.preferred_create) siblings = list(node_self.iterchildren(tag=child_other.tag)) - s_node = self.device.get_schema_node(child_other) if siblings: siblings[-1].addnext(child_self) else: - # Append node if: - # Node not in case - # Node in case but choice node not in self - # Node in case and choice node in self and the same case also - # in self - if s_node.getparent().get('type') == 'case': - choice_node = s_node.getparent().getparent() - if choice_node not in choice_nodes or \ - s_node.getparent() == choice_nodes[choice_node]: - node_self.append(child_self) - else: - node_self.append(child_self) + node_self.append(child_self) + s_node = self.device.get_schema_node(child_other) if s_node.get('type') == 'leaf-list': if s_node.get('ordered-by') == 'user' and \ s_node.tag not in ordered_by_user: ordered_by_user[s_node.tag] = 'leaf-list' - child_self.text = child_other.text + self._merge_text(child_other, child_self) elif s_node.get('type') == 'list': keys = self._get_list_keys(s_node) if s_node.get('ordered-by') == 'user' and \ @@ -809,7 +997,7 @@ def same_leaf_list(tag): for child_self, child_other in in_s_and_in_o: s_node = self.device.get_schema_node(child_self) if s_node.get('type') == 'leaf': - if child_self.text == child_other.text: + if self._same_text(child_self, child_other): if not s_node.get('is_key'): node_self.remove(child_self) node_other.remove(child_other) @@ -830,7 +1018,13 @@ def same_leaf_list(tag): node_self.remove(child_self) node_other.remove(child_other) else: - self.node_sub(child_self, child_other) + if ( + self.diff_type == 'replace' and + self.replace_depth == depth + 1 + ): + self.get_config_replace(child_self, child_other) + else: + self.node_sub(child_self, child_other, depth=depth+1) elif s_node.get('type') == 'list': if s_node.get('ordered-by') == 'user' and \ s_node.tag not in ordered_by_user: @@ -850,16 +1044,24 @@ def same_leaf_list(tag): node_self.remove(child_self) node_other.remove(child_other) else: - self.node_sub(child_self, child_other) + if ( + self.diff_type == 'replace' and + self.replace_depth == depth + 1 + ): + self.get_config_replace(child_self, child_other) + else: + self.node_sub(child_self, child_other, depth=depth+1) else: path = self.device.get_xpath(s_node) - raise ModelError("unknown schema node type: type of node {}" \ + raise ModelError("unknown schema node type: type of node {}" "is '{}'".format(path, s_node.get('type'))) for tag in ordered_by_user: scope_s = in_s_not_in_o + in_s_and_in_o scope_o = in_o_not_in_s + in_s_and_in_o - for sequence in self._get_sequence(scope_s, tag, node_self), \ - self._get_sequence(scope_o, tag, node_other): + for sequence in ( + self._get_sequence(scope_s, tag, node_self), + self._get_sequence(scope_o, tag, node_other), + ): for item in sequence: # modifying the namespace mapping of a node is not possible # in lxml. See https://bugs.launchpad.net/lxml/+bug/555602 @@ -876,11 +1078,15 @@ def same_leaf_list(tag): else: keys = ordered_by_user[tag] key_nodes = {k: precursor.find(k) for k in keys} - ids = {k: self._url_to_prefix(n, k) \ - for k, n in key_nodes.items()} - l = ["[{}='{}']".format(ids[k], key_nodes[k].text) \ - for k in keys] - item.set(key_tag, ''.join(l)) + ids = { + k: self._url_to_prefix(n, k) + for k, n in key_nodes.items() + } + id_list = [ + "[{}='{}']".format(ids[k], key_nodes[k].text) + for k in keys + ] + item.set(key_tag, ''.join(id_list)) @staticmethod def _url_to_prefix(node, id): @@ -894,7 +1100,7 @@ def _url_to_prefix(node, id): Parameters ---------- - node : `str` + node : `Element` A config node. Its identifier will be converted. id : `str` @@ -916,3 +1122,50 @@ def _url_to_prefix(node, id): else: return prefixes[ret.group(1)] + ':' + ret.group(2) return id + + def _same_leaf_list(self, leaf_list_tag, parent_node_1, parent_node_2): + '''_same_leaf_list + + Low-level api: Return True when two leaf-list's that are identified by + leaf_list_tag are same. + + Parameters + ---------- + + leaf_list_tag : `str` + A config node. Its identifier will be converted. + + parent_node_1 : `Element` + One parent node. One or multiple leaf-list nodes are its children. + + parent_node_2 : `Element` + The other parent node. One or multiple leaf-list nodes are its + children. + + Returns + ------- + + bool + True when the leaf-list nodes under two parent nodes are same. + Otherwise False. + ''' + + list_1 = [c for c in list(parent_node_1) if c.tag == leaf_list_tag] + list_2 = [c for c in list(parent_node_2) if c.tag == leaf_list_tag] + s_node = self.device.get_schema_node((list_1 + list_2)[0]) + if s_node.get('ordered-by') == 'user': + if ( + [self._parse_text(i, s_node) for i in list_1] == + [self._parse_text(i, s_node) for i in list_2] + ): + return True + else: + return False + else: + if ( + set([self._parse_text(i, s_node) for i in list_1]) == + set([self._parse_text(i, s_node) for i in list_2]) + ): + return True + else: + return False \ No newline at end of file diff --git a/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py b/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py index 0432435..e1b8caf 100755 --- a/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py +++ b/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py @@ -3,18 +3,77 @@ import unittest from lxml import etree -from yang.ncdiff import * -from yang.ncdiff.errors import * +from yang.ncdiff.device import ModelDevice +from yang.ncdiff.config import Config, ConfigDelta +from yang.ncdiff.errors import ConfigDeltaError +from yang.ncdiff.composer import Tag from yang.connector import Netconf from pyats.topology import loader +from ncclient import operations, xml_ +from ncclient.manager import Manager +from ncclient.devices.default import DefaultDeviceHandler + +nc_url = xml_.BASE_NS_1_0 +yang_url = 'urn:ietf:params:xml:ns:yang:1' +operation_tag = '{' + nc_url + '}operation' +insert_tag = '{' + yang_url + '}insert' +value_tag = '{' + yang_url + '}value' +key_tag = '{' + yang_url + '}key' def my_execute(*args, **kwargs): + if args[1] is operations.retrieve.GetSchema and len(args) > 2: + tests_dir = path.dirname(path.abspath(__file__)) + filename = args[2] + ".yang" + with open(path.join(tests_dir, "yang", filename), "r") as f: + text = f.read() + reply_xml = """ + + + + """ + reply_ele = xml_.to_ele(reply_xml) + data = reply_ele.find( + 'mon:data', + namespaces={ + 'mon': 'urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring', + } + ) + data.text = text + reply_xml = xml_.to_xml(reply_ele) + reply = operations.rpc.RPCReply(reply_xml) + reply.parse() + reply.data = text + return reply + elif ( + args[1] is operations.retrieve.Get and ( + 'ietf-netconf-monitoring' in kwargs['filter'] or + 'ietf-yang-library' in kwargs['filter'] + ) + ): + reply_xml = """ + + + rpc + missing-attribute + error + + message-id + rpc + + + + """ + reply = operations.rpc.RPCReply(reply_xml) + reply.parse() + return reply + reply_xml = """Mgmt-intfMgmt-intfoc-ni-types:L3VRFoc-types:IPV6oc-types:IPV4GigabitEthernet0GigabitEthernet0GigabitEthernet0oc-pol-types:DIRECTLY_CONNECTEDoc-types:IPV4oc-pol-types:DIRECTLY_CONNECTEDoc-types:IPV4
oc-pol-types:DIRECTLY_CONNECTEDoc-types:IPV6oc-pol-types:DIRECTLY_CONNECTEDoc-types:IPV6
oc-pol-types:STATICoc-types:IPV4oc-pol-types:STATICoc-types:IPV4
oc-pol-types:STATICoc-types:IPV6oc-pol-types:STATICoc-types:IPV6
oc-pol-types:BGP100oc-pol-types:BGP100falsefalseoc-pol-types:STATICDEFAULToc-pol-types:STATICDEFAULT0.0.0.0/00.0.0.0/05.28.0.15.28.0.15.28.0.1oc-pol-types:DIRECTLY_CONNECTEDDEFAULToc-pol-types:DIRECTLY_CONNECTEDDEFAULT
""" reply = operations.rpc.RPCReply(reply_xml) reply.parse() return reply + Netconf.execute = my_execute @@ -58,7 +117,6 @@ def add_listener(self, listener): def is_alive(self): return True - server_capabilities = [ 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-UNIFIED-FIREWALL-MIB?module=CISCO-UNIFIED-FIREWALL-MIB&revision=2005-09-22', 'urn:ietf:params:xml:ns:yang:smiv2:IANA-RTPROTO-MIB?module=IANA-RTPROTO-MIB&revision=2000-09-26', @@ -67,7 +125,6 @@ def is_alive(self): 'urn:ietf:params:xml:ns:yang:smiv2:INET-ADDRESS-MIB?module=INET-ADDRESS-MIB&revision=2005-02-04', 'http://cisco.com/ns/yang/Cisco-IOS-XE-arp?module=Cisco-IOS-XE-arp&revision=2017-11-07', 'urn:ietf:params:xml:ns:yang:smiv2:EtherLike-MIB?module=EtherLike-MIB&revision=2003-09-19', -'urn:ietf:params:xml:ns:yang:smiv2:RFC-1212?module=RFC-1212', 'urn:ietf:params:xml:ns:yang:ietf-diffserv-target?module=ietf-diffserv-target&revision=2015-04-07&features=target-inline-policy-config', 'urn:ietf:params:xml:ns:yang:smiv2:RMON2-MIB?module=RMON2-MIB&revision=1996-05-27', 'http://cisco.com/ns/yang/cisco-xe-bgp-policy-deviation?module=cisco-xe-openconfig-bgp-policy-deviation&revision=2017-07-24', @@ -75,7 +132,6 @@ def is_alive(self): 'http://cisco.com/ns/yang/Cisco-IOS-XE-spanning-tree?module=Cisco-IOS-XE-spanning-tree&revision=2017-11-27', 'http://openconfig.net/yang/rib/bgp-types?module=openconfig-rib-bgp-types&revision=2016-04-11', 'http://cisco.com/ns/yang/cisco-xe-openconfig-acl-ext?module=cisco-xe-openconfig-acl-ext&revision=2017-03-30', -'urn:ietf:params:xml:ns:yang:smiv2:RFC1315-MIB?module=RFC1315-MIB', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ospfv3?module=Cisco-IOS-XE-ospfv3&revision=2017-11-27', 'http://cisco.com/ns/yang/cisco-xe-routing-policy-deviation?module=cisco-xe-openconfig-routing-policy-deviation&revision=2017-03-30', 'http://cisco.com/ns/yang/Cisco-IOS-XE-device-hardware-oper?module=Cisco-IOS-XE-device-hardware-oper&revision=2017-11-01', @@ -87,10 +143,8 @@ def is_alive(self): 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-SIP-UA-MIB?module=CISCO-SIP-UA-MIB&revision=2004-02-19', 'urn:ietf:params:xml:ns:netconf:base:1.0?module=ietf-netconf&revision=2011-06-01', 'http://cisco.com/ns/yang/Cisco-IOS-XE-isis?module=Cisco-IOS-XE-isis&revision=2017-11-27', -'http://cisco.com/ns/yang/cisco-xe-openconfig-spanning-tree-deviation?module=cisco-xe-openconfig-spanning-tree-deviation&revision=2017-08-21', 'urn:ietf:params:xml:ns:yang:smiv2:ENTITY-MIB?module=ENTITY-MIB&revision=2005-08-10', 'urn:ietf:params:xml:ns:yang:smiv2:RMON-MIB?module=RMON-MIB&revision=2000-05-11', -'http://cisco.com/ns/yang/Cisco-IOS-XE-ptp?module=Cisco-IOS-XE-ptp&revision=2017-09-19', 'urn:ietf:params:xml:ns:yang:smiv2:IPMROUTE-STD-MIB?module=IPMROUTE-STD-MIB&revision=2000-09-22', 'urn:ietf:params:xml:ns:yang:smiv2:HCNUM-TC?module=HCNUM-TC&revision=2000-06-08', 'urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit&also-supported=report-all-tagged', 'urn:ietf:params:xml:ns:yang:smiv2:BRIDGE-MIB?module=BRIDGE-MIB&revision=2005-09-19', @@ -102,7 +156,6 @@ def is_alive(self): 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RF-MIB?module=CISCO-RF-MIB&revision=2005-09-01', 'urn:ietf:params:xml:ns:yang:smiv2:DIFFSERV-DSCP-TC?module=DIFFSERV-DSCP-TC&revision=2002-05-09', 'urn:ietf:params:xml:ns:yang:ietf-ipv6-unicast-routing?module=ietf-ipv6-unicast-routing&revision=2015-05-25&deviations=cisco-xe-ietf-ipv6-unicast-routing-deviation', -'http://cisco.com/ns/yang/Cisco-IOS-XE-udld?module=Cisco-IOS-XE-udld&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSLA-ECHO-MIB?module=CISCO-IPSLA-ECHO-MIB&revision=2007-08-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-CBP-TC-MIB?module=CISCO-CBP-TC-MIB&revision=2008-06-24', 'http://cisco.com/ns/yang/Cisco-IOS-XE-environment-oper?module=Cisco-IOS-XE-environment-oper&revision=2017-10-23', @@ -136,32 +189,26 @@ def is_alive(self): 'http://cisco.com/ns/yang/Cisco-IOS-XE-nhrp?module=Cisco-IOS-XE-nhrp&revision=2017-02-07', 'http://cisco.com/yang/cisco-odm?module=cisco-odm&revision=2017-04-25', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-AAA-SESSION-MIB?module=CISCO-AAA-SESSION-MIB&revision=2006-03-21', -'http://openconfig.net/yang/platform?module=openconfig-platform&revision=2016-12-22&deviations=cisco-xe-openconfig-platform-deviation', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-TC?module=CISCO-TC&revision=2011-11-11', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ntp-oper?module=Cisco-IOS-XE-ntp-oper&revision=2017-11-01', 'http://cisco.com/yang/cisco-self-mgmt?module=cisco-self-mgmt&revision=2016-05-14', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-FLASH-MIB?module=CISCO-FLASH-MIB&revision=2013-08-06', -'http://cisco.com/ns/yang/Cisco-IOS-XE-switch?module=Cisco-IOS-XE-switch&revision=2017-11-27&deviations=Cisco-IOS-XE-switch-deviation', -'http://cisco.com/ns/yang/cisco-xe-openconfig-platform-deviation?module=cisco-xe-openconfig-platform-deviation&revision=2017-09-01', 'http://cisco.com/yang/cisco-dmi-aaa?module=cisco-dmi-aaa&revision=2017-05-17', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-VLAN-IFTABLE-RELATIONSHIP-MIB?module=CISCO-VLAN-IFTABLE-RELATIONSHIP-MIB&revision=2013-07-15', 'urn:ietf:params:netconf:capability:yang-library:1.0?revision=2016-06-21&module-set-id=f47eea7ecf2253450bb07d0f78ca7104', 'http://cisco.com/ns/yang/Cisco-IOS-XE-eem?module=Cisco-IOS-XE-eem&revision=2017-11-20', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cfm-oper?module=Cisco-IOS-XE-cfm-oper&revision=2017-06-06', -'http://cisco.com/ns/yang/Cisco-IOS-XE-avb?module=Cisco-IOS-XE-avb&revision=2017-09-19', 'http://openconfig.net/yang/oc-mapping-acl?module=oc-mapping-acl&revision=2017-05-26', 'http://cisco.com/ns/yang/Cisco-IOS-XE-aaa-oper?module=Cisco-IOS-XE-aaa-oper&revision=2017-11-01', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-CONFIG-COPY-MIB?module=CISCO-CONFIG-COPY-MIB&revision=2005-04-06', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-CBP-TARGET-TC-MIB?module=CISCO-CBP-TARGET-TC-MIB&revision=2006-03-24', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-DOT3-OAM-MIB?module=CISCO-DOT3-OAM-MIB&revision=2006-05-31', -'http://cisco.com/ns/yang/Cisco-IOS-XE-switch-deviation?module=Cisco-IOS-XE-switch-deviation&revision=2016-12-01', 'http://tail-f.com/yang/netconf-monitoring?module=tailf-netconf-monitoring&revision=2016-11-24', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-VTP-MIB?module=CISCO-VTP-MIB&revision=2013-10-14', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-BGP4-MIB?module=CISCO-BGP4-MIB&revision=2010-09-30', 'http://openconfig.net/yang/network-instance-types?module=openconfig-network-instance-types&revision=2016-12-15', 'http://cisco.com/ns/yang/Cisco-IOS-XE-checkpoint-archive-oper?module=Cisco-IOS-XE-checkpoint-archive-oper&revision=2017-04-01', 'urn:ietf:params:netconf:capability:xpath:1.0', 'http://cisco.com/ns/yang/Cisco-IOS-XE-sanet?module=Cisco-IOS-XE-sanet&revision=2017-11-27', -'urn:ietf:params:xml:ns:yang:ietf-interfaces-ext?module=ietf-interfaces-ext', 'urn:ietf:params:xml:ns:yang:ietf-interfaces?module=ietf-interfaces&revision=2014-05-08&features=pre-provisioning,if-mib,arbitrary-names', 'urn:ietf:params:netconf:capability:writable-running:1.0', 'http://openconfig.net/yang/bgp-policy?module=openconfig-bgp-policy&revision=2016-06-21&deviations=cisco-xe-openconfig-bgp-policy-deviation', @@ -180,7 +227,6 @@ def is_alive(self): 'urn:ietf:params:xml:ns:yang:ietf-diffserv-policy?module=ietf-diffserv-policy&revision=2015-04-07&features=policy-template-support,hierarchial-policy-support', 'http://cisco.com/ns/yang/Cisco-IOS-XE-igmp?module=Cisco-IOS-XE-igmp&revision=2017-11-27', '\n urn:ietf:params:netconf:capability:notification:1.1\n ', -'http://cisco.com/ns/yang/Cisco-IOS-XE-vstack?module=Cisco-IOS-XE-vstack&revision=2017-02-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mpls-ldp?module=Cisco-IOS-XE-mpls-ldp&revision=2017-02-07&features=mpls-ldp-nsr,mpls-ldp-iccp,mpls-ldp-extended,mpls-ldp-bgp', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IMAGE-MIB?module=CISCO-IMAGE-MIB&revision=1995-08-15', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ppp?module=Cisco-IOS-XE-ppp&revision=2017-11-29', @@ -204,14 +250,11 @@ def is_alive(self): 'http://cisco.com/ns/yang/Cisco-IOS-XE-mdt-cfg?module=Cisco-IOS-XE-mdt-cfg&revision=2017-09-20', 'urn:ietf:params:xml:ns:yang:ietf-event-notifications?module=ietf-event-notifications&revision=2016-10-27&features=json,configured-subscriptions&deviations=cisco-xe-ietf-event-notifications-deviation,cisco-xe-ietf-yang-push-deviation', 'http://cisco.com/ns/yang/Cisco-IOS-XE-vlan?module=Cisco-IOS-XE-vlan&revision=2017-10-02', -'http://cisco.com/ns/yang/Cisco-IOS-XE-device-sensor?module=Cisco-IOS-XE-device-sensor&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ST-TC?module=CISCO-ST-TC&revision=2012-08-08', 'http://openconfig.net/yang/cisco-xe-openconfig-interfaces-deviation?module=cisco-xe-openconfig-interfaces-deviation&revision=2017-10-30', 'http://cisco.com/ns/yang/Cisco-IOS-XE-route-map?module=Cisco-IOS-XE-route-map&revision=2017-07-27', -'http://cisco.com/ns/genet/genet-state?module=genet-state', 'urn:ietf:params:xml:ns:yang:ietf-ipv4-unicast-routing?module=ietf-ipv4-unicast-routing&revision=2015-05-25&deviations=cisco-xe-ietf-ipv4-unicast-routing-deviation', 'http://openconfig.net/yang/aaa?module=openconfig-aaa&revision=2017-09-18', -'http://cisco.com/ns/yang/Cisco-IOS-XE-coap?module=Cisco-IOS-XE-coap&revision=2017-02-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-acl?module=Cisco-IOS-XE-acl&revision=2017-08-01', 'urn:ietf:params:xml:ns:yang:smiv2:SNMP-TARGET-MIB?module=SNMP-TARGET-MIB&revision=1998-08-04', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mpls?module=Cisco-IOS-XE-mpls&revision=2017-11-27', @@ -219,7 +262,6 @@ def is_alive(self): 'http://openconfig.net/yang/lldp?module=openconfig-lldp&revision=2016-05-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-DATA-COLLECTION-MIB?module=CISCO-DATA-COLLECTION-MIB&revision=2002-10-30', 'http://cisco.com/ns/yang/Cisco-IOS-XE-flow?module=Cisco-IOS-XE-flow&revision=2017-11-27', -'http://cisco.com/ns/yang/cisco-xe-openconfig-vlan-deviation?module=cisco-xe-openconfig-vlan-deviation&revision=2017-03-17', 'urn:ietf:params:xml:ns:yang:smiv2:RFC1213-MIB?module=RFC1213-MIB', 'http://cisco.com/ns/yang/Cisco-IOS-XE-types?module=Cisco-IOS-XE-types&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:ietf-yang-smiv2?module=ietf-yang-smiv2&revision=2012-06-22', @@ -253,23 +295,20 @@ def is_alive(self): 'http://cisco.com/ns/yang/Cisco-IOS-XE-bgp-common-oper?module=Cisco-IOS-XE-bgp-common-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:cisco-policy?module=cisco-policy&revision=2016-03-30', 'http://cisco.com/ns/yang/Cisco-IOS-XE-platform?module=Cisco-IOS-XE-platform&revision=2017-06-02', -'http://cisco.com/ns/yang/cisco-xe-openconfig-vlan-ext?module=cisco-xe-openconfig-vlan-ext&revision=2017-06-14&deviations=cisco-xe-openconfig-vlan-deviation', 'http://tail-f.com/yang/common-monitoring?module=tailf-common-monitoring&revision=2013-06-14', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-UBE-MIB?module=CISCO-UBE-MIB&revision=2010-11-29', -'http://cisco.com/ns/yang/Cisco-IOS-XE-power?module=Cisco-IOS-XE-power&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:SNMPv2-TC?module=SNMPv2-TC', 'urn:ietf:params:xml:ns:yang:ietf-yang-types?module=ietf-yang-types&revision=2013-07-15', 'http://openconfig.net/yang/vlan-types?module=openconfig-vlan-types&revision=2016-05-26', 'urn:ietf:params:xml:ns:yang:smiv2:DRAFT-MSDP-MIB?module=DRAFT-MSDP-MIB&revision=1999-12-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-HSRP-EXT-MIB?module=CISCO-HSRP-EXT-MIB&revision=2010-09-02', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RTTMON-TC-MIB?module=CISCO-RTTMON-TC-MIB&revision=2012-05-25', -'urn:ietf:params:xml:ns:yang:smiv2:RFC1155-SMI?module=RFC1155-SMI', 'http://tail-f.com/yang/confd-monitoring?module=tailf-confd-monitoring&revision=2013-06-14', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-NETSYNC-MIB?module=CISCO-NETSYNC-MIB&revision=2010-10-15', 'http://cisco.com/ns/yang/Cisco-IOS-XE-efp-oper?module=Cisco-IOS-XE-efp-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-STP-EXTENSIONS-MIB?module=CISCO-STP-EXTENSIONS-MIB&revision=2013-03-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-features?module=Cisco-IOS-XE-features&revision=2017-02-07&features=vlan,table-map,switching-platform,qos-qsm,private-vlan,parameter-map,l2vpn,l2,dot1x,crypto', -'http://openconfig.net/yang/vlan?module=openconfig-vlan&revision=2016-05-26&deviations=cisco-xe-openconfig-vlan-deviation', +'http://openconfig.net/yang/vlan?module=openconfig-vlan&revision=2016-05-26', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IETF-BFD-MIB?module=CISCO-IETF-BFD-MIB&revision=2011-04-16', 'http://cisco.com/ns/yang/Cisco-IOS-XE-process-memory-oper?module=Cisco-IOS-XE-process-memory-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:P-BRIDGE-MIB?module=P-BRIDGE-MIB&revision=2006-01-09', @@ -300,16 +339,15 @@ def is_alive(self): 'urn:ietf:params:xml:ns:yang:smiv2:TCP-MIB?module=TCP-MIB&revision=2005-02-18', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ENHANCED-MEMPOOL-MIB?module=CISCO-ENHANCED-MEMPOOL-MIB&revision=2008-12-05', 'http://openconfig.net/yang/oc-mapping-network-instance?module=oc-mapping-network-instance&revision=2017-01-17', 'http://cisco.com/ns/yang/Cisco-IOS-XE-bgp-route-oper?module=Cisco-IOS-XE-bgp-route-oper&revision=2017-09-25', -'http://cisco.com/ns/yang/Cisco-IOS-XE-vlan-oper?module=Cisco-IOS-XE-vlan-oper&revision=2017-05-05', 'http://cisco.com/ns/yang/Cisco-IOS-XE-nd?module=Cisco-IOS-XE-nd&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:IPV6-FLOW-LABEL-MIB?module=IPV6-FLOW-LABEL-MIB&revision=2003-08-28', -'http://cisco.com/ns/yang/cisco-xe-openconfig-spanning-tree-ext?module=cisco-xe-openconfig-spanning-tree-ext&revision=2017-10-24', 'http://cisco.com/ns/yang/Cisco-IOS-XE-rip?module=Cisco-IOS-XE-rip&revision=2017-11-27', 'urn:ietf:params:netconf:base:1.1', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-LICENSE-MGMT-MIB?module=CISCO-LICENSE-MGMT-MIB&revision=2012-04-19', 'http://cisco.com/ns/yang/Cisco-IOS-XE-native?module=Cisco-IOS-XE-native&revision=2017-10-24&deviations=Cisco-IOS-XE-switch-deviation', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-DYNAMIC-TEMPLATE-MIB?module=CISCO-DYNAMIC-TEMPLATE-MIB&revision=2007-09-06', -'http://cisco.com/ns/cisco-xe-ietf-routing-deviation?module=cisco-xe-ietf-routing-deviation&revision=2016-07-09', 'http://cisco.com/ns/yang/ios-xe/template?module=Cisco-IOS-XE-template&revision=2017-11-06', 'http://openconfig.net/yang/cisco-xe-openconfig-if-ethernet-deviation?module=cisco-xe-openconfig-if-ethernet-deviation&revision=2017-11-01', 'http://cisco.com/ns/yang/Cisco-IOS-XE-track?module=Cisco-IOS-XE-track&revision=2017-04-28', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-EMBEDDED-EVENT-MGR-MIB?module=CISCO-EMBEDDED-EVENT-MGR-MIB&revision=2006-11-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-common-types?module=Cisco-IOS-XE-common-types&revision=2017-09-25', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mvrp?module=Cisco-IOS-XE-mvrp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-interfaces-oper?module=Cisco-IOS-XE-interfaces-oper&revision=2017-10-10', 'urn:ietf:params:xml:ns:yang:smiv2:Q-BRIDGE-MIB?module=Q-BRIDGE-MIB&revision=2006-01-09', 'http://cisco.com/ns/yang/Cisco-IOS-XE-wccp?module=Cisco-IOS-XE-wccp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lldp-oper?module=Cisco-IOS-XE-lldp-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:nvo?module=nvo&revision=2015-06-02&deviations=nvo-devs', 'http://openconfig.net/yang/interfaces/aggregate?module=openconfig-if-aggregate&revision=2016-12-22&deviations=cisco-xe-openconfig-interfaces-deviation', 'urn:ietf:params:xml:ns:yang:ietf-key-chain?module=ietf-key-chain&revision=2015-02-24&features=independent-send-accept-lifetime,hex-key-string,accept-tolerance', 'urn:ietf:params:xml:ns:yang:smiv2:ETHER-WIS?module=ETHER-WIS&revision=2003-09-19', 'urn:cisco:params:xml:ns:yang:pim?module=pim&revision=2014-06-27&features=bsr,auto-rp', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mld?module=Cisco-IOS-XE-mld&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:ENTITY-SENSOR-MIB?module=ENTITY-SENSOR-MIB&revision=2002-12-16', 'urn:ietf:params:netconf:capability:validate:1.1', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mmode?module=Cisco-IOS-XE-mmode&revision=2017-06-15', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ENVMON-MIB?module=CISCO-ENVMON-MIB&revision=2003-12-01', 'http://openconfig.net/yang/optical-amplfier?module=openconfig-optical-amplifier&revision=2016-03-31', 'urn:ietf:params:xml:ns:yang:smiv2:DISMAN-EXPRESSION-MIB?module=DISMAN-EXPRESSION-MIB&revision=2000-10-16', 'http://openconfig.net/yang/types/yang?module=openconfig-yang-types&revision=2017-07-30', 'urn:ietf:params:xml:ns:yang:cisco-ospf?module=cisco-ospf&revision=2016-03-30&features=graceful-shutdown,flood-reduction,database-filter', 'http://openconfig.net/yang/spanning-tree?module=openconfig-spanning-tree&revision=2017-07-14&deviations=cisco-xe-openconfig-spanning-tree-deviation', 'http://openconfig.net/yang/bgp?module=openconfig-bgp&revision=2016-06-21', 'urn:ietf:params:xml:ns:yang:smiv2:TOKEN-RING-RMON-MIB?module=TOKEN-RING-RMON-MIB', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ENTITY-ALARM-MIB?module=CISCO-ENTITY-ALARM-MIB&revision=1999-07-06', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ethernet?module=Cisco-IOS-XE-ethernet&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-QOS-PIB-MIB?module=CISCO-QOS-PIB-MIB&revision=2007-08-29', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ospf?module=Cisco-IOS-XE-ospf&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:MPLS-VPN-MIB?module=MPLS-VPN-MIB&revision=2001-10-15', 'http://cisco.com/ns/yang/Cisco-IOS-XE-nat-oper?module=Cisco-IOS-XE-nat-oper&revision=2017-11-01', 'http://tail-f.com/ns/common/query?module=tailf-common-query&revision=2017-04-27', 'http://openconfig.net/yang/oc-mapping-stp?module=oc-mapping-stp&revision=2017-02-07', 'http://cisco.com/ns/yang/cisco-xe-openconfig-bgp-deviation?module=cisco-xe-openconfig-bgp-deviation&revision=2017-05-24', 'http://openconfig.net/yang/bgp-types?module=openconfig-bgp-types&revision=2016-06-21', 'http://openconfig.net/yang/policy-types?module=openconfig-policy-types&revision=2016-05-12', 'http://tail-f.com/ns/netconf/extensions', 'http://openconfig.net/yang/interfaces/ip-ext?module=openconfig-if-ip-ext&revision=2016-12-22', 'urn:ietf:params:xml:ns:yang:smiv2:LLDP-MIB?module=LLDP-MIB&revision=2005-05-06', 'http://cisco.com/ns/yang/cisco-xe-ietf-yang-push-deviation?module=cisco-xe-ietf-yang-push-deviation&revision=2017-08-22', 'urn:ietf:params:xml:ns:yang:smiv2:TUNNEL-MIB?module=TUNNEL-MIB&revision=2005-05-16', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ntp?module=Cisco-IOS-XE-ntp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mdt-common-defs?module=Cisco-IOS-XE-mdt-common-defs&revision=2017-07-01', 'urn:ietf:params:xml:ns:yang:ietf-yang-push?module=ietf-yang-push&revision=2016-10-28&features=on-change&deviations=cisco-xe-ietf-yang-push-deviation', 'http://openconfig.net/yang/transport-line-common?module=openconfig-transport-line-common&revision=2016-03-31', 'urn:ietf:params:xml:ns:yang:smiv2:OSPF-MIB?module=OSPF-MIB&revision=2006-11-10', 'http://cisco.com/ns/yang/cisco-xe-openconfig-rib-bgp-ext?module=cisco-xe-openconfig-rib-bgp-ext&revision=2016-11-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RTTMON-MIB?module=CISCO-RTTMON-MIB&revision=2012-08-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-HSRP-MIB?module=CISCO-HSRP-MIB&revision=2010-09-06', 'http://openconfig.net/yang/types/inet?module=openconfig-inet-types&revision=2017-08-24', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lldp?module=Cisco-IOS-XE-lldp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cts?module=Cisco-IOS-XE-cts&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-snmp?module=Cisco-IOS-XE-snmp&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults?module=ietf-netconf-with-defaults&revision=2011-06-01', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-PRODUCTS-MIB?module=CISCO-PRODUCTS-MIB&revision=2014-11-06', 'http://cisco.com/ns/cisco-xe-ietf-ospf-deviation?module=cisco-xe-ietf-ospf-deviation&revision=2015-09-11', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IMAGE-LICENSE-MGMT-MIB?module=CISCO-IMAGE-LICENSE-MGMT-MIB&revision=2007-10-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-FIREWALL-TC?module=CISCO-FIREWALL-TC&revision=2006-03-03', 'urn:ietf:params:xml:ns:yang:smiv2:IP-MIB?module=IP-MIB&revision=2006-02-02', 'http://cisco.com/ns/yang/Cisco-IOS-XE-virtual-service-oper?module=Cisco-IOS-XE-virtual-service-oper&revision=2017-09-25', 'urn:ietf:params:xml:ns:yang:cisco-policy-target?module=cisco-policy-target&revision=2016-03-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSLA-TC-MIB?module=CISCO-IPSLA-TC-MIB&revision=2007-03-23', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IF-EXTENSION-MIB?module=CISCO-IF-EXTENSION-MIB&revision=2013-03-13', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-FTP-CLIENT-MIB?module=CISCO-FTP-CLIENT-MIB&revision=2006-03-31', 'http://cisco.com/ns/yang/Cisco-IOS-XE-nbar?module=Cisco-IOS-XE-nbar&revision=2017-11-27', 'http://openconfig.net/yang/rib/bgp-ext?module=openconfig-rib-bgp-ext&revision=2016-04-11', 'urn:ietf:params:netconf:capability:interleave:1.0', 'http://cisco.com/ns/yang/Cisco-IOS-XE-process-cpu-oper?module=Cisco-IOS-XE-process-cpu-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPMROUTE-MIB?module=CISCO-IPMROUTE-MIB&revision=2005-03-07', 'urn:ietf:params:netconf:base:1.0', 'urn:ietf:params:xml:ns:yang:c3pl-types?module=policy-types&revision=2013-10-07&features=protocol-name-support,match-wlan-user-priority-support,match-vpls-support,match-vlan-support,match-vlan-inner-support,match-src-mac-support,match-security-group-support,match-qos-group-support,match-prec-support,match-packet-length-support,match-mpls-exp-top-support,match-mpls-exp-imp-support,match-metadata-support,match-ipv6-acl-support,match-ipv6-acl-name-support,match-ipv4-acl-support,match-ipv4-acl-name-support,match-ip-rtp-support,match-input-interface-support,match-fr-dlci-support,match-fr-de-support,match-flow-record-support,match-flow-ip-support,match-dst-mac-support,match-discard-class-support,match-dei-support,match-dei-inner-support,match-cos-support,match-cos-inner-support,match-class-map-support,match-atm-vci-support,match-atm-clp-support,match-application-support', 'http://cisco.com/ns/yang/Cisco-IOS-XE-tcam-oper?module=Cisco-IOS-XE-tcam-oper&revision=2017-06-06', 'http://cisco.com/ns/yang/Cisco-IOS-XE-eigrp?module=Cisco-IOS-XE-eigrp&revision=2017-09-21', 'http://cisco.com/ns/cisco-xe-ietf-ipv4-unicast-routing-deviation?module=cisco-xe-ietf-ipv4-unicast-routing-deviation&revision=2015-09-11', 'http://cisco.com/ns/yang/Cisco-IOS-XE-platform-software-oper?module=Cisco-IOS-XE-platform-software-oper&revision=2017-10-10', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ezpm?module=Cisco-IOS-XE-ezpm&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-service-discovery?module=Cisco-IOS-XE-service-discovery&revision=2017-02-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-rsvp?module=Cisco-IOS-XE-rsvp&revision=2017-11-27', 'http://cisco.com/yang/cisco-ia?module=cisco-ia&revision=2017-03-02', 'urn:ietf:params:xml:ns:yang:ietf-netconf-notifications?module=ietf-netconf-notifications&revision=2012-02-06', 'http://cisco.com/ns/yang/Cisco-IOS-XE-stackwise-virtual?module=Cisco-IOS-XE-stackwise-virtual&revision=2017-06-05', 'http://openconfig.net/yang/interfaces/ethernet?module=openconfig-if-ethernet&revision=2016-12-22&deviations=cisco-xe-openconfig-if-ethernet-deviation,cisco-xe-openconfig-interfaces-deviation', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-CEF-MIB?module=CISCO-CEF-MIB&revision=2006-01-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ETHER-CFM-MIB?module=CISCO-ETHER-CFM-MIB&revision=2004-12-28', 'http://cisco.com/ns/mpls-static/devs?module=common-mpls-static-devs&revision=2015-09-11', 'http://cisco.com/ns/yang/Cisco-IOS-XE-object-group?module=Cisco-IOS-XE-object-group&revision=2017-07-31', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-OSPF-TRAP-MIB?module=CISCO-OSPF-TRAP-MIB&revision=2003-07-18', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSLA-AUTOMEASURE-MIB?module=CISCO-IPSLA-AUTOMEASURE-MIB&revision=2007-06-13', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ios-events-oper?module=Cisco-IOS-XE-ios-events-oper&revision=2017-10-10', 'urn:ietf:params:xml:ns:yang:iana-if-type?module=iana-if-type&revision=2014-05-08', 'urn:ietf:params:xml:ns:yang:smiv2:RFC-1215?module=RFC-1215', 'http://openconfig.net/yang/interfaces?module=openconfig-interfaces&revision=2016-12-22&deviations=cisco-xe-openconfig-interfaces-deviation', 'http://cisco.com/ns/yang/Cisco-IOS-XE-wsma?module=Cisco-IOS-XE-wsma&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:INTEGRATED-SERVICES-MIB?module=INTEGRATED-SERVICES-MIB&revision=1995-11-03', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RADIUS-EXT-MIB?module=CISCO-RADIUS-EXT-MIB&revision=2010-05-25', 'urn:ietf:params:netconf:capability:validate:1.0', 'http://tail-f.com/ns/netconf/actions/1.0', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-PROCESS-MIB?module=CISCO-PROCESS-MIB&revision=2011-06-23', 'http://cisco.com/ns/yang/Cisco-IOS-XE-spanning-tree-oper?module=Cisco-IOS-XE-spanning-tree-oper&revision=2017-08-10', 'http://cisco.com/ns/yang/Cisco-IOS-XE-l2vpn?module=Cisco-IOS-XE-l2vpn&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cdp?module=Cisco-IOS-XE-cdp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ip-sla-oper?module=Cisco-IOS-XE-ip-sla-oper&revision=2017-09-25', 'http://openconfig.net/yang/acl?module=openconfig-acl&revision=2017-04-26&deviations=cisco-xe-openconfig-acl-deviation', 'http://openconfig.net/yang/spanning-tree/types?module=openconfig-spanning-tree-types&revision=2017-07-14', 'http://cisco.com/ns/cisco-xe-ietf-ipv6-unicast-routing-deviation?module=cisco-xe-ietf-ipv6-unicast-routing-deviation&revision=2015-09-11', 'urn:ietf:params:xml:ns:yang:smiv2:ENTITY-STATE-TC-MIB?module=ENTITY-STATE-TC-MIB&revision=2005-11-22', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mdt-oper?module=Cisco-IOS-XE-mdt-oper&revision=2017-09-20', 'http://tail-f.com/yang/common?module=tailf-common&revision=2017-08-23', 'http://cisco.com/ns/yang/Cisco-IOS-XE-rpc?module=Cisco-IOS-XE-rpc&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSEC-POLICY-MAP-MIB?module=CISCO-IPSEC-POLICY-MAP-MIB&revision=2000-08-17', 'urn:ietf:params:xml:ns:yang:policy-attr?module=policy-attr&revision=2015-04-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-dhcp-oper?module=Cisco-IOS-XE-dhcp-oper&revision=2017-11-01', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cef?module=Cisco-IOS-XE-cef&revision=2017-05-19&features=asr1k-dpi', 'urn:ietf:params:xml:ns:yang:smiv2:IGMP-STD-MIB?module=IGMP-STD-MIB&revision=2000-09-28', 'urn:ietf:params:xml:ns:yang:ietf-ip?module=ietf-ip&revision=2014-06-16&features=ipv6-privacy-autoconf,ipv4-non-contiguous-netmasks&deviations=cisco-xe-ietf-ip-deviation', 'http://openconfig.net/yang/platform-types?module=openconfig-platform-types&revision=2017-08-16', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lisp?module=Cisco-IOS-XE-lisp&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-NTP-MIB?module=CISCO-NTP-MIB&revision=2006-07-31', 'urn:ietf:params:xml:ns:yang:smiv2:PIM-MIB?module=PIM-MIB&revision=2000-09-28', 'urn:ietf:params:xml:ns:yang:smiv2:IANAifType-MIB?module=IANAifType-MIB&revision=2006-03-31', 'http://cisco.com/ns/yang/Cisco-IOS-XE-flow-monitor-oper?module=Cisco-IOS-XE-flow-monitor-oper&revision=2017-11-30', 'urn:ietf:params:xml:ns:yang:ietf-netconf-acm?module=ietf-netconf-acm&revision=2012-02-22', 'urn:ietf:params:xml:ns:yang:smiv2:NHRP-MIB?module=NHRP-MIB&revision=1999-08-26', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-BGP-POLICY-ACCOUNTING-MIB?module=CISCO-BGP-POLICY-ACCOUNTING-MIB&revision=2002-07-26', 'urn:ietf:params:xml:ns:yang:smiv2:SNMP-PROXY-MIB?module=SNMP-PROXY-MIB&revision=2002-10-14', 'http://openconfig.net/yang/header-fields?module=openconfig-packet-match&revision=2017-04-26', 'urn:cisco:params:xml:ns:yang:cisco-xe-ietf-yang-push-ext?module=cisco-xe-ietf-yang-push-ext&revision=2017-08-14', 'http://cisco.com/ns/yang/Cisco-IOS-XE-platform-oper?module=Cisco-IOS-XE-platform-oper&revision=2017-10-11', 'urn:jon?module=jon&revision=2023-03-29'] +'http://cisco.com/ns/cisco-xe-ietf-routing-deviation?module=cisco-xe-ietf-routing-deviation&revision=2016-07-09', 'http://cisco.com/ns/yang/ios-xe/template?module=Cisco-IOS-XE-template&revision=2017-11-06', 'http://openconfig.net/yang/cisco-xe-openconfig-if-ethernet-deviation?module=cisco-xe-openconfig-if-ethernet-deviation&revision=2017-11-01', 'http://cisco.com/ns/yang/Cisco-IOS-XE-track?module=Cisco-IOS-XE-track&revision=2017-04-28', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-EMBEDDED-EVENT-MGR-MIB?module=CISCO-EMBEDDED-EVENT-MGR-MIB&revision=2006-11-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-common-types?module=Cisco-IOS-XE-common-types&revision=2017-09-25', 'http://cisco.com/ns/yang/Cisco-IOS-XE-interfaces-oper?module=Cisco-IOS-XE-interfaces-oper&revision=2017-10-10', 'urn:ietf:params:xml:ns:yang:smiv2:Q-BRIDGE-MIB?module=Q-BRIDGE-MIB&revision=2006-01-09', 'http://cisco.com/ns/yang/Cisco-IOS-XE-wccp?module=Cisco-IOS-XE-wccp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lldp-oper?module=Cisco-IOS-XE-lldp-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:nvo?module=nvo&revision=2015-06-02&deviations=nvo-devs', 'http://openconfig.net/yang/interfaces/aggregate?module=openconfig-if-aggregate&revision=2016-12-22&deviations=cisco-xe-openconfig-interfaces-deviation', 'urn:ietf:params:xml:ns:yang:ietf-key-chain?module=ietf-key-chain&revision=2015-02-24&features=independent-send-accept-lifetime,hex-key-string,accept-tolerance', 'urn:ietf:params:xml:ns:yang:smiv2:ETHER-WIS?module=ETHER-WIS&revision=2003-09-19', 'urn:cisco:params:xml:ns:yang:pim?module=pim&revision=2014-06-27&features=bsr,auto-rp', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mld?module=Cisco-IOS-XE-mld&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:ENTITY-SENSOR-MIB?module=ENTITY-SENSOR-MIB&revision=2002-12-16', 'urn:ietf:params:xml:ns:yang:smiv2:DISMAN-EXPRESSION-MIB?module=DISMAN-EXPRESSION-MIB&revision=2000-10-16', 'http://openconfig.net/yang/types/yang?module=openconfig-yang-types&revision=2017-07-30', 'urn:ietf:params:xml:ns:yang:cisco-ospf?module=cisco-ospf&revision=2016-03-30&features=graceful-shutdown,flood-reduction,database-filter', 'http://openconfig.net/yang/bgp?module=openconfig-bgp&revision=2016-06-21', 'urn:ietf:params:xml:ns:yang:smiv2:TOKEN-RING-RMON-MIB?module=TOKEN-RING-RMON-MIB', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ENTITY-ALARM-MIB?module=CISCO-ENTITY-ALARM-MIB&revision=1999-07-06', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ethernet?module=Cisco-IOS-XE-ethernet&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-QOS-PIB-MIB?module=CISCO-QOS-PIB-MIB&revision=2007-08-29', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ospf?module=Cisco-IOS-XE-ospf&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:MPLS-VPN-MIB?module=MPLS-VPN-MIB&revision=2001-10-15', 'http://cisco.com/ns/yang/Cisco-IOS-XE-nat-oper?module=Cisco-IOS-XE-nat-oper&revision=2017-11-01', 'http://tail-f.com/ns/common/query?module=tailf-common-query&revision=2017-04-27', 'http://cisco.com/ns/yang/cisco-xe-openconfig-bgp-deviation?module=cisco-xe-openconfig-bgp-deviation&revision=2017-05-24', 'http://openconfig.net/yang/bgp-types?module=openconfig-bgp-types&revision=2016-06-21', 'http://openconfig.net/yang/policy-types?module=openconfig-policy-types&revision=2016-05-12', 'http://tail-f.com/ns/netconf/extensions', 'http://openconfig.net/yang/interfaces/ip-ext?module=openconfig-if-ip-ext&revision=2016-12-22', 'urn:ietf:params:xml:ns:yang:smiv2:LLDP-MIB?module=LLDP-MIB&revision=2005-05-06', 'http://cisco.com/ns/yang/cisco-xe-ietf-yang-push-deviation?module=cisco-xe-ietf-yang-push-deviation&revision=2017-08-22', 'urn:ietf:params:xml:ns:yang:smiv2:TUNNEL-MIB?module=TUNNEL-MIB&revision=2005-05-16', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ntp?module=Cisco-IOS-XE-ntp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mdt-common-defs?module=Cisco-IOS-XE-mdt-common-defs&revision=2017-07-01', 'urn:ietf:params:xml:ns:yang:ietf-yang-push?module=ietf-yang-push&revision=2016-10-28&features=on-change&deviations=cisco-xe-ietf-yang-push-deviation', 'http://openconfig.net/yang/transport-line-common?module=openconfig-transport-line-common&revision=2016-03-31', 'urn:ietf:params:xml:ns:yang:smiv2:OSPF-MIB?module=OSPF-MIB&revision=2006-11-10', 'http://cisco.com/ns/yang/cisco-xe-openconfig-rib-bgp-ext?module=cisco-xe-openconfig-rib-bgp-ext&revision=2016-11-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RTTMON-MIB?module=CISCO-RTTMON-MIB&revision=2012-08-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-HSRP-MIB?module=CISCO-HSRP-MIB&revision=2010-09-06', 'http://openconfig.net/yang/types/inet?module=openconfig-inet-types&revision=2017-08-24', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lldp?module=Cisco-IOS-XE-lldp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cts?module=Cisco-IOS-XE-cts&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-snmp?module=Cisco-IOS-XE-snmp&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults?module=ietf-netconf-with-defaults&revision=2011-06-01', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-PRODUCTS-MIB?module=CISCO-PRODUCTS-MIB&revision=2014-11-06', 'http://cisco.com/ns/cisco-xe-ietf-ospf-deviation?module=cisco-xe-ietf-ospf-deviation&revision=2015-09-11', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IMAGE-LICENSE-MGMT-MIB?module=CISCO-IMAGE-LICENSE-MGMT-MIB&revision=2007-10-16', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-FIREWALL-TC?module=CISCO-FIREWALL-TC&revision=2006-03-03', 'urn:ietf:params:xml:ns:yang:smiv2:IP-MIB?module=IP-MIB&revision=2006-02-02', 'http://cisco.com/ns/yang/Cisco-IOS-XE-virtual-service-oper?module=Cisco-IOS-XE-virtual-service-oper&revision=2017-09-25', 'urn:ietf:params:xml:ns:yang:cisco-policy-target?module=cisco-policy-target&revision=2016-03-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSLA-TC-MIB?module=CISCO-IPSLA-TC-MIB&revision=2007-03-23', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IF-EXTENSION-MIB?module=CISCO-IF-EXTENSION-MIB&revision=2013-03-13', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-FTP-CLIENT-MIB?module=CISCO-FTP-CLIENT-MIB&revision=2006-03-31', 'http://cisco.com/ns/yang/Cisco-IOS-XE-nbar?module=Cisco-IOS-XE-nbar&revision=2017-11-27', 'http://openconfig.net/yang/rib/bgp-ext?module=openconfig-rib-bgp-ext&revision=2016-04-11', 'urn:ietf:params:netconf:capability:interleave:1.0', 'http://cisco.com/ns/yang/Cisco-IOS-XE-process-cpu-oper?module=Cisco-IOS-XE-process-cpu-oper&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPMROUTE-MIB?module=CISCO-IPMROUTE-MIB&revision=2005-03-07', 'urn:ietf:params:netconf:base:1.0', 'urn:ietf:params:xml:ns:yang:c3pl-types?module=policy-types&revision=2013-10-07&features=protocol-name-support,match-wlan-user-priority-support,match-vpls-support,match-vlan-support,match-vlan-inner-support,match-src-mac-support,match-security-group-support,match-qos-group-support,match-prec-support,match-packet-length-support,match-mpls-exp-top-support,match-mpls-exp-imp-support,match-metadata-support,match-ipv6-acl-support,match-ipv6-acl-name-support,match-ipv4-acl-support,match-ipv4-acl-name-support,match-ip-rtp-support,match-input-interface-support,match-fr-dlci-support,match-fr-de-support,match-flow-record-support,match-flow-ip-support,match-dst-mac-support,match-discard-class-support,match-dei-support,match-dei-inner-support,match-cos-support,match-cos-inner-support,match-class-map-support,match-atm-vci-support,match-atm-clp-support,match-application-support', 'http://cisco.com/ns/yang/Cisco-IOS-XE-eigrp?module=Cisco-IOS-XE-eigrp&revision=2017-09-21', 'http://cisco.com/ns/cisco-xe-ietf-ipv4-unicast-routing-deviation?module=cisco-xe-ietf-ipv4-unicast-routing-deviation&revision=2015-09-11', 'http://cisco.com/ns/yang/Cisco-IOS-XE-platform-software-oper?module=Cisco-IOS-XE-platform-software-oper&revision=2017-10-10', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ezpm?module=Cisco-IOS-XE-ezpm&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-service-discovery?module=Cisco-IOS-XE-service-discovery&revision=2017-02-07', 'http://cisco.com/ns/yang/Cisco-IOS-XE-rsvp?module=Cisco-IOS-XE-rsvp&revision=2017-11-27', 'http://cisco.com/yang/cisco-ia?module=cisco-ia&revision=2017-03-02', 'urn:ietf:params:xml:ns:yang:ietf-netconf-notifications?module=ietf-netconf-notifications&revision=2012-02-06', 'http://openconfig.net/yang/interfaces/ethernet?module=openconfig-if-ethernet&revision=2016-12-22&deviations=cisco-xe-openconfig-if-ethernet-deviation,cisco-xe-openconfig-interfaces-deviation', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-CEF-MIB?module=CISCO-CEF-MIB&revision=2006-01-30', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-ETHER-CFM-MIB?module=CISCO-ETHER-CFM-MIB&revision=2004-12-28', 'http://cisco.com/ns/mpls-static/devs?module=common-mpls-static-devs&revision=2015-09-11', 'http://cisco.com/ns/yang/Cisco-IOS-XE-object-group?module=Cisco-IOS-XE-object-group&revision=2017-07-31', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-OSPF-TRAP-MIB?module=CISCO-OSPF-TRAP-MIB&revision=2003-07-18', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSLA-AUTOMEASURE-MIB?module=CISCO-IPSLA-AUTOMEASURE-MIB&revision=2007-06-13', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ios-events-oper?module=Cisco-IOS-XE-ios-events-oper&revision=2017-10-10', 'urn:ietf:params:xml:ns:yang:iana-if-type?module=iana-if-type&revision=2014-05-08', 'http://openconfig.net/yang/interfaces?module=openconfig-interfaces&revision=2016-12-22&deviations=cisco-xe-openconfig-interfaces-deviation', 'http://cisco.com/ns/yang/Cisco-IOS-XE-wsma?module=Cisco-IOS-XE-wsma&revision=2017-02-07', 'urn:ietf:params:xml:ns:yang:smiv2:INTEGRATED-SERVICES-MIB?module=INTEGRATED-SERVICES-MIB&revision=1995-11-03', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-RADIUS-EXT-MIB?module=CISCO-RADIUS-EXT-MIB&revision=2010-05-25', 'urn:ietf:params:netconf:capability:validate:1.0', 'http://tail-f.com/ns/netconf/actions/1.0', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-PROCESS-MIB?module=CISCO-PROCESS-MIB&revision=2011-06-23', 'http://cisco.com/ns/yang/Cisco-IOS-XE-l2vpn?module=Cisco-IOS-XE-l2vpn&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cdp?module=Cisco-IOS-XE-cdp&revision=2017-11-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-ip-sla-oper?module=Cisco-IOS-XE-ip-sla-oper&revision=2017-09-25', 'http://openconfig.net/yang/acl?module=openconfig-acl&revision=2017-04-26&deviations=cisco-xe-openconfig-acl-deviation', 'http://cisco.com/ns/cisco-xe-ietf-ipv6-unicast-routing-deviation?module=cisco-xe-ietf-ipv6-unicast-routing-deviation&revision=2015-09-11', 'urn:ietf:params:xml:ns:yang:smiv2:ENTITY-STATE-TC-MIB?module=ENTITY-STATE-TC-MIB&revision=2005-11-22', 'http://cisco.com/ns/yang/Cisco-IOS-XE-mdt-oper?module=Cisco-IOS-XE-mdt-oper&revision=2017-09-20', 'http://tail-f.com/yang/common?module=tailf-common&revision=2017-08-23', 'http://cisco.com/ns/yang/Cisco-IOS-XE-rpc?module=Cisco-IOS-XE-rpc&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-IPSEC-POLICY-MAP-MIB?module=CISCO-IPSEC-POLICY-MAP-MIB&revision=2000-08-17', 'urn:ietf:params:xml:ns:yang:policy-attr?module=policy-attr&revision=2015-04-27', 'http://cisco.com/ns/yang/Cisco-IOS-XE-dhcp-oper?module=Cisco-IOS-XE-dhcp-oper&revision=2017-11-01', 'http://cisco.com/ns/yang/Cisco-IOS-XE-cef?module=Cisco-IOS-XE-cef&revision=2017-05-19&features=asr1k-dpi', 'urn:ietf:params:xml:ns:yang:smiv2:IGMP-STD-MIB?module=IGMP-STD-MIB&revision=2000-09-28', 'urn:ietf:params:xml:ns:yang:ietf-ip?module=ietf-ip&revision=2014-06-16&features=ipv6-privacy-autoconf,ipv4-non-contiguous-netmasks&deviations=cisco-xe-ietf-ip-deviation', 'http://openconfig.net/yang/platform-types?module=openconfig-platform-types&revision=2017-08-16', 'http://cisco.com/ns/yang/Cisco-IOS-XE-lisp?module=Cisco-IOS-XE-lisp&revision=2017-11-27', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-NTP-MIB?module=CISCO-NTP-MIB&revision=2006-07-31', 'urn:ietf:params:xml:ns:yang:smiv2:PIM-MIB?module=PIM-MIB&revision=2000-09-28', 'urn:ietf:params:xml:ns:yang:smiv2:IANAifType-MIB?module=IANAifType-MIB&revision=2006-03-31', 'http://cisco.com/ns/yang/Cisco-IOS-XE-flow-monitor-oper?module=Cisco-IOS-XE-flow-monitor-oper&revision=2017-11-30', 'urn:ietf:params:xml:ns:yang:ietf-netconf-acm?module=ietf-netconf-acm&revision=2012-02-22', 'urn:ietf:params:xml:ns:yang:smiv2:NHRP-MIB?module=NHRP-MIB&revision=1999-08-26', 'urn:ietf:params:xml:ns:yang:smiv2:CISCO-BGP-POLICY-ACCOUNTING-MIB?module=CISCO-BGP-POLICY-ACCOUNTING-MIB&revision=2002-07-26', 'urn:ietf:params:xml:ns:yang:smiv2:SNMP-PROXY-MIB?module=SNMP-PROXY-MIB&revision=2002-10-14', 'http://openconfig.net/yang/header-fields?module=openconfig-packet-match&revision=2017-04-26', 'urn:cisco:params:xml:ns:yang:cisco-xe-ietf-yang-push-ext?module=cisco-xe-ietf-yang-push-ext&revision=2017-08-14', 'http://cisco.com/ns/yang/Cisco-IOS-XE-platform-oper?module=Cisco-IOS-XE-platform-oper&revision=2017-10-11', 'urn:jon?module=jon&revision=2023-03-29'] + yaml = \ 'devices:\n' \ ' dummy:\n' \ @@ -1326,6 +1364,1183 @@ def test_delta_10(self): self.assertEqual(str(delta1).strip(), expected_delta1.strip()) self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + def test_delta_replace_1(self): + config_xml1 = """ + + + abc + + true + +
+ Brown + Bob + Innovation + New York +
+
+ Paul + Bob + Express + Kanata +
+
+ Wang + Ken + Main + Boston +
+ Dollar + Cheap + Quick +
+
+ """ + config_xml2 = """ + + + edf + + false + +
+ Brown + Bob + Carp + New York +
+
+ Wang + Ken + Second + Boston +
+ Dollar + Cheap +
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + + # leaf foo + foo = delta.nc.xpath('//nc:config/jon:foo', + namespaces=delta.ns)[0] + self.assertEqual(foo.get(operation_tag), 'replace') + self.assertEqual(foo.text, 'edf') + + # container tracking + tracking = delta.nc.xpath('//nc:config/jon:tracking', + namespaces=delta.ns)[0] + self.assertEqual(tracking.get(operation_tag), 'replace') + enabled = delta.nc.xpath('//nc:config/jon:tracking/jon:enabled', + namespaces=delta.ns)[0] + self.assertEqual(enabled.text, 'false') + + # list address + address = delta.nc.xpath('//nc:config/jon:address', + namespaces=delta.ns) + self.assertEqual(address[0].get(operation_tag), 'replace') + self.assertEqual(address[0].get(insert_tag), 'first') + street = address[0].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.text, 'Carp') + self.assertEqual(address[1].get(operation_tag), 'replace') + self.assertEqual(address[1].get(insert_tag), 'after') + self.assertEqual(address[1].get(key_tag), + "[first='Bob'][last='Brown']") + street = address[1].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.text, 'Second') + self.assertEqual(address[2].get(operation_tag), 'delete') + first = address[2].xpath('jon:first', + namespaces=delta.ns)[0] + self.assertEqual(first.text, 'Bob') + last = address[2].xpath('jon:last', + namespaces=delta.ns)[0] + self.assertEqual(last.text, 'Paul') + + # leaf-list store + store = delta.nc.xpath('//nc:config/jon:store', namespaces=delta.ns) + self.assertEqual(store[0].get(operation_tag), 'replace') + self.assertEqual(store[0].get(insert_tag), 'first') + self.assertEqual(store[0].text, 'Dollar') + self.assertEqual(store[1].get(operation_tag), 'replace') + self.assertEqual(store[1].get(insert_tag), 'after') + self.assertEqual(store[1].get(value_tag), 'Dollar') + self.assertEqual(store[1].text, 'Cheap') + self.assertEqual(store[2].get(operation_tag), 'delete') + self.assertEqual(store[2].text, 'Quick') + + config3 = config1 + delta + self.assertEqual(config2, config3) + + def test_delta_2(self): + xml1 = """ + + + + + + 10 + + 10.8.55.30 + + + + + + + + """ + xml2 = """ + + + + + + 10 + + 10.8.55.30 + + + 2100 + + 10.44.0.0/16 + INET1-SPOKES + + + + + + + unicast + + + + + + + + + """ + expected_delta1 = """ + + + + + 10 + + + 2100 + + 10.44.0.0/16 + INET1-SPOKES + + + + + + + unicast + + + + + + + + """ + expected_delta2 = """ + + + + + 10 + + + + + + + + + """ + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + delta1 = config2 - config1 + delta2 = -delta1 + self.assertEqual(str(delta1).strip(), expected_delta1.strip()) + self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + + def test_delta_3(self): + config_xml1 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + oc-pol-types:BGP + ROUTEMAP1 + ROUTEMAP2 + REJECT_ROUTE + + + + + + + + """ + config_xml2 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + oc-pol-types:BGP + ROUTEMAP1 + ROUTEMAP3 + ROUTEMAP0 + ROUTEMAP2 + REJECT_ROUTE + + + + + + + + """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + # modify schema node + nodes = config1.xpath('.//oc-netinst:network-instance' + '/oc-netinst:table-connections' + '/oc-netinst:table-connection' + '/oc-netinst:config/oc-netinst:import-policy') + node = nodes[0] + schema_node = config1.get_schema_node(node) + schema_node.set('ordered-by', 'user') + delta1 = config2 - config1 + config3 = config1 + delta1 + self.assertEqual(config2, config3) + self.assertTrue(config2 <= config3) + self.assertTrue(config2 >= config3) + delta2 = config1 - config2 + config4 = config2 + delta2 + self.assertEqual(config1, config4) + + def test_delta_4(self): + config_xml1 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + +
+ + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + +
+ + oc-pol-types:STATIC + oc-types:IPV4 + + oc-pol-types:STATIC + oc-types:IPV4 + +
+ + oc-pol-types:STATIC + oc-types:IPV6 + + oc-pol-types:STATIC + oc-types:IPV6 + +
+
+
+
+
+
+ """ + config_xml2 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + +
+ + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + +
+ + oc-pol-types:STATIC + oc-types:IPV4 + + oc-pol-types:STATIC + oc-types:IPV4 + +
+ + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:BGP + oc-types:IPV4 + +
+ + oc-pol-types:STATIC + oc-types:IPV6 + + oc-pol-types:STATIC + oc-types:IPV6 + +
+
+
+
+
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + # modify schema node + nodes = config1.xpath('.//oc-netinst:network-instance' + '/oc-netinst:tables/oc-netinst:table') + node = nodes[0] + schema_node = config1.get_schema_node(node) + schema_node.set('ordered-by', 'user') + delta1 = config2 - config1 + config3 = config1 + delta1 + self.assertEqual(config2, config3) + self.assertTrue(config2 <= config3) + self.assertTrue(config2 >= config3) + delta2 = config1 - config2 + config4 = config2 + delta2 + self.assertEqual(config1, config4) + + def test_delta_5(self): + config_xml1 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + +
+ + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV6 + +
+ + oc-pol-types:STATIC + oc-types:IPV4 + + oc-pol-types:STATIC + oc-types:IPV4 + +
+ + oc-pol-types:STATIC + oc-types:IPV6 + + oc-pol-types:STATIC + oc-types:IPV6 + +
+
+
+
+
+
+ """ + config_xml2 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV44 + +
+ + oc-pol-types:STATIC + oc-types:IPV4 + + oc-pol-types:STATIC + oc-types:IPV4 + +
+ + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:BGP + oc-types:IPV4 + +
+ + oc-pol-types:STATIC + oc-types:IPV6 + + oc-pol-types:STATIC + oc-types:IPV6 + +
+
+
+
+
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + # modify schema node + nodes = config1.xpath('.//oc-netinst:network-instance' + '/oc-netinst:tables/oc-netinst:table') + node = nodes[0] + schema_node = config1.get_schema_node(node) + schema_node.set('ordered-by', 'user') + delta1 = ConfigDelta(config_src=config1, config_dst=config2, + preferred_create='create', + preferred_replace='replace', + preferred_delete='remove') + config3 = config1 + delta1 + self.assertEqual(config2, config3) + self.assertTrue(config2 <= config3) + self.assertTrue(config2 >= config3) + delta2 = config1 - config2 + config4 = config2 + delta2 + self.assertEqual(config1, config4) + + def test_delta_6(self): + config_xml1 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + oc-pol-types:BGP + ROUTEMAP1 + ROUTEMAP2 + REJECT_ROUTE + + + + + + + + """ + config_xml2 = """ + + + + + default + + default + oc-ni-types:DEFAULT_INSTANCE + default-vrf [read-only] + + + + oc-pol-types:DIRECTLY_CONNECTED + oc-pol-types:BGP + oc-types:IPV4 + + oc-pol-types:DIRECTLY_CONNECTED + oc-types:IPV4 + oc-pol-types:BGP + ROUTEMAP1 + ROUTEMAP3 + ROUTEMAP0 + ROUTEMAP2 + REJECT_ROUTE + + + + + + + + """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + + # modify schema node + nodes = config1.xpath('.//oc-netinst:network-instance' + '/oc-netinst:table-connections' + '/oc-netinst:table-connection' + '/oc-netinst:config/oc-netinst:import-policy') + node = nodes[0] + schema_node = config1.get_schema_node(node) + schema_node.set('ordered-by', 'user') + + delta1 = ConfigDelta(config_src=config1, config_dst=config2, + preferred_create='create', + preferred_replace='replace', + preferred_delete='remove') + config3 = config1 + delta1 + self.assertEqual(config2, config3) + self.assertTrue(config2 <= config3) + self.assertTrue(config2 >= config3) + delta2 = config1 - config2 + config4 = config2 + delta2 + self.assertEqual(config1, config4) + + # diff_type is 'replace' + delta3 = -delta2 + delta3.diff_type = 'replace' + config5 = config1 + delta3 + self.assertEqual(config2, config5) + + def test_delta_replace_1(self): + config_xml1 = """ + + + abc + + true + +
+ Brown + Bob + Innovation + New York +
+
+ Paul + Bob + Express + Kanata +
+
+ Wang + Ken + Main + Boston +
+ Dollar + Cheap + Quick +
+
+ """ + config_xml2 = """ + + + edf + + false + +
+ Brown + Bob + Carp + New York +
+
+ Wang + Ken + Second + Boston +
+ Dollar + Cheap +
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + + # leaf foo + foo = delta.nc.xpath('//nc:config/jon:foo', + namespaces=delta.ns)[0] + self.assertEqual(foo.get(operation_tag), 'replace') + self.assertEqual(foo.text, 'edf') + + # container tracking + tracking = delta.nc.xpath('//nc:config/jon:tracking', + namespaces=delta.ns)[0] + self.assertEqual(tracking.get(operation_tag), 'replace') + enabled = delta.nc.xpath('//nc:config/jon:tracking/jon:enabled', + namespaces=delta.ns)[0] + self.assertEqual(enabled.text, 'false') + + # list address + address = delta.nc.xpath('//nc:config/jon:address', + namespaces=delta.ns) + self.assertEqual(address[0].get(operation_tag), 'replace') + self.assertEqual(address[0].get(insert_tag), 'first') + street = address[0].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.text, 'Carp') + self.assertEqual(address[1].get(operation_tag), 'replace') + self.assertEqual(address[1].get(insert_tag), 'after') + self.assertEqual(address[1].get(key_tag), + "[first='Bob'][last='Brown']") + street = address[1].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.text, 'Second') + self.assertEqual(address[2].get(operation_tag), 'delete') + first = address[2].xpath('jon:first', + namespaces=delta.ns)[0] + self.assertEqual(first.text, 'Bob') + last = address[2].xpath('jon:last', + namespaces=delta.ns)[0] + self.assertEqual(last.text, 'Paul') + + # leaf-list store + store = delta.nc.xpath('//nc:config/jon:store', namespaces=delta.ns) + self.assertEqual(store[0].get(operation_tag), 'replace') + self.assertEqual(store[0].get(insert_tag), 'first') + self.assertEqual(store[0].text, 'Dollar') + self.assertEqual(store[1].get(operation_tag), 'replace') + self.assertEqual(store[1].get(insert_tag), 'after') + self.assertEqual(store[1].get(value_tag), 'Dollar') + self.assertEqual(store[1].text, 'Cheap') + self.assertEqual(store[2].get(operation_tag), 'delete') + self.assertEqual(store[2].text, 'Quick') + + config3 = config1 + delta + self.assertEqual(config2, config3) + + def test_delta_replace_2(self): + config_xml1 = """ + + + abc + + true + +
+ Brown + Bob + Innovation + New York +
+
+ Paul + Bob + Express + Kanata +
+
+ Wang + Ken + Main + Boston +
+ Dollar + Cheap + Quick +
+
+ """ + config_xml2 = """ + + + edf + + false + +
+ Brown + Bob + Carp + New York +
+
+ Wang + Ken + Second + Boston +
+ Dollar + Cheap +
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + delta.replace_depth = 1 + + # leaf foo + foo = delta.nc.xpath('//nc:config/jon:foo', + namespaces=delta.ns)[0] + self.assertEqual(foo.get(operation_tag), None) + self.assertEqual(foo.text, 'edf') + + # container tracking + tracking = delta.nc.xpath('//nc:config/jon:tracking', + namespaces=delta.ns)[0] + enabled = delta.nc.xpath('//nc:config/jon:tracking/jon:enabled', + namespaces=delta.ns)[0] + self.assertEqual(enabled.text, 'false') + self.assertEqual(enabled.get(operation_tag), 'replace') + + # list address + address = delta.nc.xpath('//nc:config/jon:address', + namespaces=delta.ns) + self.assertEqual(address[0].get(operation_tag), None) + self.assertEqual(address[0].get(insert_tag), 'first') + street = address[0].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.get(operation_tag), 'replace') + self.assertEqual(street.text, 'Carp') + self.assertEqual(address[1].get(operation_tag), None) + self.assertEqual(address[1].get(insert_tag), 'after') + self.assertEqual(address[1].get(key_tag), + "[first='Bob'][last='Brown']") + street = address[1].xpath('jon:street', + namespaces=delta.ns)[0] + self.assertEqual(street.get(operation_tag), 'replace') + self.assertEqual(street.text, 'Second') + self.assertEqual(address[2].get(operation_tag), 'delete') + first = address[2].xpath('jon:first', + namespaces=delta.ns)[0] + self.assertEqual(first.text, 'Bob') + last = address[2].xpath('jon:last', + namespaces=delta.ns)[0] + self.assertEqual(last.text, 'Paul') + + # leaf-list store + store = delta.nc.xpath('//nc:config/jon:store', namespaces=delta.ns) + self.assertEqual(store[0].get(operation_tag), None) + self.assertEqual(store[0].get(insert_tag), 'first') + self.assertEqual(store[0].text, 'Dollar') + self.assertEqual(store[1].get(operation_tag), None) + self.assertEqual(store[1].get(insert_tag), 'after') + self.assertEqual(store[1].get(value_tag), 'Dollar') + self.assertEqual(store[1].text, 'Cheap') + self.assertEqual(store[2].get(operation_tag), 'delete') + self.assertEqual(store[2].text, 'Quick') + + config3 = config1 + delta + self.assertEqual(config2, config3) + + def test_delta_replace_3(self): + xml1 = """ + + + + + + 10 + + 10.8.55.30 + + + + + + + Mgmt-vrf + + + + + + + + + + """ + xml2 = """ + + + + + + 10 + + 10.8.55.30 + + + 2100 + + 10.44.0.0/16 + INET1-SPOKES + + + + + + + unicast + + + + + + + + Mgmt-vrf + + + + + + + + + + """ + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + delta.replace_depth = 1 + + # container tracking + router = delta.nc.xpath('//nc:config/ios:native/ios:router', + namespaces=delta.ns)[0] + self.assertEqual(router.get(operation_tag), 'replace') + + # container vrf should not exist + vrf = delta.nc.xpath('//nc:config/ios:native/ios:vrf', + namespaces=delta.ns) + self.assertEqual(vrf, []) + + def test_delta_replace_4(self): + config_xml1 = """ + + + + + + 1 + +
+ +
10.77.130.126
+ 255.255.255.0 +
+
+
+ + true + +
+ + 2 + + + true + + + + 3 + + + true + + + + 2 + +
+
+
+
+ """ + config_xml2 = """ + + + + + + 1 + +
+ +
10.77.130.126
+ 255.255.255.0 +
+
+
+ + true + +
+ + 2 + + + true + + + + 3 + + + true + + + + 2 + inside + + 1.1.1.1 + + +
+
+
+
+ """ + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + delta.replace_depth = 3 + + # leaf nat66 + nat66 = delta.nc.xpath( + '//nc:config/ios:native/ios:interface/ios:Tunnel/ios-nat:nat66', + namespaces=delta.ns, + )[0] + self.assertEqual(nat66.get(operation_tag), 'replace') + + # container tunnel + tunnel = delta.nc.xpath( + '//nc:config/ios:native/ios:interface/ios:Tunnel/ios-tun:tunnel', + namespaces=delta.ns + )[0] + self.assertEqual(tunnel.get(operation_tag), 'replace') + + # self.assertEqual(str(delta), '') + + def test_delta_replace_5(self): + config_xml1 = """ + + + + + + 1 + +
+ +
10.77.130.126
+ 255.255.255.0 +
+
+
+ + true + +
+ + 2 + + + true + + + + 3 + + + true + + + + 2 + + 1.1.1.1 + + +
+
+
+
+ """ + config_xml2 = """ + + + + + + 1 + +
+ +
10.77.130.126
+ 255.255.255.0 +
+
+
+ + true + +
+ + 2 + + + true + + + + 3 + + + true + + + + 2 + inside + + + 2.2.2.2 + + + +
+
+
+
+ """ + + config1 = Config(self.d, config_xml1) + config2 = Config(self.d, config_xml2) + delta = config2 - config1 + delta.diff_type = 'replace' + delta.replace_depth = 4 + + # leaf nat66 is at level 3, but it is a leaf, so the replace operation + # has to be at level 3. + nat66 = delta.nc.xpath( + '//nc:config/ios:native/ios:interface/ios:Tunnel/ios-nat:nat66', + namespaces=delta.ns, + )[0] + self.assertEqual(nat66.get(operation_tag), 'replace') + + # container destination + destination = delta.nc.xpath( + '//nc:config/ios:native/ios:interface/ios:Tunnel/ios-tun:tunnel' + '/ios-tun:destination', + namespaces=delta.ns, + )[0] + self.assertEqual(destination.get(operation_tag), 'replace') + def test_xpath_1(self): xml = """ Date: Sat, 2 Dec 2023 19:28:33 +0000 Subject: [PATCH 11/20] PR#26 Flow record and some of its child commands are orderless --- ncdiff/src/yang/ncdiff/runningconfig.py | 4 ++ .../yang/ncdiff/tests/test_running_config.py | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/ncdiff/src/yang/ncdiff/runningconfig.py b/ncdiff/src/yang/ncdiff/runningconfig.py index 5e09c09..5178420 100755 --- a/ncdiff/src/yang/ncdiff/runningconfig.py +++ b/ncdiff/src/yang/ncdiff/runningconfig.py @@ -50,6 +50,10 @@ (re.compile(r'^ *flow exporter '), 0), (re.compile(r'^ *exporter '), 1), (re.compile(r'^ *username '), 0), + (re.compile(r'^ *flow record '), 0), + (re.compile(r'^ *match ipv4 '), 1), + (re.compile(r'^ *match ipv6 '), 1), + (re.compile(r'^ *collect connection '), 1), ] # Some commands can be overwritten without a no command. For example, changing diff --git a/ncdiff/src/yang/ncdiff/tests/test_running_config.py b/ncdiff/src/yang/ncdiff/tests/test_running_config.py index b286a4e..43ed033 100644 --- a/ncdiff/src/yang/ncdiff/tests/test_running_config.py +++ b/ncdiff/src/yang/ncdiff/tests/test_running_config.py @@ -1037,6 +1037,53 @@ def test_username(self): username priv01 password 7 1209171E045B5D username guestshell privilege 0 password 7 03034E0E151B32444B0515 """ + running_diff = RunningConfigDiff( + running1=config_1, + running2=config_2, + ) + self.assertFalse(running_diff) + self.assertEqual(running_diff.diff, None) + self.assertEqual(running_diff.diff_reverse, None) + self.assertEqual(running_diff.cli, '') + self.assertEqual(running_diff.cli_reverse, '') + + def test_flow_record(self): + config_1 = """ +flow record fr_1 + match ipv6 protocol +flow record fr_ipv4 + match ipv4 protocol + match ipv4 version + collect connection initiator + collect connection new-connections + collect connection server counter bytes network long + collect connection server counter packets long +flow record fr_ipv6 + match ipv6 protocol + match ipv6 version + collect connection initiator + collect connection new-connections + collect connection server counter bytes network long + collect connection server counter packets long +""" + config_2 = """ +flow record fr_ipv4 + match ipv4 version + match ipv4 protocol + collect connection server counter bytes network long + collect connection server counter packets long + collect connection initiator + collect connection new-connections +flow record fr_ipv6 + match ipv6 version + match ipv6 protocol + collect connection server counter bytes network long + collect connection server counter packets long + collect connection initiator + collect connection new-connections +flow record fr_1 + match ipv6 protocol +""" running_diff = RunningConfigDiff( running1=config_1, running2=config_2, From c04f5c05720d4e5e7c2398e86fdd02afce66b469 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sat, 2 Dec 2023 20:33:42 +0000 Subject: [PATCH 12/20] PR#27 Add new choice logic and unittests --- ncdiff/src/yang/ncdiff/device.py | 27 +- ncdiff/src/yang/ncdiff/netconf.py | 18 +- ncdiff/src/yang/ncdiff/tests/test_ncdiff.py | 349 +++++ .../tests/yang/Cisco-IOS-XE-tunnel.yang | 1165 ++++++++++++++++- ncdiff/src/yang/ncdiff/tests/yang/jon.yang | 2 +- 5 files changed, 1493 insertions(+), 68 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/device.py b/ncdiff/src/yang/ncdiff/device.py index 4fe6ffa..90609c4 100755 --- a/ncdiff/src/yang/ncdiff/device.py +++ b/ncdiff/src/yang/ncdiff/device.py @@ -571,7 +571,7 @@ def get_schema_node(self, config_node): ------- Element - A schema node, or None when nothing can be found. + A schema node. Raises ------ @@ -579,12 +579,15 @@ def get_schema_node(self, config_node): ModelError If identifier is not unique in a namespace. + ConfigError + when nothing can be found. + Code Example:: - >>> device.nc.load_model('openconfig-interfaces') - >>> reply = device.nc.get_config(models='openconfig-interfaces') - >>> config = device.nc.extract_config(reply) + >>> m.load_model('openconfig-interfaces') + >>> reply = m.get_config(models='openconfig-interfaces') + >>> config = m.extract_config(reply) >>> print(config) ... >>> config.ns @@ -592,25 +595,25 @@ def get_schema_node(self, config_node): >>> config_nodes = config.xpath('/nc:config/oc-if:interfaces/oc-if:interface[oc-if:name="GigabitEthernet0/0"]') >>> config_node = config_nodes[0] >>> - >>> device.nc.get_schema_node(config_node) + >>> m.get_schema_node(config_node) >>> ''' def get_child(parent, tag): - children = [i for i in parent.iter(tag=tag) - if i.attrib['type'] != 'choice' and - i.attrib['type'] != 'case' and - is_parent(parent, i)] + children = [i for i in parent.iter(tag=tag) \ + if i.attrib['type'] != 'choice' and \ + i.attrib['type'] != 'case' and \ + is_parent(parent, i)] if len(children) == 1: return children[0] elif len(children) > 1: if parent.getparent() is None: - raise ModelError("more than one root has tag '{}'" + raise ModelError("more than one root has tag '{}'" \ .format(tag)) else: - raise ModelError("node {} has more than one child with " - "tag '{}'" + raise ModelError("node {} has more than one child with " \ + "tag '{}'" \ .format(self.get_xpath(parent), tag)) else: return None diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index c3918bd..191b80c 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -939,6 +939,7 @@ def node_sub(self, node_self, node_other, depth=0): in_s_not_in_o, in_o_not_in_s, in_s_and_in_o = \ self._group_kids(node_self, node_other) ordered_by_user = {} + choice_nodes = {} for child_self in in_s_not_in_o: child_other = etree.Element(child_self.tag, {operation_tag: self.preferred_delete}, @@ -968,6 +969,9 @@ def node_sub(self, node_self, node_other, depth=0): e = etree.SubElement( child_other, key, nsmap=key_node.nsmap) e.text = key_node.text + if s_node.getparent().get('type') == 'case': + # key: choice node, value: case node + choice_nodes[s_node.getparent().getparent()] = s_node.getparent() for child_other in in_o_not_in_s: child_self = etree.Element(child_other.tag, {operation_tag: self.preferred_delete}, @@ -975,11 +979,21 @@ def node_sub(self, node_self, node_other, depth=0): if self.preferred_create != 'merge': child_other.set(operation_tag, self.preferred_create) siblings = list(node_self.iterchildren(tag=child_other.tag)) + s_node = self.device.get_schema_node(child_other) if siblings: siblings[-1].addnext(child_self) else: - node_self.append(child_self) - s_node = self.device.get_schema_node(child_other) + # Append node if: + # Node not in case + # Node in case but choice node not in self + # Node in case and choice node in self and the same case also in self + if s_node.getparent().get('type') == 'case': + choice_node = s_node.getparent().getparent() + if choice_node not in choice_nodes or \ + s_node.getparent() == choice_nodes[choice_node]: + node_self.append(child_self) + else: + node_self.append(child_self) if s_node.get('type') == 'leaf-list': if s_node.get('ordered-by') == 'user' and \ s_node.tag not in ordered_by_user: diff --git a/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py b/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py index e1b8caf..03fdcad 100755 --- a/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py +++ b/ncdiff/src/yang/ncdiff/tests/test_ncdiff.py @@ -2003,6 +2003,355 @@ def test_delta_6(self): config5 = config1 + delta3 self.assertEqual(config2, config5) + def test_delta_7(self): + # choice + no_choice_xml = """ + + + + + + 5 + + + + + + """ + xml1 = """ + + + + + + 5 + + + hostname + + + + + + + + """ + xml2 = """ + + + + + + 5 + + + 1.1.1.1 + + + + + + + + """ + xml3 = """ + + + + + + 5 + + + 2.2.2.2 + + + + + + + + """ + expected_delta1 = """ + + + + + 5 + + + 1.1.1.1 + + + + + + + """ + expected_delta2 = """ + + + + + 5 + + + hostname + + + + + + + """ + expected_delta3 = """ + + + + + 5 + + + + + + """ + expected_delta4 = """ + + + + + 5 + + + 2.2.2.2 + + + + + + + """ + no_choice = Config(self.d, no_choice_xml) + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + config3 = Config(self.d, xml3) + delta1 = config2 - config1 + delta2 = config1 - config2 + delta3 = no_choice - config1 + delta4 = config1 - no_choice + delta5 = config3 - config2 + delta6 = config2 - config3 + self.assertEqual(str(delta1).strip(), expected_delta1.strip()) + self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + self.assertEqual(str(delta3).strip(), expected_delta3.strip()) + self.assertEqual(str(delta4).strip(), expected_delta2.strip()) + self.assertEqual(str(delta5).strip(), expected_delta4.strip()) + self.assertEqual(str(delta6).strip(), expected_delta1.strip()) + + def test_delta_8(self): + # multiple choices + xml1 = """ + + + + + + 5 + + + + + + + + + + 1.1.1.1 + + + + + + + + """ + xml2 = """ + + + + + + 5 + + + + + + + + + + hostname + + + + + + + + """ + expected_delta1 = """ + + + + + 5 + + + + + + + + + + hostname + + + + + + + """ + expected_delta2 = """ + + + + + 5 + + + + + + + + + + 1.1.1.1 + + + + + + + """ + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + delta1 = config2 - config1 + delta2 = config1 - config2 + self.assertEqual(str(delta1).strip(), expected_delta1.strip()) + self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + + def test_delta_9(self): + # choice with list + xml1 = """ + + + + + Ottawa + + + + + """ + xml2 = """ + + + + + Calgary + + + + + """ + expected_delta1 = """ + + + + Calgary + + + + """ + expected_delta2 = """ + + + + Ottawa + + + + """ + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + delta1 = config2 - config1 + delta2 = config1 - config2 + self.assertEqual(str(delta1).strip(), expected_delta1.strip()) + self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + + def test_delta_10(self): + # choice multiple nodes in one case + xml1 = """ + + + + one + + + + """ + xml2 = """ + + + + two + three + + + + """ + expected_delta1 = """ + + + two + three + + + """ + expected_delta2 = """ + + + one + + + """ + config1 = Config(self.d, xml1) + config2 = Config(self.d, xml2) + delta1 = config2 - config1 + delta2 = config1 - config2 + self.assertEqual(str(delta1).strip(), expected_delta1.strip()) + self.assertEqual(str(delta2).strip(), expected_delta2.strip()) + def test_delta_replace_1(self): config_xml1 = """ diff --git a/ncdiff/src/yang/ncdiff/tests/yang/Cisco-IOS-XE-tunnel.yang b/ncdiff/src/yang/ncdiff/tests/yang/Cisco-IOS-XE-tunnel.yang index d79c134..18a4a26 100644 --- a/ncdiff/src/yang/ncdiff/tests/yang/Cisco-IOS-XE-tunnel.yang +++ b/ncdiff/src/yang/ncdiff/tests/yang/Cisco-IOS-XE-tunnel.yang @@ -1,11 +1,22 @@ module Cisco-IOS-XE-tunnel { + yang-version 1.1; namespace "http://cisco.com/ns/yang/Cisco-IOS-XE-tunnel"; prefix ios-tun; + import cisco-semver { + prefix cisco-semver; + } + import cisco-semver-internal { + prefix cisco-semver-internal; + } + import ietf-inet-types { prefix inet; } + import tailf-common { + prefix tailf; + } import Cisco-IOS-XE-native { prefix ios; @@ -23,6 +34,14 @@ module Cisco-IOS-XE-tunnel { prefix ios-features; } + import Cisco-IOS-XE-interface-common { + prefix ios-ifc; + } + + import Cisco-IOS-XE-policy { + prefix ios-policy; + } + organization "Cisco Systems, Inc."; @@ -39,18 +58,78 @@ module Cisco-IOS-XE-tunnel { description "Cisco XE Native Tunnel Interfaces Yang model. - Copyright (c) 2016-2017 by Cisco Systems, Inc. + Copyright (c) 2016-2019 by Cisco Systems, Inc. All rights reserved."; // ========================================================================= // REVISION // ========================================================================= + revision 2022-11-01 { + description + "- Update yang-version to 1.1"; + cisco-semver-internal:os-version "17.10.1"; + } + revision 2022-03-01 { + description + "- added dual-overlay support for tunnel mode"; + cisco-semver-internal:os-version "17.8.1"; + } + + revision 2021-11-01 { + description + "- Added tunnel metadata command + - Ipsec v4 and v6 overlay support"; + cisco-semver-internal:os-version "17.7.1"; + } + + revision 2020-11-01 { + description + "- Initial revision for 17.4.1"; + cisco-semver-internal:os-version "17.4.1"; + } + + revision 2020-03-01 { + description + "- Added host option for tunnel destination"; + cisco-semver-internal:os-version "17.2.1"; + } + revision 2019-11-01 { + description + "- Added route-via config"; + cisco-semver:module-version "1.1.0"; + cisco-semver-internal:os-version "17.1.1"; + } + revision 2019-07-01 { + description + "- Establish semantic version baseline + - Tunnel VRF model condition added + - Enabled default values for tunnel parameters + - Corrected various issues with the tunnel mode + - Added Tunnel submode l2tpv3 under ethernet mode"; + cisco-semver:module-version "1.0.0"; + } + revision 2018-11-21 { + description + "Cleanup spelling errors in description statements"; + } + revision 2018-11-09 { + description + "added support for EoGRE P2P interfaces"; + } + revision 2018-08-12 { + description + "added must constraint on tunnel source on interface Dialer"; + } + revision 2018-03-06 { + description + "validate ip unnumbered and tunnel source are the same for tunnel mode sdwan"; + } revision 2017-08-28 { description "Extend modeling for multicast"; } revision 2017-07-11 { - description + description "add tailf dependency extension to order tunnel mode before tunnel key"; } revision 2017-04-28 { @@ -67,6 +146,30 @@ module Cisco-IOS-XE-tunnel { "Initial revision"; } + grouping tunnel-mode-ip-ipv6-opt-grouping { + choice tunmode-choice { + leaf ip { + description "over IP"; + tailf:cli-full-command; + type empty; + } + leaf ipv6 { + description "over IPv6"; + tailf:cli-full-command; + type empty; + } + container multipoint { + description "mode Multipoint"; + presence "true"; + tailf:cli-reset-container; + leaf "ipv6" { + description "over IPv6 (multipoint)"; + tailf:cli-full-command; + type empty; + } + } + } + } grouping config-interface-tunnel-grouping { container nhrp { @@ -80,6 +183,7 @@ module Cisco-IOS-XE-tunnel { "Number of seconds"; type uint8 { range "1..22"; + tailf:info "<1-22>;;Number of seconds"; } } } @@ -95,6 +199,8 @@ module Cisco-IOS-XE-tunnel { description "NHRP group mapping"; list nhrp-name { + tailf:cli-drop-node-name; + tailf:cli-suppress-mode; key "nhrp-name"; leaf nhrp-name { description @@ -116,6 +222,9 @@ module Cisco-IOS-XE-tunnel { leaf route-watch { description "Enable NHRP route watch"; + tailf:cli-boolean-no; + tailf:cli-trim-default; + tailf:cli-full-command; type boolean; } } @@ -125,25 +234,40 @@ module Cisco-IOS-XE-tunnel { "protocol-over-protocol tunneling"; // interface * / tunnel 6rd container tun-6rd { + when "../ios-tun:mode/ios-tun:ipv6ip-config/ios-tun:auto-6rd"; + tailf:alt-name "6rd"; + tailf:cli-diff-delete-before "../mode"; + tailf:cli-diff-create-after "../mode"; + tailf:cli-delete-when-empty; description "set tunnel 6rd parameters"; leaf br { description "Border Relay parameters"; type inet:ipv4-address; + tailf:cli-full-command; } container ipv4 { description "Common IPv4 transport source parameters"; + tailf:cli-delete-when-empty; + tailf:cli-compact-syntax; + tailf:cli-full-no; leaf prefix-len { description "Common IPv4 transport source prefix length"; - type uint8; + type uint8 { + range "0..31"; + tailf:info "<0-31>;;Length in number of bits"; + } } leaf suffix-len { description "Common IPv4 transport source suffix length"; - type uint8; + type uint8 { + range "0..31"; + tailf:info "<0-31>;;Length in number of bits"; + } } } leaf prefix { @@ -151,13 +275,37 @@ module Cisco-IOS-XE-tunnel { "Provider selected common IPv6 prefix"; type ios-types:ipv6-prefix; } + leaf reverse-map-check-config { + description + "Enable source IP address and port verification"; + tailf:alt-name "reverse-map-check"; + tailf:cli-boolean-no; + tailf:cli-full-command; + type boolean; + default "true"; + } leaf reverse-map-check { description "Enable source IP address and port verification"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + tailf:cli-full-command; type empty; } } + + // interface * / tunnel src-mac-address + leaf src-mac-address { + description + "Mac address"; + when "../ios-tun:mode/ios-tun:ethernet-config"; + tailf:cli-full-command; + type ios-types:cisco-mac-address-type { + tailf:info "H.H.H;; Source MAC address"; + } + } // interface * / tunnel bandwidth container bandwidth { description @@ -165,12 +313,24 @@ module Cisco-IOS-XE-tunnel { leaf receive { description "Receive bandwidth"; - type uint32; + tailf:cli-full-command; + type uint32 { + range "1..10000000"; + tailf:info "<1-10000000>;;Bandwidth in kilobits"; + } + default "8000"; + tailf:cli-trim-default; } leaf transmit { description "Transmit bandwidth"; - type uint32; + tailf:cli-full-command; + type uint32 { + range "1..10000000"; + tailf:info "<1-10000000>;;Bandwidth in kilobits"; + } + default "8000"; + tailf:cli-trim-default; } } @@ -178,35 +338,99 @@ module Cisco-IOS-XE-tunnel { leaf checksum { description "enable end to end checksumming of packets"; - type empty; + tailf:cli-full-command; + type empty; } // interface * / tunnel source leaf source { description "source of tunnel packets"; + must "starts-with(., 'Dialer') and " + + " (number(substring-after(., 'Dialer')) = /ios:native/ios:interface/ios:Dialer/ios:name)" + + " or not (starts-with(., 'Dialer'))" { + error-message "To use a Dialer interface, the Dialer interface has to be configured 1st, vice versa for delete"; + } type string; } // interface * / tunnel destination + container destination-config { + description + "destination of tunnel"; + tailf:alt-name "destination"; + choice dest-choice { + leaf ipv4 { + description "ip address or host name"; + tailf:cli-full-command; + tailf:cli-drop-node-name; + must "not (../../ios-tun:mode/ios-tun:gre-config/ios-tun:multipoint) + and not (../../ios-tun:mode/ios-tun:ipv6ip-config/ios-tun:auto-6rd) + and not (../../ios-tun:mode/ios-tun:ipv6ip-config/ios-tun:auto-6to4) + and not (../../ios-tun:mode/ios-tun:ipv6ip-config/ios-tun:auto-isatap)" { + error-message "tunnel destination and this mode cannot be configured together."; + } + type inet:ipv4-address{ + tailf:info "A.B.C.D;;Destination address"; + } + } + leaf ipv6 { + description "IPv6 address"; + tailf:cli-full-command; + tailf:cli-drop-node-name; + type inet:ipv6-address { + tailf:info "X:X:X:X::X;;IPv6 address"; + } + } + leaf "dynamic" { + description "destination dynamic"; + tailf:cli-full-command; + type empty; + } + leaf host { + description "host name"; + tailf:cli-drop-node-name; + type string; + } + } + } container destination { description "destination of tunnel"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; leaf ipaddress-or-host { + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + tailf:cli-drop-node-name; type inet:host; } container list { description "List of Tunnel destinations"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; container mpls { description "MPLS destination list"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; container traffic-eng { description "MPLS Traffic Engineering destination list"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; leaf name { description "Specify a destination list by name"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; type string; } } @@ -214,14 +438,22 @@ module Cisco-IOS-XE-tunnel { } } + // interface * / tunnel dst-port container dst-port { description "tunnel destination port"; + when "../ios-tun:mode/ios-tun:udp-config"; + tailf:cli-diff-delete-before "../ios-tun:mode/ios-tun:udp-config"; + tailf:cli-diff-create-after "../ios-tun:mode/ios-tun:udp-config"; choice dst-port-choice { case port-num-case { leaf port-num { - type uint16; + tailf:cli-drop-node-name; + type uint16 { + range "0..65535"; + tailf:info "<0-65535>;;tunnel destination port number"; + } } } case dynamic-case { @@ -244,6 +476,9 @@ module Cisco-IOS-XE-tunnel { leaf output { description "apply QoS service policy in the output direction"; + must "(/ios:native/ios:policy/ios-policy:policy-map[ios-policy:name = current()])" { + error-message "Warning: This control policy-map does not exist"; + } type string; } } @@ -253,6 +488,12 @@ module Cisco-IOS-XE-tunnel { leaf entropy { description "Compute hash (entropy) on payload and propagate to tunnel header"; + tailf:cli-full-command; + must "((../ios-tun:mode/ios-tun:gre-config) + or (../ios-tun:mode/ios-tun:ethernet-config/ios-tun:gre/ios-tun:ipv4) + or (../ios-tun:mode/ios-tun:ethernet-config/ios-tun:gre/ios-tun:ipv6))" { + error-message "Tunnel entropy and this tunnel mode are not allowed."; + } type empty; } @@ -282,30 +523,100 @@ module Cisco-IOS-XE-tunnel { leaf key { description "security or selector key"; + tailf:cli-diff-delete-before "../mode"; + tailf:cli-diff-create-after "../mode"; + tailf:cli-full-command; type uint64 { range "0..4294967295"; + tailf:info "<0-4294967295>;;key"; } } + // interface * / tunnel mode container mode { description "tunnel encapsulation method"; + tailf:cli-full-no; choice mode-choice { - // interface * / tunnel mode ipsec + // interface * / tunnel mode aurp + leaf aurp { + description + "AURP TunnelTalk AppleTalk encapsulation"; + tailf:cli-full-command; + type empty; + } + leaf cayman { + description + "Cayman TunnelTalk AppleTalk encapsulation"; + tailf:cli-full-command; + type empty; + } + leaf dvmrp { + description + "DVMRP multicast tunnel"; + tailf:cli-full-command; + type empty; + } + leaf eon { + description + "EON compatible CLNS tunnel"; + tailf:cli-full-command; + type empty; + } + leaf iptalk { + description + "Apple IPTalk encapsulation"; + tailf:cli-full-command; + type empty; + } + leaf nos { + description + "IP over IP encapsulation (KA9Q/NOS compatible)"; + tailf:cli-full-command; + type empty; + } + leaf rbscp { + description + "RBSCP in IP tunnel"; + tailf:cli-full-command; + type empty; + } + leaf tag-switching { + description + "IP over Tag Switching encapsulation"; + tailf:cli-full-command; + type empty; + } container ipsec { description "IPSec tunnel encapsulation"; + tailf:cli-reset-container; container ipv4 { description "over IPv4"; + tailf:cli-delete-when-empty; presence "true"; + leaf v6-overlay { + type empty; + } } container ipv6 { description "over IPv6"; + tailf:cli-delete-when-empty; presence "true"; + leaf v4-overlay { + type empty; + } + } + leaf dual-overlay { + description + "over dual-overlay"; + tailf:cli-full-command; + type empty; } } + // interface * / tunnel mode mpls container mpls { description @@ -313,6 +624,7 @@ module Cisco-IOS-XE-tunnel { container traffic-eng { description "Traffic engineering tunnels"; + tailf:cli-delete-when-empty; presence "true"; leaf multilsp { description @@ -327,40 +639,94 @@ module Cisco-IOS-XE-tunnel { } } // interface * / tunnel mode ethernet - container ethernet { + container ethernet-config { description "Ethernet over gre"; - leaf gre { - description - "Ethernet over gre"; - type enumeration { - enum "ipv4"; - enum "ipv6"; + tailf:alt-name "ethernet"; + choice eth-mode-choice { + container gre { + description "Ethernet over GRE"; + choice address-family-choice { + container ipv4 { + description "over ipv4"; + presence "true"; + tailf:cli-reset-container; + leaf "p2p" { + description "point-to-point over ipv4"; + tailf:cli-full-command; + type empty; + } + } + container ipv6 { + description "over ipv6"; + presence "true"; + tailf:cli-reset-container; + leaf "p2p" { + description "point-to-point over ipv6"; + tailf:cli-full-command; + type empty; + } + } + } + } + container l2tpv3 { + description "Use L2TPv3 encapsulation"; + leaf "manual" { + description "Manually configure L2TP parameters"; + tailf:cli-full-command; + type empty; + } } } } - // interface * / tunnel mode gre - container gre { + container ethernet { description - "generic route encapsulation protocol"; - leaf ip { - description - "over IP"; - type empty; - } - leaf ipv6 { - description - "over IPv6"; - type empty; - } - container multipoint { + "Ethernet over gre"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + container gre { description - "over IPv4 (multipoint)"; - presence "true"; - leaf ipv6 { - description - "over IPv6"; - type empty; + "Ethernet over gre"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + container ipv4 { + description "Ethernet over GRE ipv4"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + tailf:cli-delete-when-empty; + presence "true"; + leaf "p2p" { + must "count(/ios:native/ios:interface/ios:Tunnel/tunnel/mode/ethernet/gre/ipv4/p2p) + count (/ios:native/ios:interface/ios:Tunnel/tunnel/mode/ethernet/gre/ipv6/p2p) <= 10" { + error-message "Too many EoGRE P2P interfaces"; + } + description "point-to-point over ipv4"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + tailf:cli-full-command; + type empty; + } + } + container ipv6 { + description "Ethernet over GRE ipv6"; + presence "true"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + leaf "p2p" { + must "count(/ios:native/ios:interface/ios:Tunnel/tunnel/mode/ethernet/gre/ipv4/p2p) + count (/ios:native/ios:interface/ios:Tunnel/tunnel/mode/ethernet/gre/ipv6/p2p) <= 10" { + error-message "Too many EoGRE P2P interfaces"; + } + description "point-to-point over ipv6"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + tailf:cli-full-command; + type empty; + } } } } @@ -369,9 +735,15 @@ module Cisco-IOS-XE-tunnel { description "IP over IP encapsulation"; presence "true"; + tailf:cli-reset-container; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands { + tailf:cli-reset-all-siblings; + } leaf decapsulate-any { description "Incoming traffic only"; + tailf:cli-full-command; type empty; } } @@ -379,84 +751,431 @@ module Cisco-IOS-XE-tunnel { leaf ipv6 { description "Generic packet tunneling in IPv6"; + tailf:cli-full-command; type empty; } // interface * / tunnel mode ipv6ip + container ipv6ip-config { + description + "IPv6 over IP encapsulation"; + tailf:alt-name "ipv6ip"; + presence "true"; + tailf:cli-reset-container; + choice ipv6ip-choice { + leaf auto-6rd { + tailf:alt-name "6rd"; + description + "IPv6 automatic tunnelling using 6rd"; + tailf:cli-full-command; + type empty; + } + leaf auto-6to4 { + tailf:alt-name "6to4"; + description + "IPv6 automatic tunnelling using 6to4"; + tailf:cli-full-command; + type empty; + } + leaf auto-isatap { + tailf:alt-name "isatap"; + description + "IPv6 automatic tunnelling using ISATAP"; + tailf:cli-full-command; + type empty; + } + } + } container ipv6ip { description "IPv6 over IP encapsulation"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; presence "true"; leaf auto-6rd { + tailf:alt-name "6rd"; description "IPv6 automatic tunnelling using 6rd"; + tailf:cli-full-command; type empty; } leaf auto-6to4 { + tailf:alt-name "6to4"; description "IPv6 automatic tunnelling using 6to4"; + tailf:cli-full-command; type empty; } leaf isatap { description "IPv6 automatic tunnelling using ISATAP"; + tailf:cli-full-command; + type empty; + } + } + // interface * / tunnel mode gre + container gre-config { + description + "generic route encapsulation protocol"; + tailf:alt-name "gre"; + choice tunmode-choice { + leaf ip { + description "over IP"; + tailf:cli-full-command; + tailf:cli-boolean-no; + type boolean; + default "true"; + tailf:cli-trim-default; + } + leaf ipv6 { + description "over IPv6"; + tailf:cli-full-command; + type empty; + } + container multipoint { + description "mode Multipoint"; + presence "true"; + tailf:cli-reset-container; + leaf "ipv6" { + description "over IPv6 (multipoint)"; + tailf:cli-full-command; + type empty; + } + } + } + } + container gre { + description + "generic route encapsulation protocol"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + leaf ip { + description + "over IP"; + tailf:cli-full-command; type empty; } + leaf ipv6 { + description + "over IPv6"; + tailf:cli-full-command; + type empty; + } + container multipoint { + description + "over IPv4 (multipoint)"; + presence "true"; + leaf ipv6 { + description + "over IPv6"; + tailf:cli-full-command; + type empty; + } + } } // interface * / tunnel mode udp + container udp-config { + description + "UDP encapsulation protocol"; + tailf:alt-name "udp"; + uses tunnel-mode-ip-ipv6-opt-grouping; + } leaf udp { description "UDP encapsulation protocol"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; type enumeration { enum "ip"; enum "multipoint"; } } + // interface * / tunnel mode vxlan + container vxlan { + description + "VXLAN encapsulation"; + choice tunmode-choice { + container ipv4 { + description "over ip"; + choice tun-ip-vxlan-choice { + leaf default-mac { + description + "default mac"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + container src-mac { + description + "Source Mac address"; + tailf:cli-drop-node-name; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands; + leaf source-mac { + description + "Source Mac address"; + tailf:cli-drop-node-name; + tailf:cli-incomplete-command; + tailf:cli-delete-container-on-delete; + type ios-types:cisco-mac-address-type { + tailf:info "H.H.H;; Source MAC address"; + } + } + leaf dst-mac { + description + "Destination Mac address"; + tailf:cli-drop-node-name; + tailf:cli-delete-container-on-delete; + must "../ios-tun:source-mac" { + error-message "Delete src-mac and dest-mac together"; + } + type ios-types:cisco-mac-address-type { + tailf:info "H.H.H;; Destination MAC address"; + } + } + } + } + } + container ipv6 { + description "over IPv6"; + choice tun-ip-vxlan-choice { + leaf default-mac { + description + "default mac"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + container src-mac { + description + "Source Mac address"; + tailf:cli-drop-node-name; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands; + leaf source-mac { + description + "Source Mac address"; + tailf:cli-drop-node-name; + tailf:cli-incomplete-command; + tailf:cli-delete-container-on-delete; + type ios-types:cisco-mac-address-type { + tailf:info "H.H.H;; Source MAC address"; + } + } + leaf dst-mac { + description + "Destination Mac address"; + tailf:cli-drop-node-name; + tailf:cli-delete-container-on-delete; + type ios-types:cisco-mac-address-type { + tailf:info "H.H.H;; Destination MAC address"; + } + } + } + } + } + container multipoint { + choice tunmode-mul-choice { + leaf "ip" { + description "over IPv4 (multipoint)"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + tailf:cli-drop-node-name; + type empty; + } + leaf "ipv6" { + description "over IPv6 (multipoint)"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + } + } + } + } + // interface * / tunnel mode vxlan-gpe + container vxlan-gpe { + description + "VXLAN gpe encapsulation"; + choice tunmode-choice { + leaf ipv4 { + description "over ip"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + leaf ipv6 { + description "over IPv6"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + container multipoint { + choice tunmode-mul-choice { + leaf "ip" { + description "over IPv4 (multipoint)"; + tailf:cli-full-command; + tailf:cli-drop-node-name; + tailf:cli-delete-container-on-delete; + type empty; + } + leaf "ipv6" { + description "over IPv6 (multipoint)"; + tailf:cli-full-command; + tailf:cli-delete-container-on-delete; + type empty; + } + } + } + } + } + // interface * / tunnel mode sdwan + leaf sdwan { + if-feature ios-features:sdwan; + description + "SDWAN"; + tailf:cli-full-command; + must "../../source = /ios:native/ios:interface/ios:Tunnel/ios:ip/ios:unnumbered" { + error-message "ip unnumbered and tunnel source are not same for tunnel mode sdwan"; + } + tailf:cli-diff-delete-before "../../source"; + tailf:cli-diff-create-after "../../source"; + type empty; + } } } + // interface * / tunnel network-id container network-id { description "Set network-id"; + tailf:cli-compact-syntax; + tailf:cli-delete-when-empty; + tailf:cli-reset-container; + tailf:cli-incomplete-command; + tailf:cli-sequence-commands { + tailf:cli-reset-all-siblings; + } leaf id { - type uint16; + description + "Set network-id"; + tailf:cli-incomplete-command; + tailf:cli-delete-container-on-delete; + tailf:cli-drop-node-name; + must "../ios-tun:nexthop" { + error-message "Delete network-id and nexthop together"; + } + type uint16 { + range "1..65535"; + tailf:info "<1-65535>;;network id"; + } } leaf nexthop { description "nexthop"; + must "../ios-tun:id" { + error-message "Delete nexthop and network-id value together"; + } type inet:ipv4-address; } + leaf weight { + description + "weight"; + tailf:cli-optional-in-sequence; + must "../ios-tun:nexthop" { + error-message "Delete weight with the network-id deletion"; + } + type uint16 { + range "1..65535"; + tailf:info "<1-65535>;;weight"; + } + } leaf qos { description "QoS profile"; + must "../ios-tun:nexthop" { + error-message "Delete QoS profile first with the network-id deletion"; + } type string; } - leaf weight { - description - "weight"; - type uint16; - } } + // interface * / tunnel path-mtu-discovery container path-mtu-discovery { description "Enable Path MTU Discovery on tunnel"; presence "true"; + tailf:cli-delete-when-empty; + tailf:cli-display-separated; + leaf age-timer { + description + "Set PMTUD aging timer"; + tailf:cli-full-command; + type union { + type uint8 { + range "10..30"; + tailf:info "<10-30>;;Aging time"; + } + type enumeration { + enum "infinite"; + } + } + } + leaf min-mtu { + description + "Min pmtud mtu allowed"; + tailf:cli-full-command; + type uint16 { + range "92..65535"; + tailf:info "<92-65535>;;Bytes"; + } + } } + // interface * / tunnel protection container protection { description "Enable tunnel protection"; // @mount Cisco-IOS-XE-crypto } + + // interface * / tunnel tsp-hop + leaf tsp-hop { + description + "Define a TSP hop"; + tailf:cli-full-command; + type empty; + } // interface * / tunnel mpls container mpls { uses ios-mpls:config-tunnel-mpls-grouping; } // interface * / tunnel raw-packet-interface + container raw-packet-interface-config { + description + "physical interface for all packets entering into be tunneled and for all packets entering the tunnel to exit"; + tailf:alt-name "raw-packet-interface"; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands; + uses ios-ifc:interface-grouping; + } + leaf raw-packet-interface { description - "physical interface for all packets entering itto be tunneled and for all packets entering the tunnel to exit"; + "physical interface for all packets entering into be tunneled and for all packets entering the tunnel to exit"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; + type string; + } + + // interface * / tunnel rrr + leaf rrr { + description + "RRR configuration"; type string; } @@ -467,58 +1186,148 @@ module Cisco-IOS-XE-tunnel { leaf ack-split { description "ACK splitting"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type uint8 { range "1..32"; + tailf:info "<1-32>;;Split number"; } } leaf delay { description "enable delay"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type empty; } + leaf drop-scale { + description + "Drop scale"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } + type uint8 { + range "1..255"; + tailf:info "<1-255>;;Drop scale"; + } + } + leaf fuzz { + description + "Fuzz factor"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } + type uint8 { + range "1..255"; + tailf:info "<1-255>;;Fuzz factor"; + } + } + leaf init-tsn { + description + "Initial TSN"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } + type uint32 { + range "1..16777215"; + tailf:info "<1-16777215>;;Initial TSN"; + } + } leaf input-drop { description "max tunnel queue size (number of bw*delay)"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type uint8 { range "1..10"; + tailf:info "<1-10>;;Number of bw*delay products"; } } leaf long-drop { description "Drop non-transmitted packets w/excessive delay"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } + type empty; + } + leaf order { + description + "release packets in order"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type empty; } leaf report { description "enable SCTP report chunk"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } + type empty; + } + leaf scaling { + description + "ACK split scaling"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type empty; } leaf window-stuff { description "Window stuffing"; + tailf:cli-full-command; + must "../../ios-tun:mode/ios-tun:rbscp" { + error-message "Only valid in tunnel rbscp mode"; + } type uint8 { range "1..20"; + tailf:info "<1-20>;;Window step up size"; } } } - // interface * / tunnel snooping + // interface * / tunnel snooping leaf snooping { description "Snooping on tunnel"; + tailf:cli-full-command; + when "../ios-tun:mode/ios-tun:ethernet-config"; type enumeration { enum "enable"; } } - // interface * / tunnel src-port + // interface * / tunnel src-port container src-port { description "tunnel source port"; + when "../ios-tun:mode/ios-tun:udp-config"; + tailf:cli-diff-delete-before "../ios-tun:mode/ios-tun:udp-config"; + tailf:cli-diff-create-after "../ios-tun:mode/ios-tun:udp-config"; choice src-port-choice { case port-num-case { leaf port-num { - type uint16; + tailf:cli-drop-node-name; + type uint16 { + range "0..65535"; + tailf:info "<0-65535>;;Tunnel Source port number"; + } } } case dynamic-case { @@ -534,21 +1343,71 @@ module Cisco-IOS-XE-tunnel { // interface * / tunnel tos leaf tos { description - "set type of sevice byte"; - type uint8; + "set type of service byte"; + tailf:cli-full-command; + type uint8 { + range "0..255"; + tailf:info "<0-255>;;tos"; + } + } + + // interface * / tunnel metadata + container metadata { + description + "metadata sharing options"; + leaf src_vpn { + description + "enable metadata sharing src-vpn"; + type empty; + } } // interface * / tunnel ttl leaf ttl { description "set time to live"; - type uint8; + tailf:cli-full-command; + type uint8 { + range "1..255"; + tailf:info "<1-255>;;ttl"; + } + default "255"; + tailf:cli-trim-default; } // interface * / tunnel udlr + container udlr-config { + description + "associate tunnel with unidirectional interface"; + tailf:alt-name "udlr"; + leaf address-resolution { + description + "Enable ARP and NHRP over a UDLR Tunnel"; + tailf:cli-full-command; + must "(../ios-tun:send-only)" { + error-message "UDL Tunnel ARP is for transmit-only tunnel interfaces"; + } + type empty; + } + choice udlr-choice { + container receive-only { + description + "Tunnel is receive-only capable"; + uses ios-ifc:interface-grouping; + } + container send-only { + description + "Tunnel is send-only capable"; + uses ios-ifc:interface-grouping; + } + } + } container udlr { description "associate tunnel with unidirectional interface"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; leaf address-resolution { description "Enable ARP and NHRP over a UDLR Tunnel"; @@ -566,27 +1425,227 @@ module Cisco-IOS-XE-tunnel { } } - // interface * / tunnel vlan + // interface * / tunnel vxlan + container vxlan { + description "Tunnel vxlan network"; + tailf:cli-compact-syntax; + when "../ios-tun:mode/ios-tun:vxlan"; + leaf vni { + description + "Tunnel vxlan network identifier"; + tailf:cli-full-command; + type uint32 { + range "1..16777215"; + tailf:info "<1-16777215>;;Tunnel vxlan network identifier number"; + } + } + container src-port-range { + description + "Tunnel vxlan source port range"; + tailf:cli-drop-node-name; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands { + tailf:cli-reset-all-siblings; + } + leaf source-port-range { + description + "Tunnel vxlan source port number start"; + tailf:cli-incomplete-command; + type uint16 { + range "1..65535"; + tailf:info "<1-65535>;;Tunnel vxlan source port number start"; + } + } + leaf source-port-end { + description + "Tunnel vxlan source port number end"; + tailf:cli-drop-node-name; + tailf:cli-full-command; + type uint16 { + range "1..65535"; + tailf:info "<1-65535>;;Tunnel vxlan source port number end"; + } + } + } + } + + // interface * / tunnel vlan container vlan { description "Set vlan-id for ethernet over gre mode"; + when "../ios-tun:mode/ios-tun:ethernet-config"; + tailf:cli-compact-syntax; + tailf:cli-sequence-commands; leaf id1 { - type uint16; + tailf:cli-drop-node-name; + type uint16 { + range "1..4094"; + tailf:info "<1-4094>;;vlan-id"; + } } leaf hyphen { + tailf:alt-name "-"; + tailf:cli-break-sequence-commands; type empty; } leaf id2 { - type uint16; + tailf:cli-drop-node-name; + type uint16 { + range "1..4094"; + tailf:info "<1-4094>;;vlan-id"; + } } } - // interface * / tunnel vrf + container vrf-config { + description "set tunnel vrf membership"; + tailf:alt-name "vrf"; + tailf:cli-compact-syntax; + leaf multiplexing { + description + "multiple vrf's multiplexing"; + tailf:cli-full-command; + type empty; + must "not (../vrf-common/vrf)" { + error-message "common vrf configured already. Remove it and then try again"; + } + must "not (../vrf-egress/vrf) and not (../vrf-ingress/vrf)" { + error-message "egress-only ingress-only vrf configured already. Remove it and then try again"; + } + } + container vrf-common { + description "vrf common config"; + tailf:cli-drop-node-name; + leaf vrf { + tailf:cli-full-command; + tailf:cli-drop-node-name; + type string; + tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:Tunnel/ios-tun:tunnel/ios-tun:vrf-config/ios-tun:multiplexing"; + tailf:cli-diff-delete-before "/ios:native/ios:vrf/ios:definition/ios:name" { + tailf:cli-when-target-delete; + } + tailf:cli-diff-delete-before "/ios:native/ios:ip/ios:vrf/ios:name" { + tailf:cli-when-target-delete; + } + tailf:cli-diff-create-after "/ios:native/ios:vrf/ios:definition/ios:name" { + tailf:cli-when-target-set; + } + tailf:cli-diff-create-after "/ios:native/ios:ip/ios:vrf/ios:name" { + tailf:cli-when-target-set; + } + must "/ios:native/ios:vrf/ios:definition[ios:name=current()] + or /ios:native/ios:ip/ios:vrf[ios:name=current()]" { + error-message "VRF is not configured"; + } + must "not (../../vrf-egress/vrf) and not (../../vrf-ingress/vrf)" { + error-message "egress-only ingress-only vrf configured already. Remove it and then try again"; + } + must "not (../../multiplexing)" { + error-message "vrf multiplexing configured already. Remove it and then try again"; + } + } + } + container vrf-egress { + description "egress-only"; + tailf:cli-drop-node-name; + tailf:cli-sequence-commands; + tailf:cli-compact-syntax; + leaf vrf { + tailf:cli-drop-node-name; + type string; + must "not (../../vrf-common/vrf) and not (../../vrf-ingress/vrf=current())" { + error-message "ingress-only and egress-only vrf should not be same"; + } + must "not (../../multiplexing)" { + error-message "vrf multiplexing configured already. Remove it and then try again"; + } + must "/ios:native/ios:vrf/ios:definition[ios:name=current()] + or /ios:native/ios:ip/ios:vrf[ios:name=current()]" { + error-message "VRF is not configured"; + } + } + leaf egress-only { + type empty; + } + } + container vrf-ingress { + description "ingress-only"; + tailf:cli-drop-node-name; + tailf:cli-sequence-commands; + tailf:cli-compact-syntax; + leaf vrf { + tailf:cli-drop-node-name; + type string; + must "not (../../vrf-common/vrf) and not (../../vrf-egress/vrf=current())" { + error-message "ingress-only and egress-only vrf should not be same"; + } + must "not (../../multiplexing)" { + error-message "vrf multiplexing configured already. Remove it and then try again"; + } + must "/ios:native/ios:vrf/ios:definition[ios:name=current()] + or /ios:native/ios:ip/ios:vrf[ios:name=current()]" { + error-message "VRF is not configured"; + } + } + leaf ingress-only { + type empty; + } + } + } + // interface * / route-via preferred | mandatory + container route-via { + description + "Select subset of routes for tunnel transport"; + tailf:cli-sequence-commands; + tailf:cli-compact-syntax; + leaf interface { + tailf:cli-drop-node-name; + tailf:cli-incomplete-command; + description + "Routing interface for tunnel packets"; + must "starts-with(., 'Dialer') and " + + " (number(substring-after(., 'Dialer')) = /ios:native/ios:interface/ios:Dialer/ios:name)" + + " or not (starts-with(., 'Dialer'))" { + error-message "To use a Dialer interface, the Dialer interface has to be configured 1st, vice versa for delete"; + } + type string; + } + choice route-via-choice { + case preferred-case { + leaf preferred { + description + "Preferred route, if not available, use any route"; + type empty; + } + } + case mandatory-case { + leaf mandatory { + description + "Mandatory route, if not available, drop traffic"; + type empty; + } + } + } + } leaf vrf { description "set tunnel vrf membership"; + status deprecated; + tailf:hidden deprecated; + tailf:cli-ignore-modified; type string; } + leaf mpls-ip-only { + description + "Copy DF bit from MPLS header to outer GRE"; + must "../ios-tun:path-mtu-discovery" { + error-message "tunnel path-mtu-discovery needs to be configured before configuring mpls-ip-only, vice-versa for delete"; + } + tailf:cli-diff-create-after "../ios-tun:path-mtu-discovery"; + tailf:cli-diff-delete-before "../ios-tun:path-mtu-discovery"; + tailf:cli-full-command; + type empty; + } } } diff --git a/ncdiff/src/yang/ncdiff/tests/yang/jon.yang b/ncdiff/src/yang/ncdiff/tests/yang/jon.yang index 4544704..00c61df 100644 --- a/ncdiff/src/yang/ncdiff/tests/yang/jon.yang +++ b/ncdiff/src/yang/ncdiff/tests/yang/jon.yang @@ -81,4 +81,4 @@ module jon { } } } -} \ No newline at end of file +} From 5bf7fee683903f513d346a72d800f2d3e5fdc074 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sun, 3 Dec 2023 03:26:32 +0000 Subject: [PATCH 13/20] added diff_type minimum-replace and add_attribute_at_depth method --- ncdiff/src/yang/ncdiff/config.py | 2 +- ncdiff/src/yang/ncdiff/netconf.py | 34 ++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index 516b82a..bc2fc53 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -487,7 +487,7 @@ class ConfigDelta(object): 'delete' or 'remove'. diff_type : `str` - Choice of 'minimum' or 'replace'. This value has impact on attribute + Choice of 'minimum', 'minimum-replace' or 'replace'. This value has impact on attribute nc. In general, there are two options to construct nc. The first option is to find out minimal changes between config_src and config_dst. Then attribute nc will reflect what needs to be modified. diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index 191b80c..dfe5cb0 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -127,7 +127,7 @@ class NetconfCalculator(BaseCalculator): 'delete' or 'remove'. diff_type : `str` - Choice of 'minimum' or 'replace'. This value has impact on attribute + Choice of 'minimum' 'minimum-replace', or 'replace'. This value has impact on attribute nc. In general, there are two options to construct nc. The first option is to find out minimal changes between config_src and config_dst. Then attribute nc will reflect what needs to be modified. @@ -185,8 +185,40 @@ def sub(self): self.get_config_replace(ele1, ele2) else: self.node_sub(ele1, ele2, depth=0) + # add attribute at depth if diff_type is 'minimum-replace' + if self.diff_type == 'minimum-replace': + self.add_attribute_at_depth(ele1, self.replace_depth+1, 'operation', 'replace') return ele1 + def add_attribute_at_depth(self, root, depth, attribute, value): + '''add_attribute_at_depth + + High-level api: Add an attribute to all nodes at a specified depth. + + Parameters + ---------- + root : `Element` + The root of a config tree. + depth : `int` + The depth of nodes to be added with an attribute. + attribute : `str` + The name of the attribute to be added. + value : `str` + The value of the attribute to be added. + + Returns + ------- + None + + ''' + current_depth = -1 + nodes_to_visit = [(root, current_depth + 1)] + while nodes_to_visit: + node, current_depth = nodes_to_visit.pop(0) + if current_depth == depth: + node.set(attribute, value) + nodes_to_visit.extend((child, current_depth + 1) for child in node) + def get_config_replace(self, node_self, node_other): '''get_config_replace From 80d3364c53ba99c58fc314071670f42f0bdba3e9 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Sun, 3 Dec 2023 03:33:31 +0000 Subject: [PATCH 14/20] added comments to code --- ncdiff/src/yang/ncdiff/netconf.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index dfe5cb0..429e0c2 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -211,12 +211,22 @@ def add_attribute_at_depth(self, root, depth, attribute, value): None ''' + # Initialize current_depth as -1 current_depth = -1 + + # Create a list with the root node and its depth (0) nodes_to_visit = [(root, current_depth + 1)] + + # While there are nodes to visit while nodes_to_visit: + # Pop the first node and its depth from the list node, current_depth = nodes_to_visit.pop(0) + + # If the current node's depth matches the specified depth if current_depth == depth: + # Add the attribute to the node node.set(attribute, value) + # Add the attribute to the node nodes_to_visit.extend((child, current_depth + 1) for child in node) def get_config_replace(self, node_self, node_other): From 1da441c3452c3feeb04a15139443937464f4657f Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Wed, 20 Dec 2023 21:00:52 +0000 Subject: [PATCH 15/20] added replace_xpath --- ncdiff/src/yang/ncdiff/config.py | 8 ++- ncdiff/src/yang/ncdiff/netconf.py | 82 ++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index bc2fc53..4c9957e 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -500,19 +500,24 @@ class ConfigDelta(object): the specified level, depending on situations. Consider roots in a YANG module are level 0, their children are level 1, and so on so forth. The default value of replace_depth is 0. + + replace_xpath : `str` + Specify the xpath of the node to be replaced when diff_type is + 'minimum-replace'. The default value of replace_xpath is None. ''' def __init__(self, config_src, config_dst=None, delta=None, preferred_create='merge', preferred_replace='merge', preferred_delete='delete', - diff_type='minimum', replace_depth=0): + diff_type='minimum', replace_depth=0, replace_xpath=None): ''' __init__ instantiates a ConfigDelta instance. ''' self.diff_type = diff_type self.replace_depth = replace_depth + self.replace_xpath = replace_xpath if not isinstance(config_src, Config): raise TypeError("argument 'config_src' must be " "yang.ncdiff.Config, but not '{}'" @@ -569,6 +574,7 @@ def nc(self): preferred_delete=self.preferred_delete, diff_type=self.diff_type, replace_depth=self.replace_depth, + replace_xpath=self.replace_xpath, ).sub @property diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index 429e0c2..7a31e12 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -1,4 +1,5 @@ import re +import json import pprint import logging from lxml import etree @@ -140,20 +141,26 @@ class NetconfCalculator(BaseCalculator): the specified level, depending on situations. Consider roots in a YANG module are level 0, their children are level 1, and so on so forth. The default value of replace_depth is 0. + + replace_xpath: `str` + Specify the xpath of the node to be replaced when diff_type is + 'minimum-replace'. The default value of replace_xpath is None. ''' def __init__(self, device, etree1, etree2, preferred_create='merge', preferred_replace='merge', preferred_delete='delete', - diff_type='minimum', replace_depth=0): + diff_type='minimum', replace_depth=0, replace_xpath=None): ''' __init__ instantiates a NetconfCalculator instance. ''' BaseCalculator.__init__(self, device, etree1, etree2) + self.device = device self.diff_type = diff_type self.replace_depth = replace_depth + self.replace_xpath = replace_xpath if preferred_create in ['merge', 'create', 'replace']: self.preferred_create = preferred_create else: @@ -186,7 +193,11 @@ def sub(self): else: self.node_sub(ele1, ele2, depth=0) # add attribute at depth if diff_type is 'minimum-replace' - if self.diff_type == 'minimum-replace': + if self.diff_type == 'minimum-replace' and self.replace_xpath: + namespaces = self.device._get_ns(ele1) + logger.debug("Namespaces:\n{}".format(json.dumps(namespaces, indent=2))) + self.add_attribute_by_xpath(ele1, self.replace_xpath, 'operation', 'replace', namespaces) + elif self.diff_type == 'minimum-replace': self.add_attribute_at_depth(ele1, self.replace_depth+1, 'operation', 'replace') return ele1 @@ -229,6 +240,73 @@ def add_attribute_at_depth(self, root, depth, attribute, value): # Add the attribute to the node nodes_to_visit.extend((child, current_depth + 1) for child in node) + # Not used. Saved for further use-case + def find_by_tags(self, root, tags): + ''' + Finds all nodes matching a list of tags. + + Parameters + ---------- + root : `Element` + The root of a config tree. + tags : `list` of `str` + The list of tags specifying the nodes to be found. + + Returns + ------- + `list` of `Element` + The list of matching nodes. + ''' + # Start with the root element + nodes = [root] + + # Traverse the tree for each tag + for tag in tags: + new_nodes = [] + for node in nodes: + new_nodes.extend(node.findall('.//{}'.format(tag))) + nodes = new_nodes + + return nodes + + # Not used. Saved for further use-case + def add_attribute_by_tags(self, root, tags, attribute, value): + '''add_attribute_by_tags + + ''' + nodes = self.find_by_tags(self, root, tags) + + for node in nodes: + node.set(attribute, value) + + def add_attribute_by_xpath(self, root, xpath, attribute, value, namespaces=None): + ''' + Adds an attribute to all nodes matching an XPath expression. + + Parameters + ---------- + root : `Element` + The root of a config tree. + xpath : `str` + The XPath expression specifying the nodes to be modified. + attribute : `str` + The name of the attribute to be added. + value : `str` + The value of the attribute to be added. + namespaces : `dict`, optional + The namespace prefix-URI mapping, by default None + + Returns + ------- + None + ''' + # Find all nodes matching the XPath expression + nodes = root.xpath(xpath, namespaces=namespaces) + + # Add the attribute to all matching nodes + for node in nodes: + node.set(attribute, value) + def get_config_replace(self, node_self, node_other): '''get_config_replace From 2550545b8c17b425c59d41bb397de71145e9c904 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Tue, 16 Jan 2024 02:30:40 +0000 Subject: [PATCH 16/20] added enhs --- connector/src/yang/connector/netconf.py | 6 +++++- ncdiff/src/yang/ncdiff/config.py | 2 +- ncdiff/src/yang/ncdiff/device.py | 1 - ncdiff/src/yang/ncdiff/netconf.py | 8 ++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/connector/src/yang/connector/netconf.py b/connector/src/yang/connector/netconf.py index 9ec6d63..2f8a425 100644 --- a/connector/src/yang/connector/netconf.py +++ b/connector/src/yang/connector/netconf.py @@ -695,7 +695,8 @@ def request(self, msg, timeout=30, return_obj=False): exception. timeout : `int`, optional An optional keyed argument to set timeout value in seconds. Its - default value is 30 seconds. + default value is 30 seconds. If self.timeout is bigger than 30, + timeout will use self.timeout. return_obj : `boolean`, optional Normally a string is returned as a reply. In other cases, we may want to return a RPCReply object, so we can access some attributes, @@ -751,6 +752,9 @@ def request(self, msg, timeout=30, return_obj=False): >>> ''' + if timeout <= self.timeout: + timeout = self.timeout + rpc = RawRPC(session=self.session, device_handler=self._device_handler, timeout=timeout, diff --git a/ncdiff/src/yang/ncdiff/config.py b/ncdiff/src/yang/ncdiff/config.py index 4c9957e..f75f430 100755 --- a/ncdiff/src/yang/ncdiff/config.py +++ b/ncdiff/src/yang/ncdiff/config.py @@ -501,7 +501,7 @@ class ConfigDelta(object): module are level 0, their children are level 1, and so on so forth. The default value of replace_depth is 0. - replace_xpath : `str` + replace_xpath : `str` or `list` Specify the xpath of the node to be replaced when diff_type is 'minimum-replace'. The default value of replace_xpath is None. ''' diff --git a/ncdiff/src/yang/ncdiff/device.py b/ncdiff/src/yang/ncdiff/device.py index 90609c4..6a030d0 100755 --- a/ncdiff/src/yang/ncdiff/device.py +++ b/ncdiff/src/yang/ncdiff/device.py @@ -66,7 +66,6 @@ def __init__(self, *args, **kwargs): ''' __init__ instantiates a ModelDevice instance. ''' - Netconf.__init__(self, *args, **kwargs) self.models = {} self.nodes = {} diff --git a/ncdiff/src/yang/ncdiff/netconf.py b/ncdiff/src/yang/ncdiff/netconf.py index 7a31e12..0cfd9ad 100755 --- a/ncdiff/src/yang/ncdiff/netconf.py +++ b/ncdiff/src/yang/ncdiff/netconf.py @@ -142,7 +142,7 @@ class NetconfCalculator(BaseCalculator): module are level 0, their children are level 1, and so on so forth. The default value of replace_depth is 0. - replace_xpath: `str` + replace_xpath: `str` or `list Specify the xpath of the node to be replaced when diff_type is 'minimum-replace'. The default value of replace_xpath is None. ''' @@ -196,7 +196,11 @@ def sub(self): if self.diff_type == 'minimum-replace' and self.replace_xpath: namespaces = self.device._get_ns(ele1) logger.debug("Namespaces:\n{}".format(json.dumps(namespaces, indent=2))) - self.add_attribute_by_xpath(ele1, self.replace_xpath, 'operation', 'replace', namespaces) + if isinstance(self.replace_xpath, list): + for xpath in self.replace_xpath: + self.add_attribute_by_xpath(ele1, xpath, 'operation', 'replace', namespaces) + else: + self.add_attribute_by_xpath(ele1, self.replace_xpath, 'operation', 'replace', namespaces) elif self.diff_type == 'minimum-replace': self.add_attribute_at_depth(ele1, self.replace_depth+1, 'operation', 'replace') return ele1 From c970bd17a4a9936b5fb46dc9407c66371e83194e Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Tue, 6 Feb 2024 04:18:28 +0000 Subject: [PATCH 17/20] added changelog --- ncdiff/docs/changelog/2024/February.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 ncdiff/docs/changelog/2024/February.rst diff --git a/ncdiff/docs/changelog/2024/February.rst b/ncdiff/docs/changelog/2024/February.rst new file mode 100644 index 0000000..567a6b4 --- /dev/null +++ b/ncdiff/docs/changelog/2024/February.rst @@ -0,0 +1,25 @@ +February 2024 +============= + ++-------------------------------+-------------------------------+ +| Module | Versions | ++===============================+===============================+ +| ``yang.ncdiff`` | 24.2 | +| ``yang.connector`` | 24.2 | ++-------------------------------+-------------------------------+ + + +Features: +^^^^^^^^^ + +* yang.connector + * updated `request` method +* yang.ncdiff + * updated NetconfCalculator class + * added diff_type `minimum-replace` + * added `add_attribute_at_depath` method + * added `add_attribute_by_xpath` method + * added `find_by_tags` method + * updated ConfigDelta class + * added `replace_xpath` argument + * added `find_by_tags` From 3f06727fc48c6fe26b3f1e1610b81581d5e8b86c Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Tue, 6 Feb 2024 04:36:30 +0000 Subject: [PATCH 18/20] updated changelog --- ncdiff/docs/changelog/2024/February.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ncdiff/docs/changelog/2024/February.rst b/ncdiff/docs/changelog/2024/February.rst index 567a6b4..be53beb 100644 --- a/ncdiff/docs/changelog/2024/February.rst +++ b/ncdiff/docs/changelog/2024/February.rst @@ -15,6 +15,7 @@ Features: * yang.connector * updated `request` method * yang.ncdiff + * synced PR2, 3, 5, 8, 13, 14, 19, 21, 24, 25, 26 and 27 from ncdiff repository * updated NetconfCalculator class * added diff_type `minimum-replace` * added `add_attribute_at_depath` method From fe3dd514a5b9c99a565fc862ffbdf7fd2ed84c99 Mon Sep 17 00:00:00 2001 From: Takashi Higashimura <34322875+tahigash@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:12:08 -0500 Subject: [PATCH 19/20] Update connector/src/yang/connector/netconf.py Co-authored-by: Dave Wapstra --- connector/src/yang/connector/netconf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/connector/src/yang/connector/netconf.py b/connector/src/yang/connector/netconf.py index 2f8a425..612bdb3 100644 --- a/connector/src/yang/connector/netconf.py +++ b/connector/src/yang/connector/netconf.py @@ -695,8 +695,8 @@ def request(self, msg, timeout=30, return_obj=False): exception. timeout : `int`, optional An optional keyed argument to set timeout value in seconds. Its - default value is 30 seconds. If self.timeout is bigger than 30, - timeout will use self.timeout. + default value is 30 seconds. If timeout is less than 30, + timeout will use the default of 30 seconds. return_obj : `boolean`, optional Normally a string is returned as a reply. In other cases, we may want to return a RPCReply object, so we can access some attributes, From 31df6dc97d0d2a336a0fba852a843fd4fafb27fd Mon Sep 17 00:00:00 2001 From: Takashi Higashimura Date: Thu, 8 Feb 2024 22:18:08 +0000 Subject: [PATCH 20/20] addressed comment --- ncdiff/docs/changelog/2024/February.rst | 26 -------------- ...ahigash_ncdiff_sync_enh_20240208171500.rst | 15 ++++++++ .../docs/changelog/undistributed/template.rst | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+), 26 deletions(-) delete mode 100644 ncdiff/docs/changelog/2024/February.rst create mode 100644 ncdiff/docs/changelog/undistributed/changelog_tahigash_ncdiff_sync_enh_20240208171500.rst create mode 100644 ncdiff/docs/changelog/undistributed/template.rst diff --git a/ncdiff/docs/changelog/2024/February.rst b/ncdiff/docs/changelog/2024/February.rst deleted file mode 100644 index be53beb..0000000 --- a/ncdiff/docs/changelog/2024/February.rst +++ /dev/null @@ -1,26 +0,0 @@ -February 2024 -============= - -+-------------------------------+-------------------------------+ -| Module | Versions | -+===============================+===============================+ -| ``yang.ncdiff`` | 24.2 | -| ``yang.connector`` | 24.2 | -+-------------------------------+-------------------------------+ - - -Features: -^^^^^^^^^ - -* yang.connector - * updated `request` method -* yang.ncdiff - * synced PR2, 3, 5, 8, 13, 14, 19, 21, 24, 25, 26 and 27 from ncdiff repository - * updated NetconfCalculator class - * added diff_type `minimum-replace` - * added `add_attribute_at_depath` method - * added `add_attribute_by_xpath` method - * added `find_by_tags` method - * updated ConfigDelta class - * added `replace_xpath` argument - * added `find_by_tags` diff --git a/ncdiff/docs/changelog/undistributed/changelog_tahigash_ncdiff_sync_enh_20240208171500.rst b/ncdiff/docs/changelog/undistributed/changelog_tahigash_ncdiff_sync_enh_20240208171500.rst new file mode 100644 index 0000000..4def46d --- /dev/null +++ b/ncdiff/docs/changelog/undistributed/changelog_tahigash_ncdiff_sync_enh_20240208171500.rst @@ -0,0 +1,15 @@ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* yang.connector + * updated `request` method +* yang.ncdiff + * synced PR2, 3, 5, 8, 13, 14, 19, 21, 24, 25, 26 and 27 from ncdiff repository + * updated NetconfCalculator class + * added diff_type `minimum-replace` + * added `add_attribute_at_depath` method + * added `add_attribute_by_xpath` method + * added `find_by_tags` method + * updated ConfigDelta class + * added `replace_xpath` argument \ No newline at end of file diff --git a/ncdiff/docs/changelog/undistributed/template.rst b/ncdiff/docs/changelog/undistributed/template.rst new file mode 100644 index 0000000..1761119 --- /dev/null +++ b/ncdiff/docs/changelog/undistributed/template.rst @@ -0,0 +1,34 @@ +Templates +========= + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* + * : + * + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* + * : + * + +Examples +======== + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* Log + * Modified Colours: + * Allowed for rainbow font options + * All output is now rainbow + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* Log + * Added InvertedLogViewer: + * Enables inversion of LogViewer colours