diff --git a/src/sonic-yang-mgmt/sonic_yang.py b/src/sonic-yang-mgmt/sonic_yang.py index 0cb8bb6414d8..5e9619ba8caf 100644 --- a/src/sonic-yang-mgmt/sonic_yang.py +++ b/src/sonic-yang-mgmt/sonic_yang.py @@ -197,6 +197,22 @@ def _load_data_model(self, yang_dir, yang_files, data_files, output=None): return (self.ctx, self.root) + """ + load_module_str_name(): load a module based on the provided string and return + the loaded module name. This is needed by + sonic-utilities to prevent direct dependency on + libyang. + input: yang_module_str yang-formatted module + returns: module name on success, exception on failure + """ + def load_module_str_name(self, yang_module_str): + try: + module = self.ctx.parse_module_mem(yang_module_str, ly.LYS_IN_YANG) + except Exception as e: + self.fail(e) + + return module.name() + """ print_data_mem(): print the data tree input: option: "JSON" or "XML" @@ -229,7 +245,7 @@ def _get_module_tree(self, module_name, format): try: module = self.ctx.get_module(str(module_name)) except Exception as e: - self.sysLog(msg="Cound not get module: " + str(module_name), debug=syslog.LOG_ERR, doPrint=True) + self.sysLog(msg="Could not get module: " + str(module_name), debug=syslog.LOG_ERR, doPrint=True) self.fail(e) else: if (module is not None): @@ -364,7 +380,7 @@ def _find_schema_node(self, schema_xpath): return None else: for schema_node in schema_set.schema(): - if schema_xapth == schema_node.path(): + if schema_xpath == schema_node.path(): return schema_node return None """ @@ -517,7 +533,7 @@ def find_schema_must_count(self, schema_xpath, match_ancestors: bool=False): try: schema_node = self._find_schema_node(schema_xpath) except Exception as e: - self.sysLog(msg="Cound not find the schema node from xpath: " + str(schema_xpath), debug=syslog.LOG_ERR, doPrint=True) + self.sysLog(msg="Could not find the schema node from xpath: " + str(schema_xpath), debug=syslog.LOG_ERR, doPrint=True) self.fail(e) return 0 @@ -582,10 +598,28 @@ def find_schema_dependencies(self, schema_xpath, match_ancestors: bool=False): return result ref_list = [] + if schema_xpath is None or len(schema_xpath) == 0 or schema_xpath == "/": + if not match_ancestors: + return ref_list + + # Iterate across all modules, can't use "/" + for module in self.ctx.get_module_iter(): + if module.data() is None: + continue + + module_list = [] + try: + module_list = self.find_schema_dependencies(module.data().path(), match_ancestors=match_ancestors) + except Exception as e: + self.sysLog(msg=f"Exception while finding schema dependencies for module {module.name()}: {str(e)}", debug=syslog.LOG_ERR, doPrint=True) + + ref_list.extend(module_list) + return ref_list + try: schema_node = self._find_schema_node(schema_xpath) except Exception as e: - self.sysLog(msg="Cound not find the schema node from xpath: " + str(schema_xpath), debug=syslog.LOG_ERR, doPrint=True) + self.sysLog(msg=f"Could not find the schema node from xpath: {str(schema_xpath)}: {str(e)}", debug=syslog.LOG_ERR, doPrint=True) self.fail(e) return ref_list @@ -632,36 +666,80 @@ def __find_schema_dependencies_only(self, schema_node): return ref_list """ - find_data_dependencies(): find the data dependencies from data xpath - input: data_xpath - xpath of data node. (Public) - returns: - list of xpath - - Exception if error + find_data_dependencies(): find the data dependencies from data xpath (Public) + input: data_xpath - xpath to search. If it references an exact data node + only the references to that data node will be returned. + If a path contains multiple data nodes, then all references + to all child nodes will be returned. If set to None (or "" or "/"), + will return all references, globally. """ def find_data_dependencies(self, data_xpath): ref_list = [] - node = self.root - try: - data_node = self._find_data_node(data_xpath) - except Exception as e: - self.sysLog(msg="find_data_dependencies(): Failed to find data node from xpath: {}".format(data_xapth), debug=syslog.LOG_ERR, doPrint=True) - return ref_list + required_value = None + base_dnode = None + search_xpath = None + + if data_xpath is None or len(data_xpath) == 0 or data_xpath == "/": + data_xpath = None + search_xpath = "/" + + if data_xpath is not None: + dnode_list = [] + try: + dnode_list = list(self.root.find_path(data_xpath).data()) + except Exception as e: + # We don't care the reason for the failure, this is caught in + # the next statement. + pass + + if len(dnode_list) == 0: + self.sysLog(msg="find_data_dependencies(): Failed to find data node from xpath: {}".format(data_xpath), debug=syslog.LOG_ERR, doPrint=True) + return ref_list + + base_dnode = dnode_list[0] + if base_dnode.schema() is None: + return ref_list + + search_xpath = base_dnode.schema().path() + + # If exactly 1 node and it's a data node, we need to match the value. + if len(dnode_list) == 1: + try: + required_value = self._find_data_node_value(data_xpath) + except Exception as e: + # Might not be a data node, ignore + pass + + # Get a list of all schema leafrefs pointing to this node (or these data nodes). + lreflist = [] try: - value = str(self._find_data_node_value(data_xpath)) - - backlinks = self.find_schema_dependencies(data_node.schema().path(), False) - if backlinks is not None and len(backlinks) > 0: - for link in backlinks: - node_set = node.find_path(link) - for data_set in node_set.data(): - data_set.schema() - casted = data_set.subtype() - if value == casted.value_str(): - ref_list.append(data_set.path()) + match_ancestors = True + if required_value is not None: + match_ancestors = False + + lreflist = self.find_schema_dependencies(search_xpath, match_ancestors=match_ancestors) + if lreflist is None: + raise Exception("no schema backlinks found") except Exception as e: - self.sysLog(msg='Failed to find node or dependencies for {}'.format(data_xpath), debug=syslog.LOG_ERR, doPrint=True) - raise SonicYangException("Failed to find node or dependencies for \ - {}\n{}".format(data_xpath, str(e))) + self.sysLog(msg='Failed to find node or dependencies for {}: {}'.format(data_xpath, str(e)), debug=syslog.LOG_ERR, doPrint=True) + lreflist = [] + # Exception not expected by existing tests if backlinks not found, so don't raise. + # raise SonicYangException("Failed to find node or dependencies for {}\n{}".format(data_xpath, str(e))) + + # For all found data nodes, emit the path to the data node. If we need to + # restrict to a value, do so. + for lref in lreflist: + try: + data_set = self.root.find_path(lref).data() + for dnode in data_set: + if required_value is None or ( + dnode.subtype() is not None and dnode.subtype().value_str() == required_value + ): + ref_list.append(dnode.path()) + except Exception as e: + # Possible no data paths matched, ignore + pass return ref_list diff --git a/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json index 998893b7ff46..502ffcc98fdc 100644 --- a/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json @@ -56,6 +56,52 @@ ], "xpath" : "/test-port:test-port/PORT/PORT_LIST[port_name='Ethernet8']/port_name" }, + { + "dependencies" : [ + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V6'][RULE_NAME='Rule_20']/ACL_TABLE_NAME", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-test'][RULE_NAME='rule_20']/ACL_TABLE_NAME", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V4'][RULE_NAME='Rule_20']/ACL_TABLE_NAME", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V4'][RULE_NAME='Rule_40']/ACL_TABLE_NAME" + ], + "xpath" : "/test-acl:test-acl/ACL_TABLE" + }, + { + "dependencies" : [ + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V6']/ports[.='Ethernet8']", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet2']/port", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet3']/vlanid", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='555'][ip-prefix='fe80::/10']/vlanid", + "/test-interface:test-interface/INTERFACE/INTERFACE_LIST[interface='Ethernet8'][ip-prefix='10.1.1.64/26']/interface", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='2000:f500:45:6709::/64']/vlanid", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet5']/vlanid", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet1']/port", + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V4']/ports[.='Ethernet0']", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet6']/port", + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V4']/ports[.='Ethernet2']", + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V6']/ports[.='Ethernet7']", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet3']/port", + "/test-interface:test-interface/INTERFACE/INTERFACE_LIST[interface='Ethernet8'][ip-prefix='2000:f500:40:a749::/126']/interface", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-test'][RULE_NAME='rule_20']/ACL_TABLE_NAME", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet0']/port", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet5']/port", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet1']/vlanid", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='fe80::/10']/vlanid", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet4']/port", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V4'][RULE_NAME='Rule_20']/ACL_TABLE_NAME", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet2']/vlanid", + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V6']/ports[.='Ethernet9']", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet6']/vlanid", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V4'][RULE_NAME='Rule_40']/ACL_TABLE_NAME", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet0']/vlanid", + "/test-acl:test-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-V6'][RULE_NAME='Rule_20']/ACL_TABLE_NAME", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/vlanid", + "/test-vlan:test-vlan/VLAN_MEMBER/VLAN_MEMBER_LIST[vlanid='111'][port='Ethernet4']/vlanid", + "/test-acl:test-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V4']/ports[.='Ethernet1']", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='555'][ip-prefix='2000:f500:41:4e9::/64']/vlanid", + "/test-vlan:test-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='555'][ip-prefix='10.1.5.64/26']/vlanid" + ], + "xpath" : "/" + }, { "dependencies" : [], "xpath" : "/test-port:test-port/PORT/PORT_LIST[port_name='Ethernet8']/alias" diff --git a/src/sonic-yang-mgmt/tests/libyang-python-tests/test_sonic_yang.py b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_sonic_yang.py index 9d773680ce8d..d7261e8382be 100644 --- a/src/sonic-yang-mgmt/tests/libyang-python-tests/test_sonic_yang.py +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_sonic_yang.py @@ -88,6 +88,22 @@ def test_load_invalid_model_files(self, data, yang_s): with pytest.raises(Exception): assert self.load_yang_model_file(yang_s, yang_dir, file, module) + # test load_module_str_name on test-acl.yang + def test_load_module_str_name(self, data, yang_s): + yang_dir = data['yang_dir'] + file = "test-acl.yang" + + try: + with open(f'{yang_dir}/test-acl.yang', 'r') as f: + content = f.read() + + name = yang_s.load_module_str_name(content) + except Exception as e: + print(e) + raise + + assert name == "test-acl" + #test load yang modules in directory def test_load_yang_model_dir(self, data, yang_s): yang_dir = data['yang_dir']