From a1bdf52b0d9b438a7de1012b260c9a6c17d646d9 Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Wed, 5 Nov 2025 17:44:54 -0500 Subject: [PATCH 01/11] Take sub-statements --- src/ncdiff/model.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 8ebc5e4..0a1bfac 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -1186,11 +1186,26 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - n.set( - etree.QName(self.module_namespaces[ch.keyword[0]], - ch.keyword[1]), - ch.arg if ch.arg else '', - ) + if len(ch.substmts) > 0: + sub_sm_dict = { + sub.keyword[1]: sub.arg if sub.arg is not None else '' + for sub in ch.substmts + if ( + isinstance(sub.keyword, tuple) and + 'tailf' in sub.keyword[0] + ) + } + n.set( + etree.QName(self.module_namespaces[ch.keyword[0]], + ch.keyword[1]), + repr(sub_sm_dict) if sub_sm_dict else '', + ) + else: + n.set( + etree.QName(self.module_namespaces[ch.keyword[0]], + ch.keyword[1]), + ch.arg if ch.arg else '', + ) else: logger.warning("Special Tailf annotation at {}, " "keyword = {}" From 131dbaac2d739d8ed65c2e47ba027e07d3a7de20 Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Fri, 21 Nov 2025 15:14:45 -0500 Subject: [PATCH 02/11] Add leafref and TailF ordering constraints in ModelCompiler --- src/ncdiff/composer.py | 12 +- src/ncdiff/model.py | 111 ++++++++--- src/ncdiff/tailf.py | 419 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 509 insertions(+), 33 deletions(-) create mode 100644 src/ncdiff/tailf.py diff --git a/src/ncdiff/composer.py b/src/ncdiff/composer.py index 3053a97..230c2aa 100755 --- a/src/ncdiff/composer.py +++ b/src/ncdiff/composer.py @@ -108,12 +108,9 @@ def model_name(self): ret = re.search(Tag.BRACE[0], self.path[0]) if ret: url_to_name = {i[2]: i[0] for i in self.device.namespaces - if i[1] is not None} + if i[1] is not None and i[2] == ret.group(1)} if ret.group(1) in url_to_name: - raise ModelMissing("please load model '{}' by calling " - "method load_model() of device {}" - .format(url_to_name[ret.group(1)], - self.device)) + return url_to_name[ret.group(1)] else: raise ModelMissing("unknown model url '{}'" .format(ret.group(1))) @@ -123,7 +120,10 @@ def model_name(self): @property def model_ns(self): - return self.device.models[self.model_name].url + name_to_url = {i[0]: i[2] for i in self.device.namespaces + if i[1] is not None and i[0] == self.model_name} + return name_to_url[self.model_name] if self.model_name in name_to_url \ + else None @property def is_config(self): diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 0a1bfac..9ea5779 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -8,7 +8,8 @@ from copy import deepcopy from ncclient import operations from threading import Thread, current_thread -from pyang import statements +from pyang import statements, xpath_parser +from pyang import xpath as xp try: from pyang.repository import FileRepository except ImportError: @@ -19,6 +20,9 @@ from pyang import Context from .errors import ModelError +from .composer import Tag +from .tailf import get_tailf_ordering, add_tailf_annotation +from .tailf import set_ordering_xpath # create a logger for this module @@ -555,6 +559,7 @@ def __init__(self, repository): self.num_threads = 2 else: self.num_threads = 1 + self._modeldevice = None def _get_latest_revision(self, modulename): latest = None @@ -697,6 +702,37 @@ def read_dependencies(self): ) self.dependencies = read_xml(dependencies_file) + def check_xpath_statement(self, xpath_stmt, xpath, node_stmt): + p = xpath_parser.parse(xpath) + if p is None or not isinstance(p, tuple): + return None + if p[0] == 'absolute': + return xp.chk_xpath_path( + self, + xpath_stmt.i_orig_module, + xpath_stmt.pos, + node_stmt, + 'root', + p[1], + ) + elif p[0] == 'relative': + return xp.chk_xpath_path( + self, + xpath_stmt.i_orig_module, + xpath_stmt.pos, + node_stmt, + node_stmt, + p[1], + ) + else: + return None + + def get_xpath_from_schema_node(self, schema_node, type=Tag.XPATH): + if self._modeldevice is None: + return None + else: + return self._modeldevice.get_xpath(schema_node, type=type, instance=False) + def load_context(self): self.modulefile_queue = queue.Queue() for filename in os.listdir(self.repository.dirs[0]): @@ -944,6 +980,10 @@ def __init__(self, folder): self.module_namespaces = {} self.identity_deps = {} self.build_dependencies() + self.ordering_stmt_leafref = {} + self.ordering_stmt_tailf = {} + self.ordering_xpath_leafref = {} + self.ordering_xpath_tailf = {} @property def pyang_errors(self): @@ -1114,6 +1154,9 @@ def compile(self, module): else: self.identity_deps[b_idn].append(curr_idn) + self.ordering_stmt_leafref[module] = {} + self.ordering_stmt_tailf[module] = {} + for child in vm.i_children: if child.keyword in statements.data_definition_keywords: self.depict_a_schema_node(vm, st, child) @@ -1125,6 +1168,7 @@ def compile(self, module): self.depict_a_schema_node(vm, st, child, mode='notification') self._write_to_cache(module, st) + set_ordering_xpath(self, module) return Model(st) @@ -1160,7 +1204,7 @@ def depict_a_schema_node(self, module, parent, child, mode=None): if cases: n.set('values', '|'.join(cases)) elif child.keyword in ['leaf', 'leaf-list']: - self.set_leaf_datatype_value(child, n) + self.set_leaf_datatype_value(module.arg, child, n) sm = child.search_one('mandatory') if ( sm is not None and sm.arg == 'true' or @@ -1178,38 +1222,26 @@ def depict_a_schema_node(self, module, parent, child, mode=None): # Tailf annotations for ch in child.substmts: - if ( - isinstance(ch.keyword, tuple) and - 'tailf' in ch.keyword[0] - ): + if isinstance(ch.keyword, tuple) and 'tailf' in ch.keyword[0]: if ( ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - if len(ch.substmts) > 0: - sub_sm_dict = { - sub.keyword[1]: sub.arg if sub.arg is not None else '' - for sub in ch.substmts - if ( - isinstance(sub.keyword, tuple) and - 'tailf' in sub.keyword[0] - ) - } - n.set( - etree.QName(self.module_namespaces[ch.keyword[0]], - ch.keyword[1]), - repr(sub_sm_dict) if sub_sm_dict else '', - ) + ordering = get_tailf_ordering(ch) + if ordering is None: + add_tailf_annotation(self.module_namespaces, ch, n) else: - n.set( - etree.QName(self.module_namespaces[ch.keyword[0]], - ch.keyword[1]), - ch.arg if ch.arg else '', - ) + target = self.context.check_xpath_statement( + ch, ch.arg, child) + if target is not None: + self.ordering_stmt_tailf[module.arg][ + (child, target)] = (ordering, ch.pos) else: - logger.warning("Special Tailf annotation at {}, " + logger.warning("Unknown Tailf annotation at {}, " "keyword = {}" .format(ch.pos, ch.keyword)) + if not hasattr(child, 'schema_node'): + child.schema_node = n featurenames = [f.arg for f in child.search('if-feature')] if hasattr(child, 'i_augment'): @@ -1227,6 +1259,14 @@ def depict_a_schema_node(self, module, parent, child, mode=None): else: self.depict_a_schema_node(module, n, c, mode=mode) + def get_xpath_from_schema_node(self, schema_node, type=Tag.XPATH): + from .manager import ModelDevice + + if self.context._modeldevice is None: + self.context._modeldevice = ModelDevice(None, None) + self.context._modeldevice.compiler = self + return self.context.get_xpath_from_schema_node(schema_node, type=type) + @staticmethod def set_access(statement, node, mode): if ( @@ -1245,7 +1285,7 @@ def set_access(statement, node, mode): else: node.set('access', 'read-only') - def set_leaf_datatype_value(self, leaf_statement, leaf_node): + def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): sm = leaf_statement.search_one('type') if sm is None: datatype = '' @@ -1253,6 +1293,23 @@ def set_leaf_datatype_value(self, leaf_statement, leaf_node): if sm.arg == 'leafref': p = sm.search_one('path') if p is not None: + + # Consider leafref as a dpendency for ordering purpose + target_stmt = self.context.check_xpath_statement( + p, p.arg, leaf_statement) + if target_stmt is not None: + self.ordering_stmt_leafref[module][ + (leaf_statement, target_stmt) + ] = ({ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('create', 'after', 'modify'), + ('modify', 'after', 'modify'), + ('delete', 'before', 'modify'), + ('modify', 'before', 'delete'), + ('delete', 'before', 'delete'), + }, p.pos) + # Try to make the path as compact as possible. # Remove local prefixes, and only use prefix when # there is a module change in the path. diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py new file mode 100644 index 0000000..42f8047 --- /dev/null +++ b/src/ncdiff/tailf.py @@ -0,0 +1,419 @@ +import json +import logging +from os import path +from lxml import etree + +from .composer import Tag + +logger = logging.getLogger(__name__) + + +def get_tailf_ordering(stmt): + if 'tailf' not in stmt.keyword[0]: + return None + if stmt.keyword[1] in ['cli-diff-after', 'cli-diff-before']: + conj = 'after' if stmt.keyword[1] == 'cli-diff-after' else 'before' + valid_substmts = { + 'cli-when-target-set', + 'cli-when-target-create', + 'cli-when-target-modify', + 'cli-when-target-delete', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + if len(substmts) == 0: + return { + ('create', conj, 'create'), + ('modify', conj, 'create'), + ('delete', conj, 'create'), + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + ('delete', conj, 'modify'), + ('create', conj, 'delete'), + ('modify', conj, 'delete'), + ('delete', conj, 'delete'), + } + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-when-target-set': + ordering.update({ + ('create', conj, 'create'), + ('modify', conj, 'create'), + ('delete', conj, 'create'), + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + ('delete', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-create': + ordering.update({ + ('create', conj, 'create'), + ('modify', conj, 'create'), + ('delete', conj, 'create'), + }) + elif substmt.keyword[1] == 'cli-when-target-modify': + ordering.update({ + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + ('delete', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-delete': + ordering.update({ + ('create', conj, 'delete'), + ('modify', conj, 'delete'), + ('delete', conj, 'delete'), + }) + return ordering + elif stmt.keyword[1] in ['cli-diff-create-after', 'cli-diff-create-before']: + conj = 'after' if stmt.keyword[1] == 'cli-diff-create-after' else 'before' + valid_substmts = { + 'cli-when-target-set', + 'cli-when-target-create', + 'cli-when-target-modify', + 'cli-when-target-delete', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + if len(substmts) == 0: + return { + ('create', conj, 'create'), + ('create', conj, 'modify'), + ('create', conj, 'delete'), + } + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-when-target-set': + ordering.update({ + ('create', conj, 'create'), + ('create', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-create': + ordering.add( + ('create', conj, 'create'), + ) + elif substmt.keyword[1] == 'cli-when-target-modify': + ordering.add( + ('create', conj, 'modify'), + ) + elif substmt.keyword[1] == 'cli-when-target-delete': + ordering.add( + ('create', conj, 'delete'), + ) + return ordering + elif stmt.keyword[1] in ['cli-diff-delete-after', 'cli-diff-delete-before']: + conj = 'after' if stmt.keyword[1] == 'cli-diff-delete-after' else 'before' + valid_substmts = { + 'cli-when-target-set', + 'cli-when-target-create', + 'cli-when-target-modify', + 'cli-when-target-delete', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + if len(substmts) == 0: + return { + ('delete', conj, 'create'), + ('delete', conj, 'modify'), + ('delete', conj, 'delete'), + } + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-when-target-set': + ordering.update({ + ('delete', conj, 'create'), + ('delete', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-create': + ordering.add( + ('delete', conj, 'create'), + ) + elif substmt.keyword[1] == 'cli-when-target-modify': + ordering.add( + ('delete', conj, 'modify'), + ) + elif substmt.keyword[1] == 'cli-when-target-delete': + ordering.add( + ('delete', conj, 'delete'), + ) + return ordering + elif stmt.keyword[1] in ['cli-diff-modify-after', 'cli-diff-modify-before']: + conj = 'after' if stmt.keyword[1] == 'cli-diff-modify-after' else 'before' + valid_substmts = { + 'cli-when-target-set', + 'cli-when-target-create', + 'cli-when-target-modify', + 'cli-when-target-delete', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + if len(substmts) == 0: + return { + ('modify', conj, 'create'), + ('modify', conj, 'modify'), + ('modify', conj, 'delete'), + } + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-when-target-set': + ordering.update({ + ('modify', conj, 'create'), + ('modify', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-create': + ordering.add( + ('modify', conj, 'create'), + ) + elif substmt.keyword[1] == 'cli-when-target-modify': + ordering.add( + ('modify', conj, 'modify'), + ) + elif substmt.keyword[1] == 'cli-when-target-delete': + ordering.add( + ('modify', conj, 'delete'), + ) + return ordering + elif stmt.keyword[1] in ['cli-diff-set-after', 'cli-diff-set-before']: + conj = 'after' if stmt.keyword[1] == 'cli-diff-set-after' else 'before' + valid_substmts = { + 'cli-when-target-set', + 'cli-when-target-create', + 'cli-when-target-modify', + 'cli-when-target-delete', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + if len(substmts) == 0: + return { + ('create', conj, 'create'), + ('modify', conj, 'create'), + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + ('create', conj, 'delete'), + ('modify', conj, 'delete'), + } + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-when-target-set': + ordering.update({ + ('create', conj, 'create'), + ('modify', conj, 'create'), + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-create': + ordering.update({ + ('create', conj, 'create'), + ('modify', conj, 'create'), + }) + elif substmt.keyword[1] == 'cli-when-target-modify': + ordering.update({ + ('create', conj, 'modify'), + ('modify', conj, 'modify'), + }) + elif substmt.keyword[1] == 'cli-when-target-delete': + ordering.update({ + ('create', conj, 'delete'), + ('modify', conj, 'delete'), + }) + return ordering + elif stmt.keyword[1] == 'cli-diff-dependency': + valid_substmts = { + 'cli-trigger-on-set', + 'cli-trigger-on-delete', + 'cli-trigger-on-all', + } + substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] + ordering = { + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('delete', 'before', 'modify'), + ('create', 'before', 'delete'), + ('modify', 'before', 'delete'), + ('delete', 'before', 'delete'), + } + # Test result from TailF confd 8.4.7.1: + # 1 depends on 2 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'before', 'create'), + # ('create', 'before', 'modify'), + # ('modify', 'before', 'modify'), + # ('delete', 'before', 'modify'), + # ('create', 'before', 'delete'), + # ('modify', 'before', 'delete'), + # ('delete', 'before', 'delete'), + # 2 depends on 1 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'after', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'before', 'modify'), + # ('create', 'before', 'delete'), + # ('modify', 'before', 'delete'), + # ('delete', 'before', 'delete'), + if len(substmts) == 0: + return ordering + ordering = set() + for substmt in substmts: + if substmt.keyword[1] == 'cli-trigger-on-set': + ordering.update({ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('create', 'after', 'modify'), + ('modify', 'after', 'modify'), + }) + # Test result from TailF confd 8.4.7.1: + # 1 depends on 2 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'before', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'before', 'modify'), + # ('create', 'after', 'delete'), + # ('modify', 'after', 'delete'), + # ('delete', 'before', 'delete'), + # 2 depends on 1 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'after', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'after', 'modify'), + # ('create', 'after', 'delete'), + # ('modify', 'after', 'delete'), + # ('delete', 'after', 'delete'), + elif substmt.keyword[1] == 'cli-trigger-on-delete': + ordering.update({ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('delete', 'before', 'modify'), + ('delete', 'before', 'delete'), + }) + # Test result from TailF confd 8.4.7.1: + # 1 depends on 2 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'before', 'create'), + # ('create', 'before', 'modify'), + # ('modify', 'before', 'modify'), + # ('delete', 'before', 'modify'), + # ('create', 'before', 'delete'), + # ('modify', 'before', 'delete'), + # ('delete', 'before', 'delete'), + # 2 depends on 1 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'after', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'before', 'modify'), + # ('create', 'before', 'delete'), + # ('modify', 'before', 'delete'), + # ('delete', 'before', 'delete'), + elif substmt.keyword[1] == 'cli-trigger-on-all': + return { + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('delete', 'after', 'create'), + ('create', 'after', 'modify'), + ('modify', 'after', 'modify'), + ('delete', 'after', 'modify'), + ('create', 'after', 'delete'), + ('modify', 'after', 'delete'), + ('delete', 'after', 'delete'), + } + # Test result from TailF confd 8.4.7.1: + # 1 depends on 2 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'after', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'after', 'modify'), + # ('create', 'after', 'delete'), + # ('modify', 'after', 'delete'), + # ('delete', 'after', 'delete'), + # 2 depends on 1 + # ('create', 'after', 'create'), + # ('modify', 'after', 'create'), + # ('delete', 'after', 'create'), + # ('create', 'after', 'modify'), + # ('modify', 'after', 'modify'), + # ('delete', 'after', 'modify'), + # ('create', 'after', 'delete'), + # ('modify', 'after', 'delete'), + # ('delete', 'after', 'delete'), + return ordering + return None + +def add_tailf_annotation(module_namespaces, stmt, node): + if len(stmt.substmts) > 0: + sub_sm_dict = { + sub.keyword[1]: sub.arg if sub.arg is not None else '' + for sub in stmt.substmts + if ( + isinstance(sub.keyword, tuple) and + 'tailf' in sub.keyword[0] + ) + } + node.set( + etree.QName(module_namespaces[stmt.keyword[0]], + stmt.keyword[1]), + repr(sub_sm_dict) if sub_sm_dict else '', + ) + else: + node.set( + etree.QName(module_namespaces[stmt.keyword[0]], + stmt.keyword[1]), + stmt.arg if stmt.arg else '', + ) + + +def set_ordering_xpath(compiler, module): + for constraint_type in ["ordering_stmt_leafref", "ordering_stmt_tailf"]: + if ( + hasattr(compiler, constraint_type) and + module in getattr(compiler, constraint_type) + ): + constraints = write_ordering_xpath( + compiler, module, constraint_type) + + +def write_ordering_xpath(compiler, module, constraint_type): + + def get_xpath(compiler, stmt): + schema_node = getattr(stmt, 'schema_node', None) + if schema_node is None: + return '' + if not hasattr(stmt, 'schema_xpath'): + stmt.schema_xpath = compiler.get_xpath_from_schema_node( + schema_node, type=Tag.LXML_XPATH) + return stmt.schema_xpath + + constraints = [] + stmt = {} + xpath = {} + constraint_info = getattr(compiler, constraint_type)[module] + for stmt[0], stmt[1] in constraint_info: + for i in range(2): + xpath[i] = get_xpath(compiler, stmt[i]) + if xpath[0] == '' or xpath[1] == '': + # Skip entries with missing Xpath. Missing Xpaths might be in a + # different module not compiled or due to other deviations. + continue + cinstraint_list, pos = constraint_info[(stmt[0], stmt[1])] + for oper_0, sequence, oper_1 in cinstraint_list: + if sequence == 'before': + constraints.append(( + f"{xpath[0]}, {oper_0}", f"{xpath[1]}, {oper_1}", pos)) + else: + constraints.append(( + f"{xpath[1]}, {oper_1}", f"{xpath[0]}, {oper_0}", pos)) + + attribute_name = "ordering_xpath_leafref" \ + if constraint_type == "ordering_stmt_leafref" \ + else "ordering_xpath_tailf" + getattr(compiler, attribute_name)[module] = constraints + if len(constraints) > 0: + csv_filename = path.join( + compiler.dir_yang, f'{module}_{attribute_name}.csv') + with open(csv_filename, 'w') as f: + f.write("\n".join([f"{c[0]}, {c[1]}" for c in constraints])) + + return [(c[0], c[1]) for c in constraints] From 8c4d756885ac6b104e8a8f547d3d042402eeb7f5 Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Thu, 8 Jan 2026 11:27:07 -0500 Subject: [PATCH 03/11] Process tailf:annotate, tailf:annotate-module and tailf:annotate-statement --- src/ncdiff/model.py | 443 ++++++++++++++++++++++++++++++++++++++------ src/ncdiff/ref.py | 2 +- src/ncdiff/tailf.py | 108 +++++++++-- 3 files changed, 478 insertions(+), 75 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 9ea5779..886d417 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -8,7 +8,7 @@ from copy import deepcopy from ncclient import operations from threading import Thread, current_thread -from pyang import statements, xpath_parser +from pyang import statements, xpath_parser, syntax, util from pyang import xpath as xp try: from pyang.repository import FileRepository @@ -21,8 +21,8 @@ from .errors import ModelError from .composer import Tag -from .tailf import get_tailf_ordering, add_tailf_annotation -from .tailf import set_ordering_xpath +from .tailf import has_tailf_ordering, get_tailf_ordering +from .tailf import add_tailf_annotation, set_ordering_xpath # create a logger for this module @@ -31,7 +31,11 @@ logging.getLogger('ncclient.operations').setLevel(logging.WARNING) PARSER = etree.XMLParser(encoding='utf-8', remove_blank_text=True) - +PREFIX = syntax.prefix +IDENTIFIER = PREFIX + r'|\*' +KEYWORD = '((' + PREFIX + '):)?(' + IDENTIFIER + ')' +RE_SCHEMA_NODE_ID_PART = re.compile('/' + KEYWORD) +RE_ANNOTATE_STATEMENT = re.compile(r'^(.+)\[name=[\'|"](.+)[\'|"]\]') def write_xml(filename, element): element_tree = etree.ElementTree(element) @@ -702,30 +706,134 @@ def read_dependencies(self): ) self.dependencies = read_xml(dependencies_file) - def check_xpath_statement(self, xpath_stmt, xpath, node_stmt): - p = xpath_parser.parse(xpath) - if p is None or not isinstance(p, tuple): + def check_data_tree_xpath(self, xpath_stmt, node_stmt): + if not hasattr(xpath_stmt, 'i_orig_module'): + logger.warning(f"Statement at {xpath_stmt.pos} does not have " + "attribute 'i_orig_module'") return None - if p[0] == 'absolute': - return xp.chk_xpath_path( - self, - xpath_stmt.i_orig_module, - xpath_stmt.pos, - node_stmt, - 'root', - p[1], - ) - elif p[0] == 'relative': - return xp.chk_xpath_path( + + p = xpath_parser.parse(xpath_stmt.arg) + if isinstance(p, list): + node = xp.chk_xpath_path( self, xpath_stmt.i_orig_module, xpath_stmt.pos, node_stmt, node_stmt, - p[1], + p, ) + elif isinstance(p, tuple): + if p[0] == 'absolute': + node = xp.chk_xpath_path( + self, + xpath_stmt.i_orig_module, + xpath_stmt.pos, + node_stmt, + 'root', + p[1], + ) + elif p[0] == 'relative': + node = xp.chk_xpath_path( + self, + xpath_stmt.i_orig_module, + xpath_stmt.pos, + node_stmt, + node_stmt, + p[1], + ) + else: + logger.warning(f"Failed to understand Xpath '{xpath_stmt.arg}' " + f"in data tree at {xpath_stmt.pos}") + return None else: + logger.warning(f"Failed to parse Xpath '{xpath_stmt.arg}' in data " + f"tree at {xpath_stmt.pos}") return None + if node is None: + logger.warning(f"Failed to find annotated statement by the Xpath " + f"'{xpath_stmt.arg}' in data tree at " + f"{xpath_stmt.pos}") + else: + xpath_stmt.i_annotate_node = node + return node + + def check_schema_tree_xpath(self, xpath_stmt): + if xpath_stmt.arg.startswith('/'): + is_absolute = True + arg = xpath_stmt.arg + else: + is_absolute = False + arg = "/" + xpath_stmt.arg + + # Parse the path into a list of two-tuples of (prefix, identifier) + path = [(m[1], m[2]) for m in RE_SCHEMA_NODE_ID_PART.findall(arg)] + + # Find the module of the first node in the path + if not isinstance(path, list) or len(path) == 0: + logger.warning(f"Failed to parse Xpath {xpath_stmt.arg} in schema " + f"tree at {xpath_stmt.pos}") + return None + (prefix, identifier) = path[0] + module = util.prefix_to_module( + xpath_stmt.i_module, prefix, xpath_stmt.pos, self.errors) + if module is None: + logger.warning(f"Failed to find a module by the prefix {prefix} " + f"at {xpath_stmt.pos}") + return None + if is_absolute: + node = statements.search_data_keyword_child(module.i_children, + module.i_modulename, + identifier) + if node is None: + # Check all our submodules + for inc in module.search('include'): + submod = self.get_module(inc.arg) + if submod is not None: + node = statements.search_data_keyword_child( + submod.i_children, + submod.i_modulename, + identifier) + if node is not None: + break + if node is None: + logger.warning("Failed to find annotated statement by the " + f"identifier {prefix}:{identifier} at " + f"{xpath_stmt.pos}") + return None + path = path[1:] + else: + if hasattr(xpath_stmt.parent, 'i_annotate_node'): + node = xpath_stmt.parent.i_annotate_node + else: + logger.warning("Parent statement does not have a resolved " + f"target: {xpath_stmt.pos}") + return None + + # Recurse down the path + for prefix, identifier in path: + if hasattr(node, 'i_children'): + children = node.i_children + else: + children = [] + if prefix == '' and identifier == '*': + return children + module = util.prefix_to_module( + xpath_stmt.i_module, prefix, xpath_stmt.pos, self.errors) + if module is None: + logger.warning("Failed to find a module by the prefix " + f"{prefix}: {xpath_stmt.pos}") + return None + child = statements.search_data_keyword_child(children, + module.i_modulename, + identifier) + if child is None: + logger.warning("Failed to find annotated statement by the " + f"identifier {prefix}:{identifier} at " + f"{xpath_stmt.pos}") + return None + node = child + xpath_stmt.i_annotate_node = node + return node def get_xpath_from_schema_node(self, schema_node, type=Tag.XPATH): if self._modeldevice is None: @@ -747,6 +855,153 @@ def load_context(self): self.modulefile_queue.join() self.write_dependencies() + def process_annotation_module(self, preprocessing=True): + + def tailf_annotate(context, annotating_stmt): + target = context.check_schema_tree_xpath(annotating_stmt) + if target is not None: + for annitating_substmt in annotating_stmt.substmts: + if annitating_substmt.keyword == ( + 'tailf-common', + 'annotate', + ): + tailf_annotate(context, annitating_substmt) + else: + if isinstance(target, list): + for t in target: + append_annotation(t, annitating_substmt) + else: + append_annotation(target, annitating_substmt) + + def tailf_annotate_module(context, module_stmt): + for substmt in module_stmt.substmts: + if substmt.keyword == ('tailf', 'annotate-module'): + annotated_module = context.get_module(substmt.arg) + if annotated_module is None: + logger.warning("Failed to find annotated module " + f"{substmt.arg} at {substmt.pos}") + continue + substmt.i_annotate_node = annotated_module + for annotating_substmt in substmt.substmts: + if isinstance(substmt.raw_keyword, tuple): + prefix, identifier = annotating_substmt.raw_keyword + m, rev = util.prefix_to_modulename_and_revision( + annotating_substmt.i_module, + prefix, + annotating_substmt.pos, + context.errors, + ) + if ( + m == 'tailf-common' and + identifier == 'annotate-statement' + ): + tailf_annotate_statement( + context, annotating_substmt) + else: + append_annotation( + annotated_module, annotating_substmt) + else: + append_annotation( + annotated_module, annotating_substmt) + + def tailf_annotate_statement(context, annotating_stmt): + annotated_stmt = annotating_stmt.parent.i_annotate_node + match = re.match(RE_ANNOTATE_STATEMENT, annotating_stmt.arg) + if match: + matched_stmts = [ + s for s in annotated_stmt.substmts + if s.keyword == match.group(1) and s.arg == match.group(2) + ] + if len(matched_stmts) == 0: + logger.warning("Annotating statement at " + f"{annotating_stmt.pos}: Failed to find a " + f"matching sub-statement '{match.group(1)} " + f"{match.group(2)}' under the annotated " + f"statement at {annotated_stmt.pos}") + return + elif len(matched_stmts) > 1: + logger.warning("Annotating statement at " + f"{annotating_stmt.pos}: Found more than " + "one matching sub-statement " + f"'{match.group(1)} {match.group(2)}' " + "under the annotated statement at " + f"{annotated_stmt.pos}") + return + elif annotating_stmt.arg == 'type': + matched_stmts = [ + s for s in annotated_stmt.substmts + if s.keyword == 'type' + ] + if len(matched_stmts) == 0: + logger.warning("Annotating statement at " + f"{annotating_stmt.pos}: 'type' not found " + "under the annotated statement at " + f"{annotated_stmt.pos}") + return + elif len(matched_stmts) > 1: + logger.warning("Annotating statement at " + f"{annotating_stmt.pos}: found more than " + "one 'type' under the annotated statement " + f"at {annotated_stmt.pos}") + return + else: + logger.warning("Annotating statement at " + f"{annotating_stmt.pos}: Invalid arg " + f"{annotating_stmt.arg}") + return + + annotating_stmt.i_annotate_node = matched_stmts[0] + for substmt in annotating_stmt.substmts: + if isinstance(substmt.raw_keyword, tuple): + annotate_statement = False + prefix, identifier = substmt.raw_keyword + m, rev = util.prefix_to_modulename_and_revision( + substmt.i_module, + prefix, + substmt.pos, + context.errors, + ) + if ( + m == 'tailf-common' and + identifier == 'annotate-statement' + ): + tailf_annotate_statement(context, substmt) + else: + append_annotation(matched_stmts[0], substmt) + else: + append_annotation(matched_stmts[0], substmt) + + def append_annotation(target_stmt, annotation_stmt): + new_stmt = statements.new_statement( + annotation_stmt.top, + target_stmt, + annotation_stmt.pos, + annotation_stmt.keyword, + annotation_stmt.arg, + ) + new_stmt.raw_keyword = annotation_stmt.raw_keyword + new_stmt.i_orig_module = annotation_stmt.top + if hasattr(target_stmt, 'i_module'): + new_stmt.i_module = target_stmt.i_module + target_stmt.substmts.append(new_stmt) + for substmt in annotation_stmt.substmts: + append_annotation(new_stmt, substmt) + + mudule_names = [k[0] for k in self.modules] + for mudule_name in mudule_names: + if mudule_name.endswith('-ann'): + module_statement = self.get_module(mudule_name) + if module_statement is None: + logger.warning(f"Failed to find annotation module {mudule_name}") + elif preprocessing: + tailf_annotate_module(self, module_statement) + logger.debug(f"Pre-processed tailf:annotate-module in {mudule_name}") + else: + for substmt in module_statement.substmts: + if substmt.keyword == ('tailf-common', 'annotate'): + tailf_annotate(self, substmt) + logger.debug(f"Post-processed tailf:annotate in {mudule_name}") + def validate_context(self): revisions = {} for mudule_name, module_revision in self.modules: @@ -756,17 +1011,36 @@ def validate_context(self): ): revisions[mudule_name] = module_revision self.sort_modules() + + # Initialize annotation modules + annotation_modules = [m for k, m in self.modules.items() + if k[0].endswith("-ann")] + for m in annotation_modules: + statements.v_init_module(self, m) + + # Process annotation modules as a pre-processing step + self.process_annotation_module(preprocessing=True) + self.validate() if 'prune' in dir(statements.Statement): for mudule_name, module_revision in revisions.items(): self.modules[(mudule_name, module_revision)].prune() + # Process annotation modules as a post-processing step + self.process_annotation_module(preprocessing=False) + def sort_modules(self): - submodules = {k: m for k, m in self.modules.items() - if m.keyword == "submodule"} - for k in submodules: - del self.modules[k] - self.modules.update(submodules) + modulename_revision = {k[0]: k for k in self.modules.keys()} + submodules = sorted([ + k[0] for k, m in self.modules.items() if m.keyword == "submodule" + ]) + modules = sorted([ + k for k in modulename_revision if k not in submodules + ]) + self.modules = { + modulename_revision[k]: self.modules[modulename_revision[k]] + for k in modules + submodules + } def internal_reset(self): self.modules = {} @@ -984,6 +1258,8 @@ def __init__(self, folder): self.ordering_stmt_tailf = {} self.ordering_xpath_leafref = {} self.ordering_xpath_tailf = {} + self._ordering_without_obsolete = True + self._ordering_without_deprecated = False @property def pyang_errors(self): @@ -1036,27 +1312,39 @@ def get_dependencies(self, module): ------- tuple - A tuple with two elements: a set of imports and a set of depends. + A tuple with three elements: a set of imports, a set of includes + and a set of other depends. ''' + def find_all_depends(depends, dependencies): + depends_copy = set(depends) + for m in dependencies: + if ( + list(filter(lambda i: i.get('module') in depends, + m.findall('./imports/import'))) or + list(filter(lambda i: i.get('module') in depends, + m.findall('./includes/include'))) + ): + depends.add(m.get('id')) + return depends_copy != depends + if self.context is None or self.context.dependencies is None: self.build_dependencies() dependencies = self.context.dependencies imports = set() + includes = set() for m in list(filter(lambda i: i.get('id') == module, dependencies.findall('./module'))): imports.update(set(i.get('module') for i in m.findall('./imports/import'))) - depends = set() - for m in dependencies: - if list(filter(lambda i: i.get('module') == module, - m.findall('./imports/import'))): - depends.add(m.get('id')) - if list(filter(lambda i: i.get('module') == module, - m.findall('./includes/include'))): - depends.add(m.get('id')) - return (imports, depends) + includes.update(set(i.get('module') + for i in m.findall('./includes/include'))) + + depends = imports | includes + while find_all_depends(depends, dependencies): + pass + return (imports, includes, depends - imports - includes) def compile(self, module): '''compile @@ -1081,8 +1369,8 @@ def compile(self, module): return Model(cached_tree) varnames = Context.add_module.__code__.co_varnames - imports, depends = self.get_dependencies(module) - required_module_set = imports | depends + imports, includes, depends = self.get_dependencies(module) + required_module_set = imports | includes | depends required_module_set.add(module) self.context.internal_reset() for m in required_module_set: @@ -1227,15 +1515,30 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - ordering = get_tailf_ordering(ch) - if ordering is None: - add_tailf_annotation(self.module_namespaces, ch, n) - else: - target = self.context.check_xpath_statement( - ch, ch.arg, child) - if target is not None: - self.ordering_stmt_tailf[module.arg][ - (child, target)] = (ordering, ch.pos) + status = self.obsolete_or_deprecated(child) + if not ( + status == 'obsolete' and + self._ordering_without_obsolete or + status == 'deprecated' and + self._ordering_without_deprecated + ): + if not has_tailf_ordering(ch, self.context): + add_tailf_annotation(self.module_namespaces, ch, n) + else: + target = self.context.check_data_tree_xpath( + ch, child) + if target is not None: + status = self.obsolete_or_deprecated(target) + if not ( + status == 'obsolete' and + self._ordering_without_obsolete or + status == 'deprecated' and + self._ordering_without_deprecated + ): + ordering = get_tailf_ordering( + self.context, ch, target) + self.ordering_stmt_tailf[module.arg][ + (child, target)] = (ordering, ch.pos) else: logger.warning("Unknown Tailf annotation at {}, " "keyword = {}" @@ -1295,20 +1598,33 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): if p is not None: # Consider leafref as a dpendency for ordering purpose - target_stmt = self.context.check_xpath_statement( - p, p.arg, leaf_statement) - if target_stmt is not None: - self.ordering_stmt_leafref[module][ - (leaf_statement, target_stmt) - ] = ({ - ('create', 'after', 'create'), - ('modify', 'after', 'create'), - ('create', 'after', 'modify'), - ('modify', 'after', 'modify'), - ('delete', 'before', 'modify'), - ('modify', 'before', 'delete'), - ('delete', 'before', 'delete'), - }, p.pos) + status = self.obsolete_or_deprecated(leaf_statement) + if not ( + status == 'obsolete' and + self._ordering_without_obsolete or + status == 'deprecated' and + self._ordering_without_deprecated + ): + target_stmt = self.context.check_data_tree_xpath( + p, leaf_statement) + if target_stmt is not None: + status = self.obsolete_or_deprecated(target_stmt) + if not ( + status == 'obsolete' and + self._ordering_without_obsolete or + status == 'deprecated' and + self._ordering_without_deprecated + ): + self.ordering_stmt_leafref[module][ + (leaf_statement, target_stmt) + ] = ({ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('create', 'after', 'modify'), + ('delete', 'before', 'modify'), + ('modify', 'before', 'delete'), + ('delete', 'before', 'delete'), + }, p.pos) # Try to make the path as compact as possible. # Remove local prefixes, and only use prefix when @@ -1396,6 +1712,15 @@ def type_identityref_values(self, type_statement): return '|'.join(value_stmts) return '' + @staticmethod + def obsolete_or_deprecated(statement): + while statement is not None: + sm = statement.search_one('status') + if sm is not None and sm.arg in ['obsolete', 'deprecated']: + return sm.arg + statement = statement.parent + return None + class ModelDiff(object): '''ModelDiff diff --git a/src/ncdiff/ref.py b/src/ncdiff/ref.py index f992b0e..fad2cc2 100755 --- a/src/ncdiff/ref.py +++ b/src/ncdiff/ref.py @@ -362,7 +362,7 @@ def parse_square_bracket(self, to_node=None): self.cut(start_idx+1, end_idx-2, tag, 2) start_idx = None else: - if re.search('^\[[1-9][0-9]*\]$', substring): + if re.search(r'^\[[1-9][0-9]*\]$', substring): numbers = substring[1:-1] self.cut(start_idx+1, end_idx-1, numbers, 2) else: diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index 42f8047..bcbc1e8 100644 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -1,16 +1,33 @@ -import json import logging from os import path from lxml import etree +from pyang import util from .composer import Tag logger = logging.getLogger(__name__) -def get_tailf_ordering(stmt): - if 'tailf' not in stmt.keyword[0]: - return None +def has_tailf_ordering(stmt, context): + prefix, identifier = stmt.raw_keyword + m, rev = util.prefix_to_modulename_and_revision( + stmt.i_orig_module, + prefix, + stmt.pos, + context.errors, + ) + return m == 'tailf-common' and identifier in { + 'cli-diff-after', 'cli-diff-before', + 'cli-diff-create-after', 'cli-diff-create-before', + 'cli-diff-delete-after', 'cli-diff-delete-before', + 'cli-diff-modify-after', 'cli-diff-modify-before', + 'cli-diff-set-after', 'cli-diff-set-before', + 'cli-diff-dependency', + } + + +def get_tailf_ordering(context, stmt, target_stmt): + symmetric = is_symmetric_tailf_ordering(context, stmt, target_stmt) if stmt.keyword[1] in ['cli-diff-after', 'cli-diff-before']: conj = 'after' if stmt.keyword[1] == 'cli-diff-after' else 'before' valid_substmts = { @@ -72,11 +89,17 @@ def get_tailf_ordering(stmt): } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: - return { - ('create', conj, 'create'), - ('create', conj, 'modify'), - ('create', conj, 'delete'), - } + if symmetric: + return { + ('create', conj, 'modify'), + ('create', conj, 'delete'), + } + else: + return { + ('create', conj, 'create'), + ('create', conj, 'modify'), + ('create', conj, 'delete'), + } ordering = set() for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': @@ -107,11 +130,17 @@ def get_tailf_ordering(stmt): } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: - return { - ('delete', conj, 'create'), - ('delete', conj, 'modify'), - ('delete', conj, 'delete'), - } + if symmetric: + return { + ('delete', conj, 'create'), + ('delete', conj, 'modify'), + } + else: + return { + ('delete', conj, 'create'), + ('delete', conj, 'modify'), + ('delete', conj, 'delete'), + } ordering = set() for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': @@ -340,7 +369,7 @@ def get_tailf_ordering(stmt): # ('modify', 'after', 'delete'), # ('delete', 'after', 'delete'), return ordering - return None + return set() def add_tailf_annotation(module_namespaces, stmt, node): if len(stmt.substmts) > 0: @@ -399,12 +428,16 @@ def get_xpath(compiler, stmt): continue cinstraint_list, pos = constraint_info[(stmt[0], stmt[1])] for oper_0, sequence, oper_1 in cinstraint_list: + if xpath[0] == xpath[1] and oper_0 == oper_1: + # Skip entries with same Xpath and same operation. + continue if sequence == 'before': constraints.append(( f"{xpath[0]}, {oper_0}", f"{xpath[1]}, {oper_1}", pos)) else: constraints.append(( f"{xpath[1]}, {oper_1}", f"{xpath[0]}, {oper_0}", pos)) + update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) attribute_name = "ordering_xpath_leafref" \ if constraint_type == "ordering_stmt_leafref" \ @@ -417,3 +450,48 @@ def get_xpath(compiler, stmt): f.write("\n".join([f"{c[0]}, {c[1]}" for c in constraints])) return [(c[0], c[1]) for c in constraints] + + +def update_schema_tree(stmt_0, oper_0, stmt_1, oper_1): + schema_node_1 = getattr(stmt_0, 'schema_node', None) + if schema_node_1 is None: + logger.warning( + f"Schema node not found for statement {stmt_0.keyword} " + f"at {stmt_0.pos}") + return + ordering_str = schema_node_1.get("before") + if ordering_str is None: + schema_node_1.set("before", repr({ + oper_0: {stmt_1.schema_xpath: [oper_1]} + })) + else: + ordering = eval(ordering_str) + if oper_0 in ordering: + if stmt_1.schema_xpath in ordering[oper_0]: + if oper_1 in ordering[oper_0][stmt_1.schema_xpath]: + return + else: + ordering[oper_0][stmt_1.schema_xpath].append(oper_1) + else: + ordering[oper_0][stmt_1.schema_xpath] = [oper_1] + else: + ordering[oper_0] = {stmt_1.schema_xpath: [oper_1]} + schema_node_1.set("before", repr(ordering)) + + +def is_symmetric_tailf_ordering(context, stmt, target_stmt): + if len(stmt.substmts) != 0: + return False + substmts = { + s for s in target_stmt.substmts + if isinstance(s.keyword, tuple) and + 'tailf' in s.keyword[0] and + len(s.substmts) == 0 and + s.keyword[1] == stmt.keyword[1] + } + for substmt in substmts: + target = context.check_data_tree_xpath( + substmt, target_stmt) + if target == stmt.parent: + return True + return False From 34b28fd621ec93d8f0f8901f78e41606345ef773 Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Fri, 9 Jan 2026 11:40:03 -0500 Subject: [PATCH 04/11] Fix wrong schema tree --- src/ncdiff/tailf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index bcbc1e8..c25a0b3 100644 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -434,10 +434,11 @@ def get_xpath(compiler, stmt): if sequence == 'before': constraints.append(( f"{xpath[0]}, {oper_0}", f"{xpath[1]}, {oper_1}", pos)) + update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) else: constraints.append(( f"{xpath[1]}, {oper_1}", f"{xpath[0]}, {oper_0}", pos)) - update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) + update_schema_tree(stmt[1], oper_1, stmt[0], oper_0) attribute_name = "ordering_xpath_leafref" \ if constraint_type == "ordering_stmt_leafref" \ From 000bfbfbf2f992ce23a43b7b6f6d1e111d69f32f Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Wed, 14 Jan 2026 11:49:56 -0500 Subject: [PATCH 05/11] Fix an issue when multiple annotations present --- src/ncdiff/model.py | 28 +++++--- src/ncdiff/tailf.py | 171 ++++++++++++++++++++++---------------------- 2 files changed, 102 insertions(+), 97 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 886d417..d21380e 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -1537,8 +1537,11 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ): ordering = get_tailf_ordering( self.context, ch, target) - self.ordering_stmt_tailf[module.arg][ - (child, target)] = (ordering, ch.pos) + self.ordering_stmt_tailf[module.arg][ch] = ( + child, + target, + ordering, + ) else: logger.warning("Unknown Tailf annotation at {}, " "keyword = {}" @@ -1616,15 +1619,18 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): self._ordering_without_deprecated ): self.ordering_stmt_leafref[module][ - (leaf_statement, target_stmt) - ] = ({ - ('create', 'after', 'create'), - ('modify', 'after', 'create'), - ('create', 'after', 'modify'), - ('delete', 'before', 'modify'), - ('modify', 'before', 'delete'), - ('delete', 'before', 'delete'), - }, p.pos) + leaf_statement] = ( + leaf_statement, + target_stmt, + [ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('create', 'after', 'modify'), + ('delete', 'before', 'modify'), + ('modify', 'before', 'delete'), + ('delete', 'before', 'delete'), + ], + ) # Try to make the path as compact as possible. # Remove local prefixes, and only use prefix when diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index c25a0b3..38d2653 100644 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -38,7 +38,7 @@ def get_tailf_ordering(context, stmt, target_stmt): } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: - return { + return [ ('create', conj, 'create'), ('modify', conj, 'create'), ('delete', conj, 'create'), @@ -48,75 +48,75 @@ def get_tailf_ordering(context, stmt, target_stmt): ('create', conj, 'delete'), ('modify', conj, 'delete'), ('delete', conj, 'delete'), - } - ordering = set() + ] + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': - ordering.update({ + ordering.extend([ ('create', conj, 'create'), ('modify', conj, 'create'), ('delete', conj, 'create'), ('create', conj, 'modify'), ('modify', conj, 'modify'), ('delete', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-create': - ordering.update({ + ordering.extend([ ('create', conj, 'create'), ('modify', conj, 'create'), ('delete', conj, 'create'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-modify': - ordering.update({ + ordering.extend([ ('create', conj, 'modify'), ('modify', conj, 'modify'), ('delete', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-delete': - ordering.update({ + ordering.extend([ ('create', conj, 'delete'), ('modify', conj, 'delete'), ('delete', conj, 'delete'), - }) + ]) return ordering elif stmt.keyword[1] in ['cli-diff-create-after', 'cli-diff-create-before']: conj = 'after' if stmt.keyword[1] == 'cli-diff-create-after' else 'before' - valid_substmts = { + valid_substmts = [ 'cli-when-target-set', 'cli-when-target-create', 'cli-when-target-modify', 'cli-when-target-delete', - } + ] substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: if symmetric: - return { + return [ ('create', conj, 'modify'), ('create', conj, 'delete'), - } + ] else: - return { + return [ ('create', conj, 'create'), ('create', conj, 'modify'), ('create', conj, 'delete'), - } - ordering = set() + ] + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': - ordering.update({ + ordering.extend([ ('create', conj, 'create'), ('create', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-create': - ordering.add( + ordering.append( ('create', conj, 'create'), ) elif substmt.keyword[1] == 'cli-when-target-modify': - ordering.add( + ordering.append( ('create', conj, 'modify'), ) elif substmt.keyword[1] == 'cli-when-target-delete': - ordering.add( + ordering.append( ('create', conj, 'delete'), ) return ordering @@ -131,33 +131,33 @@ def get_tailf_ordering(context, stmt, target_stmt): substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: if symmetric: - return { + return [ ('delete', conj, 'create'), ('delete', conj, 'modify'), - } + ] else: - return { + return [ ('delete', conj, 'create'), ('delete', conj, 'modify'), ('delete', conj, 'delete'), - } - ordering = set() + ] + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': - ordering.update({ + ordering.extend([ ('delete', conj, 'create'), ('delete', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-create': - ordering.add( + ordering.append( ('delete', conj, 'create'), ) elif substmt.keyword[1] == 'cli-when-target-modify': - ordering.add( + ordering.append( ('delete', conj, 'modify'), ) elif substmt.keyword[1] == 'cli-when-target-delete': - ordering.add( + ordering.append( ('delete', conj, 'delete'), ) return ordering @@ -171,28 +171,28 @@ def get_tailf_ordering(context, stmt, target_stmt): } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: - return { + return [ ('modify', conj, 'create'), ('modify', conj, 'modify'), ('modify', conj, 'delete'), - } - ordering = set() + ] + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': - ordering.update({ + ordering.extend([ ('modify', conj, 'create'), ('modify', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-create': - ordering.add( + ordering.append( ('modify', conj, 'create'), ) elif substmt.keyword[1] == 'cli-when-target-modify': - ordering.add( + ordering.append( ('modify', conj, 'modify'), ) elif substmt.keyword[1] == 'cli-when-target-delete': - ordering.add( + ordering.append( ('modify', conj, 'delete'), ) return ordering @@ -206,38 +206,38 @@ def get_tailf_ordering(context, stmt, target_stmt): } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] if len(substmts) == 0: - return { + return [ ('create', conj, 'create'), ('modify', conj, 'create'), ('create', conj, 'modify'), ('modify', conj, 'modify'), ('create', conj, 'delete'), ('modify', conj, 'delete'), - } - ordering = set() + ] + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-when-target-set': - ordering.update({ + ordering.extend([ ('create', conj, 'create'), ('modify', conj, 'create'), ('create', conj, 'modify'), ('modify', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-create': - ordering.update({ + ordering.extend([ ('create', conj, 'create'), ('modify', conj, 'create'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-modify': - ordering.update({ + ordering.extend([ ('create', conj, 'modify'), ('modify', conj, 'modify'), - }) + ]) elif substmt.keyword[1] == 'cli-when-target-delete': - ordering.update({ + ordering.extend([ ('create', conj, 'delete'), ('modify', conj, 'delete'), - }) + ]) return ordering elif stmt.keyword[1] == 'cli-diff-dependency': valid_substmts = { @@ -246,14 +246,14 @@ def get_tailf_ordering(context, stmt, target_stmt): 'cli-trigger-on-all', } substmts = [s for s in stmt.substmts if s.keyword[1] in valid_substmts] - ordering = { + ordering = [ ('create', 'after', 'create'), ('modify', 'after', 'create'), ('delete', 'before', 'modify'), ('create', 'before', 'delete'), ('modify', 'before', 'delete'), ('delete', 'before', 'delete'), - } + ] # Test result from TailF confd 8.4.7.1: # 1 depends on 2 # ('create', 'after', 'create'), @@ -277,15 +277,15 @@ def get_tailf_ordering(context, stmt, target_stmt): # ('delete', 'before', 'delete'), if len(substmts) == 0: return ordering - ordering = set() + ordering = [] for substmt in substmts: if substmt.keyword[1] == 'cli-trigger-on-set': - ordering.update({ + ordering.extend([ ('create', 'after', 'create'), ('modify', 'after', 'create'), ('create', 'after', 'modify'), ('modify', 'after', 'modify'), - }) + ]) # Test result from TailF confd 8.4.7.1: # 1 depends on 2 # ('create', 'after', 'create'), @@ -308,12 +308,12 @@ def get_tailf_ordering(context, stmt, target_stmt): # ('modify', 'after', 'delete'), # ('delete', 'after', 'delete'), elif substmt.keyword[1] == 'cli-trigger-on-delete': - ordering.update({ + ordering.extend([ ('create', 'after', 'create'), ('modify', 'after', 'create'), ('delete', 'before', 'modify'), ('delete', 'before', 'delete'), - }) + ]) # Test result from TailF confd 8.4.7.1: # 1 depends on 2 # ('create', 'after', 'create'), @@ -336,7 +336,7 @@ def get_tailf_ordering(context, stmt, target_stmt): # ('modify', 'before', 'delete'), # ('delete', 'before', 'delete'), elif substmt.keyword[1] == 'cli-trigger-on-all': - return { + return [ ('create', 'after', 'create'), ('modify', 'after', 'create'), ('delete', 'after', 'create'), @@ -346,7 +346,7 @@ def get_tailf_ordering(context, stmt, target_stmt): ('create', 'after', 'delete'), ('modify', 'after', 'delete'), ('delete', 'after', 'delete'), - } + ] # Test result from TailF confd 8.4.7.1: # 1 depends on 2 # ('create', 'after', 'create'), @@ -369,7 +369,7 @@ def get_tailf_ordering(context, stmt, target_stmt): # ('modify', 'after', 'delete'), # ('delete', 'after', 'delete'), return ordering - return set() + return [] def add_tailf_annotation(module_namespaces, stmt, node): if len(stmt.substmts) > 0: @@ -400,7 +400,7 @@ def set_ordering_xpath(compiler, module): hasattr(compiler, constraint_type) and module in getattr(compiler, constraint_type) ): - constraints = write_ordering_xpath( + write_ordering_xpath( compiler, module, constraint_type) @@ -419,38 +419,37 @@ def get_xpath(compiler, stmt): stmt = {} xpath = {} constraint_info = getattr(compiler, constraint_type)[module] - for stmt[0], stmt[1] in constraint_info: + + for annotation_stmt in constraint_info: + stmt[0], stmt[1], cinstraint_list = constraint_info[annotation_stmt] + for i in range(2): xpath[i] = get_xpath(compiler, stmt[i]) - if xpath[0] == '' or xpath[1] == '': # Skip entries with missing Xpath. Missing Xpaths might be in a # different module not compiled or due to other deviations. - continue - cinstraint_list, pos = constraint_info[(stmt[0], stmt[1])] - for oper_0, sequence, oper_1 in cinstraint_list: - if xpath[0] == xpath[1] and oper_0 == oper_1: - # Skip entries with same Xpath and same operation. - continue - if sequence == 'before': - constraints.append(( - f"{xpath[0]}, {oper_0}", f"{xpath[1]}, {oper_1}", pos)) - update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) - else: - constraints.append(( - f"{xpath[1]}, {oper_1}", f"{xpath[0]}, {oper_0}", pos)) - update_schema_tree(stmt[1], oper_1, stmt[0], oper_0) + if xpath[i] == '': + logger.warning("Xpath is not available for the statement at " + f"{stmt[i].pos}. Skipping this TailF ordering " + "constraint.") + break + else: + for oper_0, sequence, oper_1 in cinstraint_list: + if xpath[0] == xpath[1] and oper_0 == oper_1: + # Skip entries with same Xpath and same operation. + continue + if sequence == 'before': + constraints.append(( + xpath[0], oper_0, xpath[1], oper_1, annotation_stmt)) + update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) + else: + constraints.append(( + xpath[1], oper_1, xpath[0], oper_0, annotation_stmt)) + update_schema_tree(stmt[1], oper_1, stmt[0], oper_0) attribute_name = "ordering_xpath_leafref" \ if constraint_type == "ordering_stmt_leafref" \ else "ordering_xpath_tailf" getattr(compiler, attribute_name)[module] = constraints - if len(constraints) > 0: - csv_filename = path.join( - compiler.dir_yang, f'{module}_{attribute_name}.csv') - with open(csv_filename, 'w') as f: - f.write("\n".join([f"{c[0]}, {c[1]}" for c in constraints])) - - return [(c[0], c[1]) for c in constraints] def update_schema_tree(stmt_0, oper_0, stmt_1, oper_1): From 87c3a639a81161e2ad31109f9f5e8c0dcc72248b Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Mon, 19 Jan 2026 16:28:10 -0500 Subject: [PATCH 06/11] Support if-feature --- src/ncdiff/model.py | 138 ++++++++++++++++++++++---------------------- src/ncdiff/tailf.py | 3 - 2 files changed, 69 insertions(+), 72 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index d21380e..822ddaa 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -1258,8 +1258,12 @@ def __init__(self, folder): self.ordering_stmt_tailf = {} self.ordering_xpath_leafref = {} self.ordering_xpath_tailf = {} - self._ordering_without_obsolete = True - self._ordering_without_deprecated = False + self._dependencies = {} + + self.exclude_obsolete = False + self.exclude_deprecated = False + self.include_xpaths = set() + self.exclude_xpaths = set() @property def pyang_errors(self): @@ -1344,7 +1348,9 @@ def find_all_depends(depends, dependencies): depends = imports | includes while find_all_depends(depends, dependencies): pass - return (imports, includes, depends - imports - includes) + self._dependencies[module] = ( + imports, includes, depends - imports - includes) + return self._dependencies[module] def compile(self, module): '''compile @@ -1470,6 +1476,13 @@ def depict_a_schema_node(self, module, parent, child, mode=None): sm = child.search_one('status') if sm is not None and sm.arg in ['deprecated', 'obsolete']: n.set('status', sm.arg) + + if self.skip(child, n): + parent.remove(n) + return + if not hasattr(child, 'schema_node'): + child.schema_node = n + sm = child.search('default') if sm is not None and len(sm) > 0: n.set('default', ",".join(map(lambda x: x.arg, sm))) @@ -1515,39 +1528,23 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - status = self.obsolete_or_deprecated(child) - if not ( - status == 'obsolete' and - self._ordering_without_obsolete or - status == 'deprecated' and - self._ordering_without_deprecated - ): - if not has_tailf_ordering(ch, self.context): - add_tailf_annotation(self.module_namespaces, ch, n) - else: - target = self.context.check_data_tree_xpath( - ch, child) - if target is not None: - status = self.obsolete_or_deprecated(target) - if not ( - status == 'obsolete' and - self._ordering_without_obsolete or - status == 'deprecated' and - self._ordering_without_deprecated - ): - ordering = get_tailf_ordering( - self.context, ch, target) - self.ordering_stmt_tailf[module.arg][ch] = ( - child, - target, - ordering, - ) + if not has_tailf_ordering(ch, self.context): + add_tailf_annotation(self.module_namespaces, ch, n) + else: + target = self.context.check_data_tree_xpath( + ch, child) + if target is not None: + ordering = get_tailf_ordering( + self.context, ch, target) + self.ordering_stmt_tailf[module.arg][ch] = ( + child, + target, + ordering, + ) else: logger.warning("Unknown Tailf annotation at {}, " "keyword = {}" .format(ch.pos, ch.keyword)) - if not hasattr(child, 'schema_node'): - child.schema_node = n featurenames = [f.arg for f in child.search('if-feature')] if hasattr(child, 'i_augment'): @@ -1601,36 +1598,23 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): if p is not None: # Consider leafref as a dpendency for ordering purpose - status = self.obsolete_or_deprecated(leaf_statement) - if not ( - status == 'obsolete' and - self._ordering_without_obsolete or - status == 'deprecated' and - self._ordering_without_deprecated - ): + if not self.skip(leaf_statement, leaf_node): target_stmt = self.context.check_data_tree_xpath( p, leaf_statement) if target_stmt is not None: - status = self.obsolete_or_deprecated(target_stmt) - if not ( - status == 'obsolete' and - self._ordering_without_obsolete or - status == 'deprecated' and - self._ordering_without_deprecated - ): - self.ordering_stmt_leafref[module][ - leaf_statement] = ( - leaf_statement, - target_stmt, - [ - ('create', 'after', 'create'), - ('modify', 'after', 'create'), - ('create', 'after', 'modify'), - ('delete', 'before', 'modify'), - ('modify', 'before', 'delete'), - ('delete', 'before', 'delete'), - ], - ) + self.ordering_stmt_leafref[module][ + leaf_statement] = ( + leaf_statement, + target_stmt, + [ + ('create', 'after', 'create'), + ('modify', 'after', 'create'), + ('create', 'after', 'modify'), + ('delete', 'before', 'modify'), + ('modify', 'before', 'delete'), + ('delete', 'before', 'delete'), + ], + ) # Try to make the path as compact as possible. # Remove local prefixes, and only use prefix when @@ -1647,12 +1631,12 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): else: target.append(prefix + ':' + name) curprefix = prefix - datatype = "-> %s" % "/".join(target) + datatype = f'leafref {"/".join(target)}' else: datatype = sm.arg elif sm.arg == 'identityref': idn_base = sm.search_one('base') - datatype = sm.arg + ":" + idn_base.arg + datatype = f'identityref {idn_base.arg}' else: datatype = sm.arg leaf_node.set('datatype', datatype) @@ -1718,14 +1702,30 @@ def type_identityref_values(self, type_statement): return '|'.join(value_stmts) return '' - @staticmethod - def obsolete_or_deprecated(statement): - while statement is not None: - sm = statement.search_one('status') - if sm is not None and sm.arg in ['obsolete', 'deprecated']: - return sm.arg - statement = statement.parent - return None + def skip(self, statement, schema_node): + xpath = self.get_xpath_from_schema_node( + schema_node, type=Tag.LXML_XPATH) + for in_xpath in self.include_xpaths: + if in_xpath == xpath or in_xpath.startswith(xpath + '/'): + return False + for ex_xpath in self.exclude_xpaths: + if ex_xpath == xpath or xpath.startswith(ex_xpath + '/'): + return True + + # i_not_implemented should be set to True when features in the context + # are not met + if getattr(statement, "i_not_implemented", None) is True: + return True + + status = schema_node.get('status', default=None) + if ( + status == 'obsolete' and + self.exclude_obsolete or + status == 'deprecated' and + self.exclude_deprecated + ): + return True + return False class ModelDiff(object): diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index 38d2653..0b17143 100644 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -428,9 +428,6 @@ def get_xpath(compiler, stmt): # Skip entries with missing Xpath. Missing Xpaths might be in a # different module not compiled or due to other deviations. if xpath[i] == '': - logger.warning("Xpath is not available for the statement at " - f"{stmt[i].pos}. Skipping this TailF ordering " - "constraint.") break else: for oper_0, sequence, oper_1 in cinstraint_list: From 71ddfeac63f5119ce579a6298b687cd7800a6529 Mon Sep 17 00:00:00 2001 From: yuekyang <[yuekyang@cisco.com]> Date: Thu, 22 Jan 2026 13:50:29 -0500 Subject: [PATCH 07/11] Add unit tests --- src/ncdiff/model.py | 4 +- src/ncdiff/tailf.py | 39 +- src/ncdiff/tests/test_tailf_annotation.py | 590 ++++++++++++++++++ .../tests/yang/Cisco-IOS-XE-sla-ann.yang | 83 +++ 4 files changed, 696 insertions(+), 20 deletions(-) mode change 100644 => 100755 src/ncdiff/tailf.py create mode 100755 src/ncdiff/tests/test_tailf_annotation.py create mode 100644 src/ncdiff/tests/yang/Cisco-IOS-XE-sla-ann.yang diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 822ddaa..d112d2c 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -21,7 +21,7 @@ from .errors import ModelError from .composer import Tag -from .tailf import has_tailf_ordering, get_tailf_ordering +from .tailf import is_tailf_ordering, get_tailf_ordering from .tailf import add_tailf_annotation, set_ordering_xpath @@ -1528,7 +1528,7 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - if not has_tailf_ordering(ch, self.context): + if not is_tailf_ordering(ch, self.context): add_tailf_annotation(self.module_namespaces, ch, n) else: target = self.context.check_data_tree_xpath( diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py old mode 100644 new mode 100755 index 0b17143..8c41f31 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -8,22 +8,25 @@ logger = logging.getLogger(__name__) -def has_tailf_ordering(stmt, context): - prefix, identifier = stmt.raw_keyword - m, rev = util.prefix_to_modulename_and_revision( - stmt.i_orig_module, - prefix, - stmt.pos, - context.errors, - ) - return m == 'tailf-common' and identifier in { - 'cli-diff-after', 'cli-diff-before', - 'cli-diff-create-after', 'cli-diff-create-before', - 'cli-diff-delete-after', 'cli-diff-delete-before', - 'cli-diff-modify-after', 'cli-diff-modify-before', - 'cli-diff-set-after', 'cli-diff-set-before', - 'cli-diff-dependency', - } +def is_tailf_ordering(stmt, context): + if isinstance(stmt.raw_keyword, tuple): + prefix, identifier = stmt.raw_keyword + m, rev = util.prefix_to_modulename_and_revision( + stmt.i_orig_module, + prefix, + stmt.pos, + context.errors, + ) + return m == 'tailf-common' and identifier in { + 'cli-diff-after', 'cli-diff-before', + 'cli-diff-create-after', 'cli-diff-create-before', + 'cli-diff-delete-after', 'cli-diff-delete-before', + 'cli-diff-modify-after', 'cli-diff-modify-before', + 'cli-diff-set-after', 'cli-diff-set-before', + 'cli-diff-dependency', + } + else: + return False def get_tailf_ordering(context, stmt, target_stmt): @@ -400,11 +403,11 @@ def set_ordering_xpath(compiler, module): hasattr(compiler, constraint_type) and module in getattr(compiler, constraint_type) ): - write_ordering_xpath( + update_ordering_xpath( compiler, module, constraint_type) -def write_ordering_xpath(compiler, module, constraint_type): +def update_ordering_xpath(compiler, module, constraint_type): def get_xpath(compiler, stmt): schema_node = getattr(stmt, 'schema_node', None) diff --git a/src/ncdiff/tests/test_tailf_annotation.py b/src/ncdiff/tests/test_tailf_annotation.py new file mode 100755 index 0000000..4e1c4c6 --- /dev/null +++ b/src/ncdiff/tests/test_tailf_annotation.py @@ -0,0 +1,590 @@ +#!/bin/env python +""" Unit tests for the ncdiff cisco-shared package. """ + +import os +import unittest +from ncdiff.composer import Tag +from ncdiff.model import ModelCompiler +from ncdiff.tailf import is_tailf_ordering, get_tailf_ordering +from ncdiff.tailf import is_symmetric_tailf_ordering + + +curr_dir = os.path.dirname(os.path.abspath(__file__)) + + +def delete_xml_files(folder): + for filename in os.listdir(folder): + if filename.endswith(".xml"): + file_path = os.path.join(folder, filename) + os.remove(file_path) + + +class TestNative(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.compiler = ModelCompiler(os.path.join(curr_dir, 'yang')) + delete_xml_files(cls.compiler.dir_yang) + cls.native = cls.compiler.compile('Cisco-IOS-XE-native') + # cls.oc_interfaces = cls.compiler.compile('openconfig-interfaces') + + def test_dependencies(self): + self.assertIsNotNone(self.compiler.context) + self.assertEqual(self.native.tree.tag, 'Cisco-IOS-XE-native') + imports, includes, depends = \ + self.compiler._dependencies['Cisco-IOS-XE-native'] + self.assertIn('Cisco-IOS-XE-features', imports) + self.assertIn('Cisco-IOS-XE-interfaces', includes) + self.assertIn('Cisco-IOS-XE-sla', depends) + self.assertIn('Cisco-IOS-XE-sla-ann', depends) + + def test_check_data_tree_xpath(self): + # Line 52 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate-module Cisco-IOS-XE-sla { + # tailf:annotate-statement "grouping[name='config-ip-sla-grouping']" { + # tailf:annotate-statement "container[name='sla']" { + # tailf:annotate-statement "list[name='entry']" { + # tailf:annotate-statement "choice[name='sla-param'] " { + # tailf:annotate-statement "case[name='path-echo-case'] " { + # tailf:annotate-statement "container[name='path-echo']" { + # tailf:annotate-statement "leaf[name='source-ip']" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-set; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # } + # } + # } + # } + # } + # } + # } + # } + stmt = self.compiler.context.get_module('Cisco-IOS-XE-sla') + for arg in [ + "config-ip-sla-grouping", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "source-ip", + ]: + stmts = [i for i in stmt.substmts if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after")] + self.assertEqual(len(stmts), 1) + xpath_stmt = stmts[0] + + target = self.compiler.context.check_data_tree_xpath(xpath_stmt, stmt) + + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "interface", + "GigabitEthernet", + "carrier-delay", + "delay-choice", + "seconds", + "seconds", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + + self.assertIs(target, stmt) + + def test_check_schema_tree_xpath(self): + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + stmt = self.compiler.context.get_module('Cisco-IOS-XE-sla-ann') + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "annotate")] + self.assertEqual(len(stmts), 1) + annotating_stmt = stmts[0] + + target = self.compiler.context.check_schema_tree_xpath(annotating_stmt) + + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + + self.assertIs(target, stmt) + + def test_get_xpath_from_schema_node(self): + xpath = "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry" \ + "/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo" \ + "/ios-sla:dst-ip" + self.assertIsNotNone(self.native.tree) + matches = self.native.tree.xpath( + "/Cisco-IOS-XE-native" + xpath, + namespaces=self.native.prefixes, + ) + self.assertEqual(len(matches), 1) + schema_node = matches[0] + result_xpath = self.compiler.context.get_xpath_from_schema_node( + schema_node, type=Tag.LXML_XPATH) + self.assertEqual(result_xpath, xpath) + + def test_process_annotation_module_1(self): + # Line 52 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate-module Cisco-IOS-XE-sla { + # tailf:annotate-statement "grouping[name='config-ip-sla-grouping']" { + # tailf:annotate-statement "container[name='sla']" { + # tailf:annotate-statement "list[name='entry']" { + # tailf:annotate-statement "choice[name='sla-param'] " { + # tailf:annotate-statement "case[name='path-echo-case'] " { + # tailf:annotate-statement "container[name='path-echo']" { + # tailf:annotate-statement "leaf[name='source-ip']" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-set; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # } + # } + # } + # } + # } + # } + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "source-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-set"), + ) + + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "cli-diff-delete-before") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-delete"), + ) + + def test_process_annotation_module_2(self): + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-delete"), + ) + + stmts = [i for i in stmt.substmts + if i.keyword == ("tailf-common", "cli-diff-delete-before") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-create"), + ) + + def test_ordering_stmt_leafref(self): + self.assertIn( + 'Cisco-IOS-XE-native', + self.compiler.ordering_stmt_leafref, + ) + + # Line 159 in Cisco-IOS-XE-parser.yang: + # leaf view-name { + # type leafref { + # path "../../../view-name-list/name"; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "parser", + "view", + "view-name-superview-list", + "view", + "view-name", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + leafref = stmt + self.assertIn( + leafref, + self.compiler.ordering_stmt_leafref['Cisco-IOS-XE-native'], + ) + leafref_stmt, target_stmt, ordering = \ + self.compiler.ordering_stmt_leafref['Cisco-IOS-XE-native'][leafref] + + type_stmt = leafref.search_one('type') + self.assertIsNotNone(type_stmt) + path_stmt = type_stmt.search_one('path') + self.assertIsNotNone(path_stmt) + target = self.compiler.context.check_data_tree_xpath( + path_stmt, leafref) + + self.assertIs(leafref, leafref_stmt) + self.assertIs(target, target_stmt) + + def test_ordering_stmt_tailf(self): + self.assertIn('Cisco-IOS-XE-native', self.compiler.ordering_stmt_tailf) + + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + node = stmt + + # tailf:cli-diff-create-after + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-delete"), + ) + self.assertIn( + annotation, + self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'], + ) + node_stmt, target_stmt, ordering = \ + self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'][annotation] + + target = self.compiler.context.check_data_tree_xpath( + annotation, node) + + self.assertIs(node_stmt, node) + self.assertIs(target, target_stmt) + + # tailf:cli-diff-delete-before + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-delete-before") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + self.assertEqual(len(annotation.substmts), 1) + annotation_substmt = annotation.substmts[0] + self.assertEqual( + annotation_substmt.keyword, + ("tailf-common", "cli-when-target-create"), + ) + self.assertIn( + annotation, + self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'], + ) + node_stmt, target_stmt, ordering = \ + self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'][annotation] + + target = self.compiler.context.check_data_tree_xpath( + annotation, node) + + self.assertIs(node_stmt, node) + self.assertIs(target, target_stmt) + + def test_datatype_leafref(self): + # Line 159 in Cisco-IOS-XE-parser.yang: + # leaf view-name { + # type leafref { + # path "../../../view-name-list/name"; + # } + # } + xpath = "/ios:native/ios:parser/ios:view" \ + "/ios:view-name-superview-list/ios:view/ios:view-name" + matches = self.native.tree.xpath( + "/Cisco-IOS-XE-native" + xpath, + namespaces=self.native.prefixes, + ) + self.assertEqual(len(matches), 1) + schema_node = matches[0] + datatype = schema_node.get("datatype", None) + self.assertEqual(datatype, "leafref ../../../view-name-list/name") + + def test_has_tailf_ordering(self): + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + node = stmt + func_result = is_tailf_ordering(node, self.compiler.context) + self.assertFalse(func_result) + + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + func_result = is_tailf_ordering(annotation, self.compiler.context) + self.assertTrue(func_result) + + def test_get_tailf_ordering(self): + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + node = stmt + + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + target = self.compiler.context.check_data_tree_xpath( + annotation, node) + ordering = get_tailf_ordering(self.compiler.context, annotation, target) + self.assertEqual(ordering, [('create', 'after', 'delete')]) + + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-delete-before") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + target = self.compiler.context.check_data_tree_xpath( + annotation, node) + ordering = get_tailf_ordering(self.compiler.context, annotation, target) + self.assertEqual(ordering, [('delete', 'before', 'create')]) + + def test_is_symmetric_tailf_ordering(self): + # Line 75 in Cisco-IOS-XE-sla-ann.yang: + # tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-delete; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + # tailf:cli-when-target-create; + # } + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "ip", + "sla", + "entry", + "sla-param", + "path-echo-case", + "path-echo", + "dst-ip", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + node = stmt + + stmts = [i for i in node.substmts + if i.keyword == ("tailf-common", "cli-diff-create-after") and + i.arg == "/ios:native/ios:interface/ios:GigabitEthernet" + "/ios-eth:carrier-delay/ios-eth:seconds"] + self.assertEqual(len(stmts), 1) + annotation = stmts[0] + target = self.compiler.context.check_data_tree_xpath( + annotation, node) + func_result = is_symmetric_tailf_ordering(self.compiler.context, annotation, target) + self.assertFalse(func_result) + + +class TestOpenConfigInterfaces(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.compiler = ModelCompiler(os.path.join(curr_dir, 'yang')) + delete_xml_files(cls.compiler.dir_yang) + cls.oc_interfaces = cls.compiler.compile('openconfig-interfaces') + + def test_datatype_identityref(self): + # Line 254 in Cisco-IOS-XE-interfaces.yang: + # leaf type { + # type identityref { + # base ietf-if:interface-type; + # } + # mandatory true; + # description + # "[adapted from IETF interfaces model (RFC 7223)] + + # The type of the interface. + + # When an interface entry is created, a server MAY + # initialize the type leaf with a valid value, e.g., if it + # is possible to derive the type from the name of the + # interface. + + # If a client tries to set the type of an interface to a + # value that can never be used by the system, e.g., if the + # type is not supported or if the type does not match the + # name of the interface, the server MUST reject the request. + # A NETCONF server MUST reply with an rpc-error with the + # error-tag 'invalid-value' in this case."; + # reference + # "RFC 2863: The Interfaces Group MIB - ifType"; + # } + xpath = "/oc-if:interfaces/oc-if:interface/oc-if:config/oc-if:type" + matches = self.oc_interfaces.tree.xpath( + "/openconfig-interfaces" + xpath, + namespaces=self.oc_interfaces.prefixes, + ) + self.assertEqual(len(matches), 1) + schema_node = matches[0] + datatype = schema_node.get("datatype", None) + self.assertEqual(datatype, "identityref ietf-if:interface-type") diff --git a/src/ncdiff/tests/yang/Cisco-IOS-XE-sla-ann.yang b/src/ncdiff/tests/yang/Cisco-IOS-XE-sla-ann.yang new file mode 100644 index 0000000..c3e8921 --- /dev/null +++ b/src/ncdiff/tests/yang/Cisco-IOS-XE-sla-ann.yang @@ -0,0 +1,83 @@ +module Cisco-IOS-XE-sla-ann { + namespace "http://cisco.com/ns/yang/Cisco-IOS-XE-sla-ann"; + prefix ios-sla-ann; + + import Cisco-IOS-XE-native { + prefix ios; + } + + import Cisco-IOS-XE-sla { + prefix ios-sla; + } + + import Cisco-IOS-XE-ethernet { + prefix ios-eth; + } + import tailf-common { + prefix tailf; + } + + organization + "Cisco Systems, Inc."; + + contact + "Cisco Systems, Inc. + Customer Service + Postal: 170 W Tasman Drive + San Jose, CA 95134 + Tel: +1 1800 553-NETS + E-mail: cs-yang@cisco.com"; + + description + "Cisco XE Native Service Level Agreements (SLA) Annotation Yang Model. + Copyright (c) 2019-2021 by Cisco Systems, Inc. + All rights reserved."; + + revision 2020-07-01 { + description + "Removed annotations for ethernet-monitor container nodes as they are hardened in + 17.1.1 release"; + } + + revision 2019-12-04 { + description + "Added annotations for all non-hardened nodes to hide them from + controller"; + } + + revision 2019-03-28 { + description "Initial revision"; + } + + tailf:annotate-module Cisco-IOS-XE-sla { + tailf:annotate-statement "grouping[name='config-ip-sla-grouping']" { + tailf:annotate-statement "container[name='sla']" { + tailf:annotate-statement "list[name='entry']" { + tailf:annotate-statement "choice[name='sla-param'] " { + tailf:annotate-statement "case[name='path-echo-case'] " { + tailf:annotate-statement "container[name='path-echo']" { + tailf:annotate-statement "leaf[name='source-ip']" { + tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + tailf:cli-when-target-set; + } + tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + tailf:cli-when-target-delete; + } + } + } + } + } + } + } + } + } + + tailf:annotate "/ios:native/ios:ip/ios-sla:sla/ios-sla:entry/ios-sla:sla-param/ios-sla:path-echo-case/ios-sla:path-echo/ios-sla:dst-ip" { + tailf:cli-diff-create-after "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + tailf:cli-when-target-delete; + } + tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:GigabitEthernet/ios-eth:carrier-delay/ios-eth:seconds" { + tailf:cli-when-target-create; + } + } +} From 1cfc1804e8d066eb739bbfcbcbabf3c2c46fc699 Mon Sep 17 00:00:00 2001 From: yuekyang Date: Wed, 4 Mar 2026 15:24:36 -0500 Subject: [PATCH 08/11] Improve update_ordering_xpath() --- src/ncdiff/model.py | 15 +++--- src/ncdiff/tailf.py | 55 +++++++++++++++++++--- src/ncdiff/tests/test_tailf_annotation.py | 56 ++++++++++++----------- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index d112d2c..459f3c6 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -1448,8 +1448,8 @@ def compile(self, module): else: self.identity_deps[b_idn].append(curr_idn) - self.ordering_stmt_leafref[module] = {} - self.ordering_stmt_tailf[module] = {} + self.ordering_stmt_leafref[module] = [] + self.ordering_stmt_tailf[module] = [] for child in vm.i_children: if child.keyword in statements.data_definition_keywords: @@ -1536,11 +1536,12 @@ def depict_a_schema_node(self, module, parent, child, mode=None): if target is not None: ordering = get_tailf_ordering( self.context, ch, target) - self.ordering_stmt_tailf[module.arg][ch] = ( + self.ordering_stmt_tailf[module.arg].append(( child, target, ordering, - ) + ch.pos, + )) else: logger.warning("Unknown Tailf annotation at {}, " "keyword = {}" @@ -1602,8 +1603,7 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): target_stmt = self.context.check_data_tree_xpath( p, leaf_statement) if target_stmt is not None: - self.ordering_stmt_leafref[module][ - leaf_statement] = ( + self.ordering_stmt_leafref[module].append(( leaf_statement, target_stmt, [ @@ -1614,7 +1614,8 @@ def set_leaf_datatype_value(self, module, leaf_statement, leaf_node): ('modify', 'before', 'delete'), ('delete', 'before', 'delete'), ], - ) + sm.pos, + )) # Try to make the path as compact as possible. # Remove local prefixes, and only use prefix when diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index 8c41f31..e9bebf7 100755 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -398,16 +398,40 @@ def add_tailf_annotation(module_namespaces, stmt, node): def set_ordering_xpath(compiler, module): - for constraint_type in ["ordering_stmt_leafref", "ordering_stmt_tailf"]: + # There are cases where a leafref node has TailF ordering annotations + # defined. For example: + # leaf nve { + # description + # "Network virtualization endpoint interface"; + # tailf:cli-allow-join-with-value { + # tailf:cli-display-joined; + # } + # tailf:cli-diff-create-after "/ios:native/ios:interface/ios:nve/ios:name" { + # tailf:cli-when-target-set; + # } + # tailf:cli-diff-delete-before "/ios:native/ios:interface/ios:nve/ios:name" { + # tailf:cli-when-target-delete; + # } + # type leafref { + # path "/ios:native/ios:interface/ios:nve/ios:name"; + # } + # } + # In this case, we treat TailF ordering annotations as higher priority and + # ignore the default leafref ordering constraints. To support this, we + # define a dictionary to track existing nodes that have TailF ordering + # annotations applied. + tailf_ordering = {} + + for constraint_type in ["ordering_stmt_tailf", "ordering_stmt_leafref"]: if ( hasattr(compiler, constraint_type) and module in getattr(compiler, constraint_type) ): update_ordering_xpath( - compiler, module, constraint_type) + compiler, module, constraint_type, tailf_ordering) -def update_ordering_xpath(compiler, module, constraint_type): +def update_ordering_xpath(compiler, module, constraint_type, tailf_ordering): def get_xpath(compiler, stmt): schema_node = getattr(stmt, 'schema_node', None) @@ -423,27 +447,44 @@ def get_xpath(compiler, stmt): xpath = {} constraint_info = getattr(compiler, constraint_type)[module] - for annotation_stmt in constraint_info: - stmt[0], stmt[1], cinstraint_list = constraint_info[annotation_stmt] + for stmt[0], stmt[1], cinstraint_list, position in constraint_info: for i in range(2): xpath[i] = get_xpath(compiler, stmt[i]) + # Skip entries with missing Xpath. Missing Xpaths might be in a # different module not compiled or due to other deviations. if xpath[i] == '': break else: + + # Track nodes that have TailF ordering annotations applied. + if constraint_type == "ordering_stmt_tailf": + if stmt[0] not in tailf_ordering: + tailf_ordering[stmt[0]] = {} + if stmt[1] not in tailf_ordering[stmt[0]]: + tailf_ordering[stmt[0]][stmt[1]] = True + + # Skip entries where either node has TailF ordering annotations + # applied. + if constraint_type == "ordering_stmt_leafref": + if ( + stmt[0] in tailf_ordering and + stmt[1] in tailf_ordering[stmt[0]] + ): + continue + for oper_0, sequence, oper_1 in cinstraint_list: if xpath[0] == xpath[1] and oper_0 == oper_1: # Skip entries with same Xpath and same operation. continue if sequence == 'before': constraints.append(( - xpath[0], oper_0, xpath[1], oper_1, annotation_stmt)) + xpath[0], oper_0, xpath[1], oper_1, position)) update_schema_tree(stmt[0], oper_0, stmt[1], oper_1) else: constraints.append(( - xpath[1], oper_1, xpath[0], oper_0, annotation_stmt)) + xpath[1], oper_1, xpath[0], oper_0, position)) update_schema_tree(stmt[1], oper_1, stmt[0], oper_0) attribute_name = "ordering_xpath_leafref" \ diff --git a/src/ncdiff/tests/test_tailf_annotation.py b/src/ncdiff/tests/test_tailf_annotation.py index 4e1c4c6..7209201 100755 --- a/src/ncdiff/tests/test_tailf_annotation.py +++ b/src/ncdiff/tests/test_tailf_annotation.py @@ -299,12 +299,14 @@ def test_ordering_stmt_leafref(self): self.assertEqual(len(stmts), 1) stmt = stmts[0] leafref = stmt - self.assertIn( - leafref, - self.compiler.ordering_stmt_leafref['Cisco-IOS-XE-native'], - ) - leafref_stmt, target_stmt, ordering = \ - self.compiler.ordering_stmt_leafref['Cisco-IOS-XE-native'][leafref] + + tuples = [ + i + for i in self.compiler.ordering_stmt_leafref['Cisco-IOS-XE-native'] + if i[0] is leafref + ] + self.assertEqual(len(tuples), 1) + leafref_stmt, target_stmt, ordering, position = tuples[0] type_stmt = leafref.search_one('type') self.assertIsNotNone(type_stmt) @@ -315,6 +317,8 @@ def test_ordering_stmt_leafref(self): self.assertIs(leafref, leafref_stmt) self.assertIs(target, target_stmt) + self.assertIsInstance(ordering, list) + self.assertIn('Cisco-IOS-XE-parser.yang:160', str(position)) def test_ordering_stmt_tailf(self): self.assertIn('Cisco-IOS-XE-native', self.compiler.ordering_stmt_tailf) @@ -359,18 +363,6 @@ def test_ordering_stmt_tailf(self): annotation_substmt.keyword, ("tailf-common", "cli-when-target-delete"), ) - self.assertIn( - annotation, - self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'], - ) - node_stmt, target_stmt, ordering = \ - self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'][annotation] - - target = self.compiler.context.check_data_tree_xpath( - annotation, node) - - self.assertIs(node_stmt, node) - self.assertIs(target, target_stmt) # tailf:cli-diff-delete-before stmts = [i for i in node.substmts @@ -385,18 +377,28 @@ def test_ordering_stmt_tailf(self): annotation_substmt.keyword, ("tailf-common", "cli-when-target-create"), ) - self.assertIn( - annotation, - self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'], - ) - node_stmt, target_stmt, ordering = \ - self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'][annotation] + + tuples = [ + i + for i in self.compiler.ordering_stmt_tailf['Cisco-IOS-XE-native'] + if i[0] is node + ] + self.assertEqual(len(tuples), 2) target = self.compiler.context.check_data_tree_xpath( annotation, node) - - self.assertIs(node_stmt, node) - self.assertIs(target, target_stmt) + positions = [ + 'Cisco-IOS-XE-sla-ann.yang:76', + 'Cisco-IOS-XE-sla-ann.yang:79', + ] + for node_stmt, target_stmt, ordering, position in tuples: + self.assertIs(target, target_stmt) + self.assertIsInstance(ordering, list) + for p in positions: + if p in str(position): + positions.remove(p) + break + self.assertEqual(len(positions), 0) def test_datatype_leafref(self): # Line 159 in Cisco-IOS-XE-parser.yang: From 2ee839914539c05bf136b35b504152cfaaa5f920 Mon Sep 17 00:00:00 2001 From: yuekyang Date: Fri, 10 Apr 2026 13:08:56 -0400 Subject: [PATCH 09/11] ModelCompiler takes context in initialization --- src/ncdiff/manager.py | 4 +++- src/ncdiff/model.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ncdiff/manager.py b/src/ncdiff/manager.py index f4cea8b..06421a1 100755 --- a/src/ncdiff/manager.py +++ b/src/ncdiff/manager.py @@ -271,7 +271,9 @@ def scan_models(self, folder='./yang', download='check'): if download in ['check', 'force']: d = ModelDownloader(self, folder) d.download_all(check_before_download=(download == 'check')) - self.compiler = ModelCompiler(folder) + self.compiler = ModelCompiler(folder, context=d.context) + else: + self.compiler = ModelCompiler(folder) def load_model(self, model): '''load_model diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 459f3c6..a61272d 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -555,7 +555,7 @@ def run(self): class CompilerContext(Context): - def __init__(self, repository): + def __init__(self, repository, modeldevice=None): Context.__init__(self, repository) self.dependencies = None self.modulefile_queue = None @@ -563,7 +563,7 @@ def __init__(self, repository): self.num_threads = 2 else: self.num_threads = 1 - self._modeldevice = None + self.modeldevice = modeldevice def _get_latest_revision(self, modulename): latest = None @@ -836,10 +836,10 @@ def check_schema_tree_xpath(self, xpath_stmt): return node def get_xpath_from_schema_node(self, schema_node, type=Tag.XPATH): - if self._modeldevice is None: + if self.modeldevice is None: return None else: - return self._modeldevice.get_xpath(schema_node, type=type, instance=False) + return self.modeldevice.get_xpath(schema_node, type=type, instance=False) def load_context(self): self.modulefile_queue = queue.Queue() @@ -1089,7 +1089,7 @@ def __init__(self, nc_device, folder): 'capabilities.txt', ) repo = FileRepository(path=self.dir_yang) - self.context = CompilerContext(repository=repo) + self.context = CompilerContext(repository=repo, modeldevice=nc_device) self.download_queue = queue.Queue() self.num_threads = 2 @@ -1243,13 +1243,13 @@ class ModelCompiler(object): call pyang.error.err_to_str() to print out detailed error messages. ''' - def __init__(self, folder): + def __init__(self, folder, context=None): ''' __init__ instantiates a ModelCompiler instance. ''' self.dir_yang = os.path.abspath(folder) - self.context = None + self.context = context self.module_prefixes = {} self.module_namespaces = {} self.identity_deps = {} @@ -1566,9 +1566,9 @@ def depict_a_schema_node(self, module, parent, child, mode=None): def get_xpath_from_schema_node(self, schema_node, type=Tag.XPATH): from .manager import ModelDevice - if self.context._modeldevice is None: - self.context._modeldevice = ModelDevice(None, None) - self.context._modeldevice.compiler = self + if self.context.modeldevice is None: + self.context.modeldevice = ModelDevice(None, None) + self.context.modeldevice.compiler = self return self.context.get_xpath_from_schema_node(schema_node, type=type) @staticmethod From 15e7adaa09f9185b466414f15700a094e43279f0 Mon Sep 17 00:00:00 2001 From: yuekyang Date: Fri, 10 Apr 2026 20:07:07 -0400 Subject: [PATCH 10/11] Detect deprecated-without-replacement --- src/ncdiff/model.py | 13 ++- src/ncdiff/tailf.py | 19 ++-- src/ncdiff/tests/test_tailf_annotation.py | 49 +++++++++- src/ncdiff/tests/yang/Cisco-IOS-XE-lisp.yang | 93 ++++++++++--------- src/ncdiff/tests/yang/Cisco-IOS-XE-types.yang | 14 ++- 5 files changed, 127 insertions(+), 61 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index a61272d..0038ff8 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -21,6 +21,7 @@ from .errors import ModelError from .composer import Tag +from .tailf import is_deprecated_without_replacement from .tailf import is_tailf_ordering, get_tailf_ordering from .tailf import add_tailf_annotation, set_ordering_xpath @@ -1262,6 +1263,7 @@ def __init__(self, folder, context=None): self.exclude_obsolete = False self.exclude_deprecated = False + self.include_deprecated_without_replacement = False self.include_xpaths = set() self.exclude_xpaths = set() @@ -1476,6 +1478,8 @@ def depict_a_schema_node(self, module, parent, child, mode=None): sm = child.search_one('status') if sm is not None and sm.arg in ['deprecated', 'obsolete']: n.set('status', sm.arg) + if is_deprecated_without_replacement(child): + n.set('deprecated-without-replacement', 'true') if self.skip(child, n): parent.remove(n) @@ -1528,7 +1532,7 @@ def depict_a_schema_node(self, module, parent, child, mode=None): ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 ): - if not is_tailf_ordering(ch, self.context): + if not is_tailf_ordering(ch): add_tailf_annotation(self.module_namespaces, ch, n) else: target = self.context.check_data_tree_xpath( @@ -1719,11 +1723,16 @@ def skip(self, statement, schema_node): return True status = schema_node.get('status', default=None) + deprecated_without_replacement = schema_node.get( + 'deprecated-without-replacement', default=None) if ( status == 'obsolete' and self.exclude_obsolete or status == 'deprecated' and - self.exclude_deprecated + self.exclude_deprecated and not ( + self.include_deprecated_without_replacement and + deprecated_without_replacement == 'true' + ) ): return True return False diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index e9bebf7..b850b52 100755 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -8,15 +8,9 @@ logger = logging.getLogger(__name__) -def is_tailf_ordering(stmt, context): - if isinstance(stmt.raw_keyword, tuple): - prefix, identifier = stmt.raw_keyword - m, rev = util.prefix_to_modulename_and_revision( - stmt.i_orig_module, - prefix, - stmt.pos, - context.errors, - ) +def is_tailf_ordering(stmt): + if isinstance(stmt.keyword, tuple): + m, identifier = stmt.keyword return m == 'tailf-common' and identifier in { 'cli-diff-after', 'cli-diff-before', 'cli-diff-create-after', 'cli-diff-create-before', @@ -29,6 +23,13 @@ def is_tailf_ordering(stmt, context): return False +def is_deprecated_without_replacement(stmt): + for substmt in stmt.search(('Cisco-IOS-XE-types', 'yang-meta-data')): + if substmt.arg == 'deprecated-without-replacement': + return True + return False + + def get_tailf_ordering(context, stmt, target_stmt): symmetric = is_symmetric_tailf_ordering(context, stmt, target_stmt) if stmt.keyword[1] in ['cli-diff-after', 'cli-diff-before']: diff --git a/src/ncdiff/tests/test_tailf_annotation.py b/src/ncdiff/tests/test_tailf_annotation.py index 7209201..740cdaa 100755 --- a/src/ncdiff/tests/test_tailf_annotation.py +++ b/src/ncdiff/tests/test_tailf_annotation.py @@ -7,6 +7,7 @@ from ncdiff.model import ModelCompiler from ncdiff.tailf import is_tailf_ordering, get_tailf_ordering from ncdiff.tailf import is_symmetric_tailf_ordering +from ncdiff.tailf import is_deprecated_without_replacement curr_dir = os.path.dirname(os.path.abspath(__file__)) @@ -24,6 +25,9 @@ class TestNative(unittest.TestCase): @classmethod def setUpClass(cls): cls.compiler = ModelCompiler(os.path.join(curr_dir, 'yang')) + cls.compiler.exclude_obsolete = True + cls.compiler.exclude_deprecated = True + cls.compiler.include_deprecated_without_replacement = True delete_xml_files(cls.compiler.dir_yang) cls.native = cls.compiler.compile('Cisco-IOS-XE-native') # cls.oc_interfaces = cls.compiler.compile('openconfig-interfaces') @@ -445,7 +449,7 @@ def test_has_tailf_ordering(self): self.assertEqual(len(stmts), 1) stmt = stmts[0] node = stmt - func_result = is_tailf_ordering(node, self.compiler.context) + func_result = is_tailf_ordering(node) self.assertFalse(func_result) stmts = [i for i in node.substmts @@ -454,7 +458,7 @@ def test_has_tailf_ordering(self): "/ios-eth:carrier-delay/ios-eth:seconds"] self.assertEqual(len(stmts), 1) annotation = stmts[0] - func_result = is_tailf_ordering(annotation, self.compiler.context) + func_result = is_tailf_ordering(annotation) self.assertTrue(func_result) def test_get_tailf_ordering(self): @@ -546,6 +550,47 @@ def test_is_symmetric_tailf_ordering(self): func_result = is_symmetric_tailf_ordering(self.compiler.context, annotation, target) self.assertFalse(func_result) + def test_is_deprecated_without_replacement(self): + # Line 1523 in Cisco-IOS-XE-lisp.yang: + # grouping router-lisp-ip-grouping { + # leaf alt-vrf { + # description + # "Activate LISP-ALT functionality in VRF"; + # status deprecated; + # ios-types:yang-meta-data "deprecated-without-replacement"; + # type string; + # } + # ... + # } + module_stmt = self.compiler.context.get_module('Cisco-IOS-XE-native') + stmts = [i for i in module_stmt.substmts if i.arg == "native"] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + for arg in [ + "router", + "lisp", + "ipv4", + "alt-vrf", + ]: + stmts = [i for i in stmt.i_children if i.arg == arg] + self.assertEqual(len(stmts), 1) + stmt = stmts[0] + + # Node ipv4 does not have deprecated-without-replacement + func_result = is_deprecated_without_replacement(stmt.parent) + self.assertFalse(func_result) + + # Node alt-vrf has deprecated-without-replacement + func_result = is_deprecated_without_replacement(stmt) + self.assertTrue(func_result) + + # Check that the alt-vrf node is present in the compiled tree, even + # though it is deprecated + nodes = self.native.tree.xpath( + "//ios:native/ios:router/ios-lisp:lisp/ios-lisp:ipv4/ios-lisp:alt-vrf", + namespaces=self.native.prefixes) + self.assertEqual(len(nodes), 1) + class TestOpenConfigInterfaces(unittest.TestCase): diff --git a/src/ncdiff/tests/yang/Cisco-IOS-XE-lisp.yang b/src/ncdiff/tests/yang/Cisco-IOS-XE-lisp.yang index f485164..7f9a844 100644 --- a/src/ncdiff/tests/yang/Cisco-IOS-XE-lisp.yang +++ b/src/ncdiff/tests/yang/Cisco-IOS-XE-lisp.yang @@ -71,7 +71,7 @@ module Cisco-IOS-XE-lisp { } - //router lisp new grouping starts + //router lisp new grouping starts //router lisp service route import database protocol grouping router-lisp-inst-service-ip-route-import-database-protocol-grouping { container application { @@ -339,13 +339,13 @@ module Cisco-IOS-XE-lisp { } } - //router lisp inst service ipv6 router-import + //router lisp inst service ipv6 router-import grouping router-lisp-inst-service-ipv6-route-import-protocol-grouping { uses router-lisp-inst-service-ipv6-route-import-database-protocol-grouping; uses router-lisp-inst-service-ipv6-route-import-map-cache-protocol-grouping; } - //router lisp inst service ipv4 router-import + //router lisp inst service ipv4 router-import grouping router-lisp-inst-service-ipv4-route-import-protocol-grouping { uses router-lisp-inst-service-ipv4-route-import-database-protocol-grouping; uses router-lisp-inst-service-ipv4-route-import-map-cache-protocol-grouping; @@ -373,7 +373,7 @@ module Cisco-IOS-XE-lisp { } } } - + //router lisp etr grouping router-lisp-etr-grouping { container etr-enable { @@ -401,7 +401,7 @@ module Cisco-IOS-XE-lisp { } } } - } + } //router lisp database mapping limit grouping router-lisp-database-mapping-limit-grouping { @@ -422,7 +422,7 @@ module Cisco-IOS-XE-lisp { range "1..100"; } } - } + } } //router lisp map cache @@ -466,7 +466,7 @@ module Cisco-IOS-XE-lisp { type inet:ipv6-address; } } - + //router lisp map request source any grouping router-lisp-map-request-source-any-grouping { leaf map-request-source { @@ -550,7 +550,7 @@ module Cisco-IOS-XE-lisp { } } - //router lisp service common + //router lisp service common grouping router-lisp-service-common-grouping { container database-mapping { description @@ -686,8 +686,8 @@ module Cisco-IOS-XE-lisp { uses router-lisp-map-cache-persistent-grouping; uses router-lisp-proxy-grouping; uses router-lisp-route-export-grouping; - uses router-lisp-sgt-grouping; - uses router-lisp-use-petr-grouping; + uses router-lisp-sgt-grouping; + uses router-lisp-use-petr-grouping; } //router lisp service ipv4 @@ -702,7 +702,7 @@ module Cisco-IOS-XE-lisp { uses router-lisp-map-request-source-ipv6-grouping; } - //router lisp four key + //router lisp four key grouping router-lisp-four-key-grouping { leaf unc-pwd { description @@ -724,10 +724,10 @@ module Cisco-IOS-XE-lisp { "The ENCRYPTED password"; type string; } - } + } //router lisp key hash function - grouping router-lisp-key-hash-function-grouping { + grouping router-lisp-key-hash-function-grouping { leaf hash-function { description "authentication type"; @@ -757,8 +757,8 @@ module Cisco-IOS-XE-lisp { uses router-lisp-key-hash-function-grouping; } } - - //router lisp passowd key-7 + + //router lisp passowd key-7 grouping router-lisp-password-key-7-grouping { container key-7 { leaf ak-7 { @@ -867,7 +867,7 @@ module Cisco-IOS-XE-lisp { } } } - + //router lisp use-petr grouping router-lisp-use-petr-grouping { list use-petr { @@ -906,28 +906,28 @@ module Cisco-IOS-XE-lisp { "LISP routes installed in the ALT table"; type uint8 { range 1..255; - } + } } leaf away { description "Administrative distance for RIB route installation"; type uint8 { range 1..255; - } + } } leaf dyn-eid { description "LISP installed routes of type dynamic-EID"; type uint8 { range 1..255; - } + } } leaf site-registrations { description "LISP installed routes of type site-registrations"; type uint8 { range 1..255; - } + } } } } @@ -961,7 +961,7 @@ module Cisco-IOS-XE-lisp { description "Configures which Locators from a set are preferred"; type uint8 { range 0..255; - } + } } leaf weight { description "Traffic load-spreading among Locators"; @@ -969,14 +969,14 @@ module Cisco-IOS-XE-lisp { range 0..100; } } - leaf down { + leaf down { description "Configure this database mapping down"; type empty; } } } - //router lisp inst database mapping common + //router lisp inst database mapping common grouping router-lisp-inst-database-mapping-common-grouping { leaf locator-set { description @@ -997,7 +997,7 @@ module Cisco-IOS-XE-lisp { key "address"; leaf address { type inet:ipv6-address; - } + } uses router-lisp-inst-database-mapping-option-grouping; } @@ -1034,7 +1034,7 @@ module Cisco-IOS-XE-lisp { //router lisp inst service ethernet grouping router-lisp-inst-service-ethernet-grouping { container eid-table { - description "Bind an eid-table"; + description "Bind an eid-table"; leaf vlan { description "VLAN configuration"; type uint16 { @@ -1042,7 +1042,7 @@ module Cisco-IOS-XE-lisp { } } } - container broadcast-underlay { + container broadcast-underlay { description "Multicast group to use for underlay"; leaf ipv4-multicast { description "IPv4 multicast group address"; @@ -1187,9 +1187,9 @@ module Cisco-IOS-XE-lisp { uses router-lisp-map-cache-persistent-grouping; uses router-lisp-proxy-grouping; uses router-lisp-route-export-grouping; - uses router-lisp-sgt-grouping; - uses router-lisp-use-petr-grouping; - } + uses router-lisp-sgt-grouping; + uses router-lisp-use-petr-grouping; + } //router lisp inst service ipv4 grouping grouping router-lisp-inst-service-ipv4-grouping { @@ -1243,7 +1243,7 @@ module Cisco-IOS-XE-lisp { } } - //router lisp inst + //router lisp inst grouping router-lisp-inst-grouping { container decapsulation { description @@ -1421,7 +1421,7 @@ module Cisco-IOS-XE-lisp { } } container service { - description + description "Configure lisp service type"; presence true; container ipv4 { @@ -1444,11 +1444,11 @@ module Cisco-IOS-XE-lisp { uses router-lisp-inst-service-ethernet-grouping; } uses router-lisp-inst-service-ethernet-grouping; - } + } } } - //router lisp new grouping ends + //router lisp new grouping ends grouping router-lisp-ip-route-import-map-cache-grouping { container map-cache-container { @@ -1462,7 +1462,7 @@ module Cisco-IOS-XE-lisp { } grouping router-lisp-ip-route-import-database-grouping { - container lisp-ip-route-import { + container lisp-ip-route-import { leaf route-map { description "Route map for route selection filtering"; @@ -1525,6 +1525,7 @@ module Cisco-IOS-XE-lisp { description "Activate LISP-ALT functionality in VRF"; status deprecated; + ios-types:yang-meta-data "deprecated-without-replacement"; type string; } container database-mapping { @@ -2173,10 +2174,10 @@ module Cisco-IOS-XE-lisp { description "Configures a LISP Egress Tunnel Router (ETR)"; container map-server { - description + description "Configures map server for ETR registration"; leaf source-address { - description + description "Configures map server source address"; type string; } @@ -2393,7 +2394,7 @@ module Cisco-IOS-XE-lisp { } } - //router lisp site common grouping + //router lisp site common grouping grouping router-lisp-site-common-grouping { container authentication-key { description @@ -2451,13 +2452,13 @@ module Cisco-IOS-XE-lisp { description "Accept registrations for any L2 EID records"; type empty; - } + } } leaf any-mac { description "Accept registrations for any L2 EID records"; type empty; - } + } } container eid-record { description @@ -2477,13 +2478,13 @@ module Cisco-IOS-XE-lisp { description "Accept registrations for any L2 EID records"; type empty; - } + } } leaf any-mac { description "Accept registrations for any L2 EID records"; type empty; - } + } } leaf site-id { description @@ -2492,9 +2493,9 @@ module Cisco-IOS-XE-lisp { range "0..4294967295"; } } - } - - //router lisp site grouping + } + + //router lisp site grouping grouping rouer-lisp-site-grouping { list site { description @@ -2507,7 +2508,7 @@ module Cisco-IOS-XE-lisp { } container default { uses router-lisp-site-common-grouping; - } + } uses router-lisp-site-common-grouping; } } @@ -2827,7 +2828,7 @@ module Cisco-IOS-XE-lisp { type empty; } } - + uses rouer-lisp-site-grouping; leaf site-id { diff --git a/src/ncdiff/tests/yang/Cisco-IOS-XE-types.yang b/src/ncdiff/tests/yang/Cisco-IOS-XE-types.yang index 3533b39..67593de 100644 --- a/src/ncdiff/tests/yang/Cisco-IOS-XE-types.yang +++ b/src/ncdiff/tests/yang/Cisco-IOS-XE-types.yang @@ -26,6 +26,16 @@ module Cisco-IOS-XE-types { Copyright (c) 2016-2017 by Cisco Systems, Inc. All rights reserved."; + // ========================================================================= + // EXTENSION + // ========================================================================= + + extension yang-meta-data { + argument value; + description "Extra information associated with the yang node to tell + model compiler to compile it in a specific way"; + } + // ========================================================================= // REVISION // ========================================================================= @@ -685,8 +695,8 @@ module Cisco-IOS-XE-types { } } } - - // Comma-separated numbers with ranges + + // Comma-separated numbers with ranges typedef range-string { type string { pattern From 4b603830c400a2d8e2cb695c7b02b07c68153350 Mon Sep 17 00:00:00 2001 From: yuekyang Date: Mon, 13 Apr 2026 15:41:50 -0400 Subject: [PATCH 11/11] Cleanup --- src/ncdiff/model.py | 13 ++++++++++--- src/ncdiff/tailf.py | 8 +++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/ncdiff/model.py b/src/ncdiff/model.py index 0038ff8..3ee8de1 100755 --- a/src/ncdiff/model.py +++ b/src/ncdiff/model.py @@ -680,8 +680,8 @@ def update_dependencies(self, module_statement): for stmt in module_statement.search(node_name): for substmt in stmt.substmts: if ( + isinstance(substmt.keyword, tuple) and 'tailf' in substmt.keyword[0] and - len(substmt.keyword) == 2 and substmt.keyword[1] == 'hidden' ): break @@ -876,7 +876,11 @@ def tailf_annotate(context, annotating_stmt): def tailf_annotate_module(context, module_stmt): for substmt in module_stmt.substmts: - if substmt.keyword == ('tailf', 'annotate-module'): + if ( + isinstance(substmt.keyword, tuple) and + 'tailf' in substmt.keyword[0] and + substmt.keyword[1] == 'annotate-module' + ): annotated_module = context.get_module(substmt.arg) if annotated_module is None: logger.warning("Failed to find annotated module " @@ -1527,7 +1531,10 @@ def depict_a_schema_node(self, module, parent, child, mode=None): # Tailf annotations for ch in child.substmts: - if isinstance(ch.keyword, tuple) and 'tailf' in ch.keyword[0]: + if ( + isinstance(ch.keyword, tuple) and + ch.keyword[0] == 'tailf-common' + ): if ( ch.keyword[0] in self.module_namespaces and len(ch.keyword) == 2 diff --git a/src/ncdiff/tailf.py b/src/ncdiff/tailf.py index b850b52..2c9b8a2 100755 --- a/src/ncdiff/tailf.py +++ b/src/ncdiff/tailf.py @@ -466,8 +466,8 @@ def get_xpath(compiler, stmt): if stmt[1] not in tailf_ordering[stmt[0]]: tailf_ordering[stmt[0]][stmt[1]] = True - # Skip entries where either node has TailF ordering annotations - # applied. + # Skip leafref entries where it already has TailF ordering + # annotations applied. if constraint_type == "ordering_stmt_leafref": if ( stmt[0] in tailf_ordering and @@ -476,9 +476,11 @@ def get_xpath(compiler, stmt): continue for oper_0, sequence, oper_1 in cinstraint_list: + + # Skip entries with same Xpath and same operation. if xpath[0] == xpath[1] and oper_0 == oper_1: - # Skip entries with same Xpath and same operation. continue + if sequence == 'before': constraints.append(( xpath[0], oper_0, xpath[1], oper_1, position))