diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2 index f62ef44f0fc0..ed1281619896 100644 --- a/files/build_templates/sonic_debian_extension.j2 +++ b/files/build_templates/sonic_debian_extension.j2 @@ -110,6 +110,15 @@ sudo cp {{swsssdk_py2_wheel_path}} $FILESYSTEM_ROOT/$SWSSSDK_PY2_WHEEL_NAME sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip install $SWSSSDK_PY2_WHEEL_NAME sudo rm -rf $FILESYSTEM_ROOT/$SWSSSDK_PY2_WHEEL_NAME +# Install sonic-yang-mgmt Python 2 package, install dependencies +sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/libyang_*.deb +sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/libyang-cpp_*.deb +sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/python2-yang_*.deb +SONIC_YANG_MGMT_PY2_WHEEL_NAME=$(basename {{sonic_yang_mgmt_py2_wheel_path}}) +sudo cp {{sonic_yang_mgmt_py2_wheel_path}} $FILESYSTEM_ROOT/$SONIC_YANG_MGMT_PY2_WHEEL_NAME +sudo https_proxy=$https_proxy LANG=C chroot $FILESYSTEM_ROOT pip install $SONIC_YANG_MGMT_PY2_WHEEL_NAME +sudo rm -rf $FILESYSTEM_ROOT/$SONIC_YANG_MGMT_PY2_WHEEL_NAME + # Install sonic-platform-common Python 2 package PLATFORM_COMMON_PY2_WHEEL_NAME=$(basename {{platform_common_py2_wheel_path}}) sudo cp {{platform_common_py2_wheel_path}} $FILESYSTEM_ROOT/$PLATFORM_COMMON_PY2_WHEEL_NAME diff --git a/rules/sonic-yang-mgmt-py2.mk b/rules/sonic-yang-mgmt-py2.mk index dca3fb5e3303..c62dedd5b637 100644 --- a/rules/sonic-yang-mgmt-py2.mk +++ b/rules/sonic-yang-mgmt-py2.mk @@ -3,4 +3,6 @@ SONIC_YANG_MGMT_PY2 = sonic_yang_mgmt-1.0-py2-none-any.whl $(SONIC_YANG_MGMT_PY2)_SRC_PATH = $(SRC_PATH)/sonic-yang-mgmt $(SONIC_YANG_MGMT_PY2)_PYTHON_VERSION = 2 +$(SONIC_YANG_MGMT_PY2)_DEBS_DEPENDS = $(LIBYANG) + SONIC_PYTHON_WHEELS += $(SONIC_YANG_MGMT_PY2) diff --git a/slave.mk b/slave.mk index f81895f74dd2..e1d094dadfb6 100644 --- a/slave.mk +++ b/slave.mk @@ -440,7 +440,8 @@ SONIC_TARGET_LIST += $(addprefix $(PYTHON_DEBS_PATH)/, $(SONIC_PYTHON_STDEB_DEBS # $(SOME_NEW_WHL)_PYTHON_VERSION = 2 (or 3) # $(SOME_NEW_WHL)_DEPENDS = $(SOME_OTHER_WHL1) $(SOME_OTHER_WHL2) ... # SONIC_PYTHON_WHEELS += $(SOME_NEW_WHL) -$(addprefix $(PYTHON_WHEELS_PATH)/, $(SONIC_PYTHON_WHEELS)) : $(PYTHON_WHEELS_PATH)/% : .platform $$(addsuffix -install,$$(addprefix $(PYTHON_WHEELS_PATH)/,$$($$*_DEPENDS))) +$(addprefix $(PYTHON_WHEELS_PATH)/, $(SONIC_PYTHON_WHEELS)) : $(PYTHON_WHEELS_PATH)/% : .platform $$(addsuffix -install,$$(addprefix $(PYTHON_WHEELS_PATH)/,$$($$*_DEPENDS))) \ + $$(addsuffix -install,$$(addprefix $(DEBS_PATH)/,$$($$*_DEBS_DEPENDS))) $(HEADER) pushd $($*_SRC_PATH) $(LOG) # apply series of patches if exist @@ -624,7 +625,8 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \ $(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_CONFIG_ENGINE)) \ $(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_PLATFORM_COMMON_PY2)) \ $(addprefix $(PYTHON_WHEELS_PATH)/,$(REDIS_DUMP_LOAD_PY2)) \ - $(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_PLATFORM_API_PY2)) + $(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_PLATFORM_API_PY2)) \ + $(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_YANG_MGMT_PY2)) $(HEADER) # Pass initramfs and linux kernel explicitly. They are used for all platforms export debs_path="$(STRETCH_DEBS_PATH)" @@ -649,6 +651,7 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \ export platform_common_py2_wheel_path="$(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_PLATFORM_COMMON_PY2))" export redis_dump_load_py2_wheel_path="$(addprefix $(PYTHON_WHEELS_PATH)/,$(REDIS_DUMP_LOAD_PY2))" export install_debug_image="$(INSTALL_DEBUG_TOOLS)" + export sonic_yang_mgmt_py2_wheel_path="$(addprefix $(PYTHON_WHEELS_PATH)/,$(SONIC_YANG_MGMT_PY2))" $(foreach docker, $($*_DOCKERS),\ export docker_image="$(docker)" diff --git a/src/sonic-yang-mgmt/setup.py b/src/sonic-yang-mgmt/setup.py index 71d242366aa1..e98e82129fd6 100644 --- a/src/sonic-yang-mgmt/setup.py +++ b/src/sonic-yang-mgmt/setup.py @@ -7,17 +7,17 @@ from setuptools.command.build_py import build_py from os import system from sys import exit +import pytest +import os # important reuirements parameters build_requirements = ['../../target/debs/stretch/libyang_1.0.73_amd64.deb', - '../../target/debs/stretch/libyang-cpp_1.0.73_amd64.deb', - '../../target/debs/stretch/python2-yang_1.0.73_amd64.deb'] + '../../target/debs/stretch/libyang-cpp_1.0.73_amd64.deb', + '../../target/debs/stretch/python2-yang_1.0.73_amd64.deb',] -install_requirements = [] +setup_requirements = ['pytest-runner'] -setup_requirements = ['pytest-runner',] - -test_requirements = ['pytest>=3', ] +test_requirements = ['pytest>=3'] # read me with open('README.rst') as readme_file: @@ -39,9 +39,10 @@ def run (self): if 'target/debs'in req: pkg_install_cmd = "sudo dpkg -i {}".format(req) if (system(pkg_install_cmd)): - print("{} installed".format(req)) + print("{} installation failed".format(req)) + exit(1) else: - print("{} installtion failed".format(req)) + print("{} installed".format(req)) # run tests for yang models test_yang_cmd = "python {} -f {} -y {}".format(yang_test_py, test_yangJson_file, yang_model_dir) @@ -53,8 +54,13 @@ def run (self): print("YANG Tests passed\n") # Continue usual build steps - build_py.run(self) + # run pytest for libyang python APIs + self.pytest_args = [] + errno = pytest.main(self.pytest_args) + if (errno): + exit(errno) + build_py.run(self) setup( cmdclass={ @@ -77,12 +83,13 @@ def run (self): 'Programming Language :: Python :: 3.8', ], description="Package contains YANG models for sonic.", - install_requires=install_requirements, + tests_require = test_requirements, license="GNU General Public License v3", long_description=readme + '\n\n', include_package_data=True, keywords='sonic_yang_mgmt', name='sonic_yang_mgmt', + py_modules=['sonic_yang'], packages=find_packages(), setup_requires=setup_requirements, version='1.0', diff --git a/src/sonic-yang-mgmt/sonic_yang.py b/src/sonic-yang-mgmt/sonic_yang.py new file mode 100644 index 000000000000..8e82f68d4726 --- /dev/null +++ b/src/sonic-yang-mgmt/sonic_yang.py @@ -0,0 +1,490 @@ +import yang as ly +import sys +import json +import glob + +""" +Yang schema and data tree python APIs based on libyang python +""" +class sonic_yang: + def __init__(self, yang_dir): + self.yang_dir = yang_dir + self.ctx = None + self.module = None + self.root = None + + try: + self.ctx = ly.Context(yang_dir) + except Exception as e: + self.fail(e) + + def fail(self, e): + print(e) + raise e + + """ + load_schema_module(): load a Yang model file + input: yang_file - full path of a Yang model file + returns: Exception if error + """ + def load_schema_module(self, yang_file): + try: + self.ctx.parse_module_path(yang_file, ly.LYS_IN_YANG) + except Exception as e: + print("Failed to load yang module file: " + yang_file) + self.fail(e) + + """ + load_schema_module_list(): load all Yang model files in the list + input: yang_files - a list of Yang model file full path + returns: Exception if error + """ + def load_schema_module_list(self, yang_files): + for file in yang_files: + try: + self.load_schema_module(file) + except Exception as e: + self.fail(e) + + """ + load_schema_modules(): load all Yang model files in the directory + input: yang_dir - the directory of the yang model files to be loaded + returns: Exception if error + """ + def load_schema_modules(self, yang_dir): + py = glob.glob(yang_dir+"/*.yang") + for file in py: + try: + self.load_schema_module(file) + except Exception as e: + self.fail(e) + + """ + load_schema_modules_ctx(): load all Yang model files in the directory to context: ctx + input: yang_dir, context + returns: Exception if error, returrns context object if no error + """ + def load_schema_modules_ctx(self, yang_dir=None): + if not yang_dir: + yang_dir = self.yang_dir + + ctx = ly.Context(yang_dir) + + py = glob.glob(yang_dir+"/*.yang") + for file in py: + try: + ctx.parse_module_path(str(file), ly.LYS_IN_YANG) + except Exception as e: + print("Failed to parse yang module file: " + file) + self.fail(e) + + return ctx + + """ + load_data_file(): load a Yang data json file + input: data_file - the full path of the yang json data file to be loaded + returns: Exception if error + """ + def load_data_file(self, data_file): + try: + node = self.ctx.parse_data_path(data_file, ly.LYD_JSON, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) + except Exception as e: + print("Failed to load data file: " + str(data_file)) + self.fail(e) + else: + self.root = node + + """ + get module name from xpath + input: path + returns: module name + """ + def get_module_name(self, schema_xpath): + module_name = schema_xpath.split(':')[0].strip('/') + return module_name + + """ + get_module(): get module object from Yang module name + input: yang module name + returns: Schema_Node object + """ + def get_module(self, module_name): + mod = self.ctx.get_module(module_name) + return mod + + """ + load_data_model(): load both Yang module fileis and data json files + input: yang directory, list of yang files and list of data files (full path) + returns: returns (context, root) if no error, or Exception if failed + """ + def load_data_model (self, yang_dir, yang_files, data_files, output=None): + if (self.ctx is None): + self.ctx = ly.Context(yang_dir) + + try: + self.load_schema_module_list(yang_files) + if len(data_files) == 0: + return (self.ctx, self.root) + + self.load_data_file(data_files[0]) + + for i in range(2, len(data_files)): + self.merge_data(data_files[i]) + except Exception as e: + print("Failed to load data files") + self.fail(e) + return + + if output is not None: + self.print_data_mem(output) + + return (self.ctx, self.root) + + """ + print_data_mem(): print the data tree + input: option: "JSON" or "XML" + """ + def print_data_mem (self, option): + if (option == "JSON"): + mem = self.root.print_mem(ly.LYD_JSON, ly.LYP_WITHSIBLINGS | ly.LYP_FORMAT) + else: + mem = self.root.print_mem(ly.LYD_XML, ly.LYP_WITHSIBLINGS | ly.LYP_FORMAT) + + print("======================= print data =================") + print(mem) + + """ + save_data_file_json(): save the data tree in memory into json file + input: outfile - full path of the file to save the data tree to + """ + def save_data_file_json(self, outfile): + mem = self.root.print_mem(ly.LYD_JSON, ly.LYP_FORMAT) + with open(outfile, 'w') as out: + json.dump(mem, out, indent=4) + + """ + get_module_tree(): get yang module tree in JSON or XMAL format + input: module name + returns: JSON or XML format of the input yang module schema tree + """ + def get_module_tree(self, module_name, format): + result = None + + try: + module = self.ctx.get_module(str(module_name)) + except Exception as e: + print("Cound not get module: " + str(module_name)) + self.fail(e) + else: + if (module is not None): + if (format == "XML"): + #libyang bug with format + result = module.print_mem(ly.LYD_JSON, ly.LYP_FORMAT) + else: + result = module.print_mem(ly.LYD_XML, ly.LYP_FORMAT) + + return result + + """ + validate_data(): validate data tree + input: + node: root of the data tree + ctx: context + returns: Exception if failed + """ + def validate_data (self, node=None, ctx=None): + if not node: + node = self.root + + if not ctx: + ctx = self.ctx + + try: + rc = node.validate(ly.LYD_OPT_CONFIG, ctx) + except Exception as e: + self.fail(e) + + """ + validate_data_tree(): validate the data tree + returns: Exception if failed + """ + def validate_data_tree (self): + try: + self.validate_data(self.root, self.ctx) + except Exception as e: + print("Failed to validate data tree") + self.fail(e) + + """ + find_parent_node(): find the parent node object + input: data_xpath - xpath of the data node + returns: parent node + """ + def find_parent_node (self, data_xpath): + if (self.root is None): + print("data not loaded") + return None + try: + node = self.find_data_node(data_xpath) + except Exception as e: + print("Failed to find data node from xpath: " + str(data_xpath)) + self.fail(e) + else: + if node is not None: + return node.parent() + + return None + + """ + get_parent_xpath(): find the parent node xpath + input: data_xpath - xpathof the data node + returns: - xpath of parent node + - Exception if error + """ + def get_parent_xpath (self, data_xpath): + path="" + try: + node = self.find_parent_node(data_xpath) + except Exception as e: + print("Failed to find parent node from xpath: " + str(data_xpath)) + self.fail(e) + else: + if (node is not None): + path = node.path() + return path + + """ + new_node(): create a new data node in the data tree + input: + xpath: xpath of the new node + value: value of the new node + returns: new Data_Node object if success, Exception if falied + """ + def new_node(self, xpath, value): + val = str(value) + try: + node = self.root.new_path(self.ctx, xpath, val, 0, 0) + except Exception as e: + print("Failed to add data node for path: " + str(xpath)) + self.fail(e) + else: + return node + + """ + find_data_node(): find the data node from xpath + input: data_xpath: xpath of the data node + returns - Data_Node object if found + - None if not exist + - Exception if there is error + """ + def find_data_node(self, data_xpath): + try: + set = self.root.find_path(data_xpath) + except Exception as e: + print("Failed to find data node from xpath: " + str(data_xpath)) + self.fail(e) + else: + if set is not None: + for node in set.data(): + if (data_xpath == node.path()): + return node + return None + """ + find_schema_node(): find the schema node from schema xpath + example schema xpath: + "/sonic-port:sonic-port/sonic-port:PORT/sonic-port:PORT_LIST/sonic-port:port_name" + input: xpath of the node + returns: Schema_Node oject or None if not found + """ + def find_schema_node(self, schema_xpath): + try: + schema_set = self.ctx.find_path(schema_xpath) + for snode in schema_set.schema(): + if (schema_xpath == snode.path()): + return snode + except Exception as e: + self.fail(e) + return None + else: + for snode in schema_set.schema(): + if schema_xapth == snode.path(): + return snode + return None + """ + find_node_schema_xpath(): find the xpath of the schema node from data xpath + data xpath example: + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet0']/port_name" + input: data_xpath - xpath of the data node + returns: - xpath of the schema node if success + - Exception if error + """ + def find_node_schema_xpath(self, data_xpath): + path = "" + try: + set = self.root.find_path(data_xpath) + except Exception as e: + self.fail(e) + else: + for node in set.data(): + if data_xpath == node.path(): + return node.schema().path() + return path + + """ + add_node(): add a node to Yang schema or data tree + input: xpath and value of the node to be added + returns: Exception if failed + """ + def add_node(self, xpath, value): + try: + node = self.new_node(xpath, value) + #check if the node added to the data tree + self.find_data_node(xpath) + except Exception as e: + print("add_node(): Failed to add data node for xpath: " + str(data_xpath)) + self.fail(e) + + """ + merge_data(): merge a data file to the existing data tree + input: yang model directory and full path of the data json file to be merged + returns: Exception if failed + """ + def merge_data(self, data_file, yang_dir=None): + #load all yang models to ctx + if not yang_dir: + yang_dir = self.yang_dir + + try: + ctx = self.load_schema_modules_ctx(yang_dir) + + #source data node + source_node = ctx.parse_data_path(str(data_file), ly.LYD_JSON, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) + + #merge + self.root.merge(source_node, 0) + except Exception as e: + self.fail(e) + + """ + delete_node(): delete a node from the schema/data tree + input: xpath of the schema/data node + returns: True - success False - failed + """ + def delete_node(self, data_xpath): + try: + node = self.find_data_node(data_xpath) + except Exception as e: + print("Failed to delete data node for xpath: " + str(data_xpath)) + self.fail(e) + else: + if (node): + node.unlink() + + """ + find_node_value(): find the value of a node from the schema/data tree + input: data_xpath of the data node + returns: value string of the node + """ + def find_node_value(self, data_xpath): + output = "" + try: + node = self.find_data_node(data_xpath) + except Exception as e: + print("find_node_value(): Failed to find data node from xpath: {}".format(data_xpath)) + self.fail(e) + else: + if (node is not None): + subtype = node.subtype() + if (subtype is not None): + value = subtype.value_str() + return value + return output + + """ + set the value of a node in the data tree + input: xpath of the data node + returns: Exception if failed + """ + def set_dnode_value(self, data_xpath, value): + try: + node = self.root.new_path(self.ctx, data_xpath, str(value), ly.LYD_ANYDATA_STRING, ly.LYD_PATH_OPT_UPDATE) + except Exception as e: + print("set data node value failed for xpath: " + str(data_xpath)) + self.fail(e) + + """ + find_data_nodes(): find the set of nodes for the xpath + input: xpath of the data node + returns: list of xpath of the dataset + """ + def find_data_nodes(self, data_xpath): + list = [] + node = self.root.child() + try: + node_set = node.find_path(data_xpath); + except Exception as e: + self.fail(e) + else: + if node_set is None: + raise Exception('data node not found') + + for data_set in node_set.data(): + schema = data_set.schema() + list.append(data_set.path()) + return list + + """ + find_schema_dependencies(): find the schema dependencies from schema xpath + input: schema_xpath of the schema node + returns: - list of xpath of the dependencies + - Exception if schema node not found + """ + def find_schema_dependencies (self, schema_xpath): + ref_list = [] + node = self.root + try: + schema_node = self.find_schema_node(schema_xpath) + except Exception as e: + print("Cound not find the schema node from xpath: " + str(schema_xpath)) + self.fail(e) + return ref_list + + snode = ly.Schema_Node_Leaf(schema_node) + backlinks = snode.backlinks() + if backlinks.number() > 0: + for link in backlinks.schema(): + print("backlink schema: {}".format(link.path())) + ref_list.append(link.path()) + return ref_list + + """ + find_data_dependencies(): find the data dependencies from data xpath + input: data_xpath - xpath of data node + returns: - list of xpath + - Exception if error + """ + 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: + print("find_data_dependencies(): Failed to find data node from xpath: {}".format(data_xapth)) + self.fail(e) + return ref_list + + value = str(self.find_node_value(data_xpath)) + + schema_node = ly.Schema_Node_Leaf(data_node.schema()) + backlinks = schema_node.backlinks() + if backlinks.number() > 0: + for link in backlinks.schema(): + node_set = node.find_path(link.path()) + for data_set in node_set.data(): + schema = data_set.schema() + casted = data_set.subtype() + if value == casted.value_str(): + ref_list.append(data_set.path()) + + return ref_list + diff --git a/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data.json b/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data.json new file mode 100644 index 000000000000..f7de6902c5d0 --- /dev/null +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data.json @@ -0,0 +1,276 @@ +{ + "sonic-vlan:sonic-vlan": { + "sonic-vlan:VLAN_INTERFACE": { + "VLAN_INTERFACE_LIST": [{ + "vlanid": 111, + "ip-prefix": "2000:f500:45:6709::1/64", + "scope": "global", + "family": "IPv6" + }, + { + "vlanid": 111, + "ip-prefix": "10.1.1.65/26", + "scope": "global", + "family": "IPv4" + }, + { + "vlanid": 111, + "ip-prefix": "fe80::1/10", + "scope": "local", + "family": "IPv6" + }, + { + "vlanid": 555, + "ip-prefix": "2000:f500:41:4e9::1/64", + "scope": "global", + "family": "IPv6" + }, + { + "vlanid": 555, + "ip-prefix": "10.1.5.65/26", + "scope": "global", + "family": "IPv4" + }, + { + "vlanid": 555, + "ip-prefix": "fe80::1/10", + "scope": "local", + "family": "IPv6" + } + ] + }, + + "sonic-vlan:VLAN": { + "VLAN_LIST": [{ + "vlanid": 111, + "description": "server_vlan", + "dhcp_servers": [ + "10.1.7.116" + ], + "mtu": "9216", + "admin_status": "up", + "members": [ + "Ethernet8", + "Ethernet3", + "Ethernet0", + "Ethernet1", + "Ethernet6", + "Ethernet4", + "Ethernet5", + "Ethernet9", + "Ethernet2" + ] + }, + { + "vlanid": 555, + "description": "ipmi_vlan", + "dhcp_servers": [ + "10.1.7.116" + ], + "mtu": "9216", + "admin_status": "up", + "members": [ + "Ethernet9", + "Ethernet2", + "Ethernet8" + ] + } + ] + }, + + "sonic-vlan:VLAN_MEMBER": { + "VLAN_MEMBER_LIST": [{ + "vlanid": 111, + "port": "Ethernet0", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet1", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet2", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet3", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet4", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet5", + "tagging_mode": "tagged" + }, + { + "vlanid": 111, + "port": "Ethernet6", + "tagging_mode": "tagged" + } + ] + } + }, + "sonic-port:sonic-port": { + "sonic-port:PORT": { + "PORT_LIST": [{ + "port_name": "Ethernet0", + "alias": "eth0", + "description": "Ethernet0", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet1", + "alias": "eth1", + "description": "Ethernet1", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet2", + "alias": "eth2", + "description": "Ethernet2", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet3", + "alias": "eth2", + "description": "Ethernet3", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet4", + "alias": "eth4", + "description": "Ethernet4", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet5", + "alias": "eth5", + "description": "Ethernet5", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet6", + "alias": "eth6", + "description": "Ethernet6", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet7", + "alias": "eth7", + "description": "Ethernet7", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet8", + "alias": "eth8", + "description": "Ethernet8", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet9", + "alias": "eth9", + "description": "Ethernet9", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + } + ] + } + }, + + "sonic-acl:sonic-acl": { + "sonic-acl:ACL_RULE": { + "ACL_RULE_LIST": [{ + "ACL_TABLE_NAME": "PACL-V4", + "RULE_NAME": "Rule_20", + "PACKET_ACTION": "FORWARD", + "DST_IP": "10.1.72.0/26", + "SRC_IP": "10.1.0.0/15", + "PRIORITY": "999980", + "IP_TYPE": "IPV4ANY" + }, + { + "ACL_TABLE_NAME": "PACL-V4", + "RULE_NAME": "Rule_40", + "PACKET_ACTION": "FORWARD", + "DST_IP": "10.1.72.64/26", + "SRC_IP": "10.1.0.0/15", + "PRIORITY": "999960", + "IP_TYPE": "IPV4ANY", + "INNER_ETHER_TYPE": "0x88CC" + }, + { + "ACL_TABLE_NAME": "PACL-V6", + "RULE_NAME": "Rule_20", + "PACKET_ACTION": "FORWARD", + "IP_TYPE": "IP", + "SRC_IPV6": "2000:f500:41::/48", + "PRIORITY": "999980", + "DST_IPV6": "2000:f500:43:320::/64", + "L4_SRC_PORT_RANGE": "653-1053" + } + ] + }, + + "sonic-acl:ACL_TABLE": { + "ACL_TABLE_LIST": [{ + "ACL_TABLE_NAME": "PACL-V6", + "policy_desc": "Filter IPv6", + "type": "L3V6", + "stage": "EGRESS", + "ports": ["Ethernet7", "Ethernet9", "Ethernet8"] + }, + { + "ACL_TABLE_NAME": "PACL-V4", + "policy_desc": "Filter IPv6", + "type": "L3", + "stage": "INGRESS", + "ports": ["Ethernet2", "Ethernet0", "Ethernet1"] + } + ] + } + }, + + "sonic-interface:sonic-interface": { + "sonic-interface:INTERFACE": { + "INTERFACE_LIST": [{ + "interface": "Ethernet8", + "ip-prefix": "10.1.1.65/26", + "scope": "global", + "family": "IPv4" + }, + { + "interface": "Ethernet8", + "ip-prefix": "2000:f500:40:a749::2/126", + "scope": "global", + "family": "IPv6" + } + ] + } + } +} diff --git a/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data_merge.json b/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data_merge.json new file mode 100644 index 000000000000..3bb4f0f5954b --- /dev/null +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data_merge.json @@ -0,0 +1,177 @@ +{ + "sonic-vlan:sonic-vlan": { + "sonic-vlan:VLAN_INTERFACE": { + "VLAN_INTERFACE_LIST": [{ + "vlanid": 111, + "ip-prefix": "2000:f500:45:6709::1/64", + "scope": "global", + "family": "IPv6" + }, + { + "vlanid": 111, + "ip-prefix": "10.1.1.64/26", + "scope": "global", + "family": "IPv4" + }, + { + "vlanid": 200, + "ip-prefix": "2000:f500:45:6708::1/64", + "scope": "global", + "family": "IPv6" + }, + { + "vlanid": 200, + "ip-prefix": "2000:f500:45:6709::1/64", + "scope": "global", + "family": "IPv6" + } + ] + }, + + "sonic-vlan:VLAN": { + "VLAN_LIST": [{ + "vlanid": 200, + "description": "server_vlan", + "dhcp_servers": [ + "10.1.72.116" + ], + "mtu": "9216", + "admin_status": "up", + "members": [ + "Ethernet8", + "Ethernet3", + "Ethernet0", + "Ethernet1", + "Ethernet6", + "Ethernet4", + "Ethernet5", + "Ethernet9", + "Ethernet2" + ] + }, + { + "vlanid": 111, + "description": "server_vlan", + "dhcp_servers": [ + "10.1.72.116" + ], + "mtu": "9216", + "admin_status": "up", + "members": [ + "Ethernet8", + "Ethernet3", + "Ethernet0", + "Ethernet1", + "Ethernet6", + "Ethernet4", + "Ethernet5", + "Ethernet9", + "Ethernet2" + ] + } + ] + }, + + "sonic-vlan:VLAN_MEMBER": { + "VLAN_MEMBER_LIST": [{ + "vlanid": 200, + "port": "Ethernet0", + "tagging_mode": "tagged" + } + ] + } + }, + "sonic-port:sonic-port": { + "sonic-port:PORT": { + "PORT_LIST": [{ + "port_name": "Ethernet0", + "alias": "eth0", + "description": "Ethernet0", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet1", + "alias": "eth1", + "description": "Ethernet1", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet2", + "alias": "eth2", + "description": "Ethernet2", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet3", + "alias": "eth2", + "description": "Ethernet3", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet4", + "alias": "eth4", + "description": "Ethernet4", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet5", + "alias": "eth5", + "description": "Ethernet5", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet6", + "alias": "eth6", + "description": "Ethernet6", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet7", + "alias": "eth7", + "description": "Ethernet7", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet8", + "alias": "eth8", + "description": "Ethernet8", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet9", + "alias": "eth9", + "description": "Ethernet9", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + }, + { + "port_name": "Ethernet10", + "alias": "eth10", + "description": "Ethernet10", + "speed": 25000, + "mtu": 9000, + "admin_status": "up" + } + ] + } + } +} 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 new file mode 100644 index 000000000000..d7fdff1ac797 --- /dev/null +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json @@ -0,0 +1,103 @@ +{ + "yang_dir":"/sonic/src/sonic-yang-mgmt/yang-models/", + "data_file":"/sonic/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data.json", + "data_merge_file":"/sonic/src/sonic-yang-mgmt/tests/libyang-python-tests/sonic_config_data_merge.json", + "modules":[ + {"file":"sonic-head.yang", "module":"sonic-head"}, + {"file":"sonic-port.yang", "module":"sonic-port"}, + {"file":"sonic-acl.yang", "module":"sonic-acl"}, + {"file":"sonic-interface.yang", "module":"sonic-interface"}, + {"file":"sonic-portchannel.yang", "module":"sonic-portchannel"}, + {"file":"sonic-vlan.yang", "module":"sonic-vlan"} + ], + + "new_nodes":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet12']/alias", "value":"Ethernet10_alias"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet12']/speed", "value":"5000"}, + {"xpath":"/sonic-acl:sonic-acl/ACL_RULE/ACL_RULE_LIST[ACL_TABLE_NAME='PACL-test'][RULE_NAME='rule_20']/RULE_NAME", + "value":"rule_20"} + ], + + "data_nodes":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/alias", "valid":"True"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet20']/alias", "valid":"False"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE", "valid":"True"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST", "valid":"False"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='2000:f500:45:6709::/64']", "valid":"True"} + ], + + "set_nodes":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet10']/speed", "value":"10000"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/mtu", "value":"1500"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN/VLAN_LIST[vlanid='111']/description", "value":"server_vlan111"} + ], + + "node_values":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/speed", "value":"25000"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='2000:f500:45:6709::/64']/family", + "value":"IPv6"} + ], + + "schema_nodes":[ + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/family", + "value":"/sonic-vlan:sonic-vlan/sonic-vlan:VLAN_INTERFACE/sonic-vlan:VLAN_INTERFACE_LIST/sonic-vlan:family"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/speed", + "value":"/sonic-port:sonic-port/sonic-port:PORT/sonic-port:PORT_LIST/sonic-port:speed"} + ], + + "delete_nodes":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet10']/speed", "valid":"False"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/mtu", "valid":"True"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet20']/mtu", "valid":"False"} + ], + + "dependencies":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet8']/port_name", + "dependencies": + ["/sonic-vlan:sonic-vlan/VLAN/VLAN_LIST[vlanid='111']/members[.='Ethernet8']", + "/sonic-vlan:sonic-vlan/VLAN/VLAN_LIST[vlanid='555']/members[.='Ethernet8']", + "/sonic-acl:sonic-acl/ACL_TABLE/ACL_TABLE_LIST[ACL_TABLE_NAME='PACL-V6']/ports[.='Ethernet8']", + "/sonic-interface:sonic-interface/INTERFACE/INTERFACE_LIST[interface='Ethernet8'][ip-prefix='10.1.1.64/26']/interface", + "/sonic-interface:sonic-interface/INTERFACE/INTERFACE_LIST[interface='Ethernet8'][ip-prefix='2000:f500:40:a749::/126']/interface"]} + ], + "schema_dependencies":[ + {"xpath":"/sonic-port:sonic-port/sonic-port:PORT/sonic-port:PORT_LIST/sonic-port:port_name", + "schema_dependencies": + ["/sonic-acl:sonic-acl/sonic-acl:ACL_TABLE/sonic-acl:ACL_TABLE_LIST/sonic-acl:ports", + "/sonic-portchannel:sonic-portchannel/sonic-portchannel:PORTCHANNEL/sonic-portchannel:PORTCHANNEL_LIST/sonic-portchannel:members", + "/sonic-interface:sonic-interface/sonic-interface:INTERFACE/sonic-interface:INTERFACE_LIST/sonic-interface:interface", + "/sonic-vlan:sonic-vlan/sonic-vlan:VLAN/sonic-vlan:VLAN_LIST/sonic-vlan:members", + "/sonic-vlan:sonic-vlan/sonic-vlan:VLAN_MEMBER/sonic-vlan:VLAN_MEMBER_LIST/sonic-vlan:port"]} + ], + "members":[ + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST", + "members": + ["/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet0']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet1']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet2']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet3']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet4']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet5']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet6']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet7']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet8']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet10']", + "/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet12']"]} + ], + + "parents":[ + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='2000:f500:45:6709::/64']/family", + "parent":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='2000:f500:45:6709::/64']"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/scope", + "parent":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/vlanid", + "parent":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/ip-prefix", + "parent":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']"}, + {"xpath":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']/family", + "parent":"/sonic-vlan:sonic-vlan/VLAN_INTERFACE/VLAN_INTERFACE_LIST[vlanid='111'][ip-prefix='10.1.1.64/26']"}, + {"xpath":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']/speed", + "parent":"/sonic-port:sonic-port/PORT/PORT_LIST[port_name='Ethernet9']"} + ] +} 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 new file mode 100644 index 000000000000..15d68a028b2b --- /dev/null +++ b/src/sonic-yang-mgmt/tests/libyang-python-tests/test_sonic_yang.py @@ -0,0 +1,195 @@ +import sys +import os +import pytest +import yang as ly +import sonic_yang as sy +import json +import getopt +import subprocess +import glob + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +sys.path.insert(0, modules_path) + +class Test_SonicYang(object): + @pytest.fixture(autouse=True, scope='class') + def data(self): + test_file = "/sonic/src/sonic-yang-mgmt/tests/libyang-python-tests/test_SonicYang.json" + data = self.jsonTestParser(test_file) + return data + + @pytest.fixture(autouse=True, scope='class') + def yang_s(self, data): + yang_dir = str(data['yang_dir']) + data_file = str(data['data_file']) + yang_s = sy.sonic_yang(yang_dir) + return yang_s + + def jsonTestParser(self, file): + """ + Open the json test file + """ + with open(file) as data_file: + data = json.load(data_file) + return data + + def setup_class(cls): + pass + + def load_yang_model_file(self, yang_s, yang_dir, yang_file, module_name): + yfile = yang_dir + yang_file + try: + yang_s.load_schema_module(str(yfile)) + except Exception as e: + print(e) + raise + + #test load and get yang module + def test_load_yang_model_files(self, data, yang_s): + yang_dir = data['yang_dir'] + for module in data['modules']: + file = str(module['file']) + module = str(module['module']) + + self.load_yang_model_file(yang_s, yang_dir, file, module) + assert yang_s.get_module(module) is not None + + #test load non-exist yang module file + def test_load_invalid_model_files(self, data, yang_s): + yang_dir = data['yang_dir'] + file = "invalid.yang" + module = "invalid" + + with pytest.raises(Exception): + assert self.load_yang_model_file(yang_s, yang_dir, file, module) + + #test load yang modules in directory + def test_load_yang_model_dir(self, data, yang_s): + yang_dir = data['yang_dir'] + yang_s.load_schema_modules(str(yang_dir)) + + for module_name in data['modules']: + assert yang_s.get_module(str(module_name['module'])) is not None + + #test load yang modules and data files + def test_load_yang_model_data(self, data, yang_s): + yang_dir = str(data['yang_dir']) + yang_files = glob.glob(yang_dir+"/*.yang") + data_file = str(data['data_file']) + data_merge_file = str(data['data_merge_file']) + + data_files = [] + data_files.append(data_file) + data_files.append(data_merge_file) + print(yang_files) + yang_s.load_data_model(yang_dir, yang_files, data_files) + + #test load data file + def test_load_data_file(self, data, yang_s): + data_file = str(data['data_file']) + yang_s.load_data_file(data_file) + + #test_validate_data_tree(): + def test_validate_data_tree(self, data, yang_s): + yang_s.validate_data_tree() + + #test find node + def test_find_node(self, data, yang_s): + for node in data['data_nodes']: + expected = node['valid'] + xpath = str(node['xpath']) + dnode = yang_s.find_data_node(xpath) + + if(expected == "True"): + assert dnode is not None + assert dnode.path() == xpath + else: + assert dnode == None + + #test add node + def test_add_node(self, data, yang_s): + for node in data['new_nodes']: + xpath = str(node['xpath']) + value = node['value'] + status = yang_s.add_node(xpath, str(value)) + + node = yang_s.find_data_node(xpath) + assert node is not None + + #test find node value + def test_find_node_value(self, data, yang_s): + for node in data['node_values']: + xpath = str(node['xpath']) + value = str(node['value']) + print(xpath) + print(value) + val = yang_s.find_node_value(xpath) + assert str(val) == str(value) + + #test delete data node + def test_delete_node(self, data, yang_s): + for node in data['delete_nodes']: + expected = node['valid'] + xpath = str(node['xpath']) + yang_s.delete_node(xpath) + + #test set node's value + def test_set_datanode_value(self, data, yang_s): + for node in data['set_nodes']: + xpath = str(node['xpath']) + value = node['value'] + yang_s.set_dnode_value(xpath, value) + + val = yang_s.find_node_value(xpath) + assert str(val) == str(value) + + #test list of members + def test_find_members(self, yang_s, data): + for node in data['members']: + members = node['members'] + xpath = str(node['xpath']) + list = yang_s.find_data_nodes(xpath) + assert list.sort() == members.sort() + + #get parent xpath + def test_get_parent_xpath(self, yang_s, data): + for node in data['parents']: + xpath = str(node['xpath']) + expected_xpath = str(node['parent']) + path = yang_s.get_parent_xpath(xpath) + assert path == expected_xpath + + #test find_node_schema_xpath + def test_find_node_schema_xpath(self, yang_s, data): + for node in data['schema_nodes']: + xpath = str(node['xpath']) + schema_xpath = str(node['value']) + path = yang_s.find_node_schema_xpath(xpath) + assert path == schema_xpath + + #test data dependencies + def test_find_data_dependencies(self, yang_s, data): + for node in data['dependencies']: + xpath = str(node['xpath']) + list = node['dependencies'] + depend = yang_s.find_data_dependencies(xpath) + assert set(depend) == set(list) + + #test data dependencies + def test_find_schema_dependencies(self, yang_s, data): + for node in data['schema_dependencies']: + xpath = str(node['xpath']) + list = node['schema_dependencies'] + depend = yang_s.find_schema_dependencies(xpath) + assert set(depend) == set(list) + + #test merge data tree + def test_merge_data_tree(self, data, yang_s): + data_merge_file = data['data_merge_file'] + yang_dir = str(data['yang_dir']) + yang_s.merge_data(data_merge_file, yang_dir) + #yang_s.root.print_mem(ly.LYD_JSON, ly.LYP_FORMAT) + + def teardown_class(cls): + pass