From 4d4a4c68e730fa929d0de15f7ccb51bfbd61dfbb Mon Sep 17 00:00:00 2001 From: Ahmed Karic Date: Wed, 16 Oct 2024 19:01:40 +0200 Subject: [PATCH 1/2] test: refactor iface.py --- .../iface_phys_address/test.py | 2 +- .../verify_all_interface_types/test.py | 4 +- test/case/ietf_interfaces/veth_delete/test.py | 12 ++--- test/case/ietf_interfaces/vlan_ping/test.py | 25 +++++---- test/infamy/iface.py | 54 +++---------------- test/infamy/netconf.py | 2 +- 6 files changed, 33 insertions(+), 66 deletions(-) diff --git a/test/case/ietf_interfaces/iface_phys_address/test.py b/test/case/ietf_interfaces/iface_phys_address/test.py index 8d5147b64..e8dcbb8de 100755 --- a/test/case/ietf_interfaces/iface_phys_address/test.py +++ b/test/case/ietf_interfaces/iface_phys_address/test.py @@ -30,7 +30,7 @@ def calc_mac(base_mac, mac_offset): def reset_mac(tgt, port, mac): """Reset DUT interface MAC address to default.""" node = "infix-interfaces:custom-phys-address" - xpath = iface.get_iface_xpath(port, node) + xpath = iface.get_xpath(port, node) tgt.delete_xpath(xpath) with test.step("Verify target:data MAC address is reset to default"): until(lambda: iface.get_phys_address(tgt, tport) == mac) diff --git a/test/case/ietf_interfaces/verify_all_interface_types/test.py b/test/case/ietf_interfaces/verify_all_interface_types/test.py index e00585b43..c10508fd1 100755 --- a/test/case/ietf_interfaces/verify_all_interface_types/test.py +++ b/test/case/ietf_interfaces/verify_all_interface_types/test.py @@ -22,10 +22,10 @@ def verify_interface(target, interface, expected_type): - assert iface.interface_exist(target, interface), f"Interface <{interface}> does not exist." + assert iface.exist(target, interface), f"Interface <{interface}> does not exist." expected_type = f"infix-if-type:{expected_type}" - actual_type = iface._iface_get_param(target, interface, "type") + actual_type = iface.get_param(target, interface, "type") if expected_type == "infix-if-type:etherlike" and actual_type == "infix-if-type:ethernet": return # Allow 'etherlike' to match 'ethernet' diff --git a/test/case/ietf_interfaces/veth_delete/test.py b/test/case/ietf_interfaces/veth_delete/test.py index 047a83cc9..366cb344d 100755 --- a/test/case/ietf_interfaces/veth_delete/test.py +++ b/test/case/ietf_interfaces/veth_delete/test.py @@ -51,9 +51,9 @@ }) with test.step("Verify interfaces 'veth0a' and 'veth0b' exists"): - assert iface.interface_exist(target, veth0a), \ + assert iface.exist(target, veth0a), \ f"Interface <{veth0a}> does not exist." - assert iface.interface_exist(target, veth0b), \ + assert iface.exist(target, veth0b), \ f"Interface <{veth0b}> does not exist." with test.step("Set IP address on target:eth0 (dummy op)"): @@ -97,15 +97,15 @@ target = env.attach("target", "mgmt") with test.step("Verify target:eth0 and target:eth1 still exist"): - assert iface.interface_exist(target, eth0), \ + assert iface.exist(target, eth0), \ f"Interface {eth0} missing!" - assert iface.interface_exist(target, eth1), \ + assert iface.exist(target, eth1), \ f"Interface {eth1} missing!" with test.step("Verify VETH pair have been removed"): - assert not iface.interface_exist(target, veth0a), \ + assert not iface.exist(target, veth0a), \ f"Interface <{veth0a}> still exists!" - assert not iface.interface_exist(target, veth0b), \ + assert not iface.exist(target, veth0b), \ f"Interface <{veth0b}> still exists!" test.succeed() diff --git a/test/case/ietf_interfaces/vlan_ping/test.py b/test/case/ietf_interfaces/vlan_ping/test.py index 8220d3ded..8639f332f 100755 --- a/test/case/ietf_interfaces/vlan_ping/test.py +++ b/test/case/ietf_interfaces/vlan_ping/test.py @@ -4,26 +4,31 @@ Very basic test if the VLAN interface configuration works. """ + import infamy import infamy.iface as iface -import copy from infamy import until def test_ping(hport, should_pass): - with infamy.IsolatedMacVlan(hport) as ns: - pingtest = ns.runsh(""" - set -ex + with infamy.IsolatedMacVlan(hport) as ns: + try: + ns.runsh(""" + set -ex + ip link set iface up + ip link add dev vlan10 link iface up type vlan id 10 + ip addr add 10.0.0.1/24 dev vlan10 + """) - ip link set iface up - ip link add dev vlan10 link iface up type vlan id 10 - ip addr add 10.0.0.1/24 dev vlan10 - """) - if(should_pass): + if should_pass: ns.must_reach("10.0.0.2") else: ns.must_not_reach("10.0.0.2") + except Exception as e: + print(f"An error occurred during the VLAN setup or ping test: {e}") + raise + with infamy.Test() as test: with test.step("Set up topology and attach to target DUT"): env = infamy.Env() @@ -60,7 +65,7 @@ def test_ping(hport, should_pass): }) with test.step("Waiting for links to come up"): - until(lambda: iface.get_oper_up(target, tport)) + until(lambda: iface.get_param(target, tport, "oper-status") == "up") with test.step("Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1"): _, hport = env.ltop.xlate("host", "data") diff --git a/test/infamy/iface.py b/test/infamy/iface.py index 5406f2623..6bfdb5559 100644 --- a/test/infamy/iface.py +++ b/test/infamy/iface.py @@ -2,14 +2,14 @@ Fetch interface status from remote device. """ -def get_iface_xpath(iface, path=None): +def get_xpath(iface, path=None): """Compose complete XPath to a YANG node in /ietf-interfaces""" xpath=f"/ietf-interfaces:interfaces/interface[name='{iface}']" if not path is None: xpath=f"{xpath}/{path}" return xpath -def _iface_extract_param(json_content, param): +def _extract_param(json_content, param): """Returns (extracted) value for parameter 'param'""" interfaces = json_content.get('interfaces') if not interfaces: @@ -22,16 +22,16 @@ def _iface_extract_param(json_content, param): return None -def _iface_get_param(target, iface, param=None): +def get_param(target, iface, param=None): """Fetch target dict for iface and extract param from JSON""" - content = target.get_data(get_iface_xpath(iface, param)) + content = target.get_data(get_xpath(iface, param)) if content is None: return None - return _iface_extract_param(content, param) + return _extract_param(content, param) -def interface_exist(target, iface): +def exist(target, iface): """Verify that the target interface exists""" - return _iface_get_param(target, iface, "name") is not None + return get_param(target, iface, "name") is not None def address_exist(target, iface, address, prefix_length = 24, proto="dhcp"): """Check if 'address' is set on iface""" @@ -56,47 +56,9 @@ def get_ipv4_address(target, iface): return None return ipv4['address'] -def get_if_index(target, iface): - """Fetch interface 'if-index' (operational status)""" - return _iface_get_param(target, iface, "if-index") - -def get_oper_status(target, iface): - """Fetch interface 'oper-status' (operational status)""" - return _iface_get_param(target, iface, "oper-status") - def get_phys_address(target, iface): """Fetch interface MAC address (operational status)""" - return _iface_get_param(target, iface, "phys-address") - -def get_oper_up(target,iface): - state=get_oper_status(target,iface) - return state == "up" - -def print_iface(target, iface): - data = target.get_data(_iface_xpath(iface, None)) - print(data) - -def print_all(target): - """Print status parameters for all target interfaces""" - try: - content = target.get_dict("/ietf-interfaces:interfaces") - interfaces = content.get('interfaces') - if interfaces: - interface_list = interfaces.get('interface') - if interface_list and isinstance(interface_list, list): - col1 = "name" - col2 = "if-index" - col3 = "oper-status" - print('-'*36) - print(f"{col1: <12}{col2: <12}{col3: <12}") - print('-'*36) - for interface in interface_list: - print(f"{interface['name']: <12}" - f"{interface['if-index']: <12}" - f"{interface['oper-status']: <12}") - print('-'*36) - except: - print(f"Failed to get interfaces' status from target {target}") + return get_param(target, iface, "phys-address") def exist_bridge_multicast_filter(target, group, iface, bridge): # The interface array is different in restconf/netconf, netconf has a keyed list but diff --git a/test/infamy/netconf.py b/test/infamy/netconf.py index 86f44c129..5311a2b62 100644 --- a/test/infamy/netconf.py +++ b/test/infamy/netconf.py @@ -374,7 +374,7 @@ def get_schema(self, schema, outdir): def get_iface(self, name): """Fetch target dict for iface and extract param from JSON""" - content = self.get_data(iface.get_iface_xpath(name)) + content = self.get_data(iface.get_xpath(name)) interface = content.get("interfaces", {}).get("interface", None) if interface is None: From 420f6a055687d7729822eea309974223792ab857 Mon Sep 17 00:00:00 2001 From: Ahmed Karic Date: Thu, 17 Oct 2024 09:50:01 +0200 Subject: [PATCH 2/2] test: verify interface status (enable/disable) Fixes #694 --- test/case/ietf_interfaces/Readme.adoc | 2 + .../case/ietf_interfaces/ietf_interfaces.yaml | 3 + .../iface_enable_disable/Readme.adoc | 31 ++++++ .../iface_enable_disable/test.py | 105 ++++++++++++++++++ .../iface_enable_disable/topology.dot | 35 ++++++ .../iface_enable_disable/topology.png | Bin 0 -> 20743 bytes 6 files changed, 176 insertions(+) create mode 100644 test/case/ietf_interfaces/iface_enable_disable/Readme.adoc create mode 100755 test/case/ietf_interfaces/iface_enable_disable/test.py create mode 100644 test/case/ietf_interfaces/iface_enable_disable/topology.dot create mode 100644 test/case/ietf_interfaces/iface_enable_disable/topology.png diff --git a/test/case/ietf_interfaces/Readme.adoc b/test/case/ietf_interfaces/Readme.adoc index 28307548e..22ca74421 100644 --- a/test/case/ietf_interfaces/Readme.adoc +++ b/test/case/ietf_interfaces/Readme.adoc @@ -38,3 +38,5 @@ include::static_multicast_filters/Readme.adoc[] include::vlan_qos/Readme.adoc[] include::verify_all_interface_types/Readme.adoc[] + +include::iface_enable_disable/Readme.adoc[] diff --git a/test/case/ietf_interfaces/ietf_interfaces.yaml b/test/case/ietf_interfaces/ietf_interfaces.yaml index bb71d0292..a512bc277 100644 --- a/test/case/ietf_interfaces/ietf_interfaces.yaml +++ b/test/case/ietf_interfaces/ietf_interfaces.yaml @@ -55,3 +55,6 @@ - name: verify_all_interface_types case: verify_all_interface_types/test.py + +- name: iface_enable_disable + case: iface_enable_disable/test.py diff --git a/test/case/ietf_interfaces/iface_enable_disable/Readme.adoc b/test/case/ietf_interfaces/iface_enable_disable/Readme.adoc new file mode 100644 index 000000000..f6a641429 --- /dev/null +++ b/test/case/ietf_interfaces/iface_enable_disable/Readme.adoc @@ -0,0 +1,31 @@ +=== Interface status +==== Description +Verify interface status properly propagate changes when an interface +is disabled and then re-enabled. + +Both admin-status and oper-status are verified. + +==== Topology +ifdef::topdoc[] +image::../../test/case/ietf_interfaces/iface_enable_disable/topology.png[Interface status topology] +endif::topdoc[] +ifndef::topdoc[] +ifdef::testgroup[] +image::iface_enable_disable/topology.png[Interface status topology] +endif::testgroup[] +ifndef::testgroup[] +image::topology.png[Interface status topology] +endif::testgroup[] +endif::topdoc[] +==== Test sequence +. Set up topology and attach to target DUTs +. Configure bridge and associated interfaces in target1 +. Disable interface in target2 +. Verify the interface is disabled +. Enable the interface and assign an IP address +. Verify the interface is enabled +. Verify it is possible to ping the interface + + +<<< + diff --git a/test/case/ietf_interfaces/iface_enable_disable/test.py b/test/case/ietf_interfaces/iface_enable_disable/test.py new file mode 100755 index 000000000..76e85d1d6 --- /dev/null +++ b/test/case/ietf_interfaces/iface_enable_disable/test.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +""" +Interface status + +Verify interface status properly propagate changes when an interface +is disabled and then re-enabled. + +Both admin-status and oper-status are verified. +""" + +import infamy +import infamy.iface as iface + +from infamy import until + +def print_error_message(iface, param, exp_val, act_val): + return f"'{param}' failure for interface '{iface}'. Expected '{exp_val}', Actual: '{act_val}'" + +def assert_param(target, interface, parameter, expected_value): + def check_param(): + actual_value = iface.get_param(target, interface, parameter) + if actual_value is None: + raise ValueError(f"Failed to retrieve '{parameter}' for interface '{interface}'") + return actual_value == expected_value + + until(check_param) + + actual_value = iface.get_param(target, interface, parameter) + assert (expected_value == actual_value), print_error_message( + iface=interface, + param = parameter, + exp_val = expected_value, + act_val = actual_value + ) + +def configure_interface(target, iface_name, iface_type=None, enabled=True, ip_address=None, bridge=None): + + interface_config = { + "name": iface_name, + "enabled": enabled + } + + if iface_type: + interface_config["type"] = iface_type + + if ip_address: + interface_config["ipv4"] = { + "address": [ + { + "ip": ip_address, + "prefix-length": 24 + }]} + + if bridge: + interface_config["infix-interfaces:bridge-port"] = { + "bridge": bridge + } + + target.put_config_dict( "ietf-interfaces", { + "interfaces": { + "interface": [ + interface_config + ]}}) + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUTs"): + env = infamy.Env() + target1 = env.attach("target1", "mgmt") + target2 = env.attach("target2", "mgmt") + + _, data1 = env.ltop.xlate("target1", "data") + _, link1 = env.ltop.xlate("target1", "link") + + _, iface_under_test = env.ltop.xlate("target2", "link") + _, host_send_iface = env.ltop.xlate("host", "data") + _bridge = "br_0" + + target_address = "10.10.10.2" + host_address = "10.10.10.1" + + with test.step("Configure bridge and associated interfaces in target1"): + configure_interface(target1, _bridge, enabled=True, iface_type="infix-if-type:bridge") + configure_interface(target1, data1, enabled=True, bridge=_bridge) + configure_interface(target1, link1, enabled=True, bridge=_bridge) + + with test.step("Disable interface in target2"): + configure_interface(target2, iface_under_test, enabled=False) + + with test.step("Verify the interface is disabled"): + assert_param(target2, iface_under_test, "admin-status", "down") + assert_param(target2, iface_under_test, "oper-status", "down") + + with test.step("Enable the interface and assign an IP address"): + configure_interface(target2, iface_under_test, enabled=True, ip_address=target_address) + + with test.step("Verify the interface is enabled"): + assert_param(target2, iface_under_test, "admin-status", "up") + assert_param(target2, iface_under_test, "oper-status", "up") + + with infamy.IsolatedMacVlan(host_send_iface) as send_ns: + with test.step("Verify it is possible to ping the interface"): + send_ns.addip(host_address) + send_ns.must_reach(target_address) + + test.succeed() \ No newline at end of file diff --git a/test/case/ietf_interfaces/iface_enable_disable/topology.dot b/test/case/ietf_interfaces/iface_enable_disable/topology.dot new file mode 100644 index 000000000..06dba9916 --- /dev/null +++ b/test/case/ietf_interfaces/iface_enable_disable/topology.dot @@ -0,0 +1,35 @@ +graph "2x4" { + layout="neato"; + overlap="false"; + esep="+40"; + + node [shape=record, fontname="monospace"]; + edge [color="cornflowerblue", penwidth="2"]; + + host [ + label="host | { mgmt1 | data | mgmt2 }", + pos="0,15!", + kind="controller", + ]; + + target1 [ + label="{ mgmt | data | link } | { \n dut1 \n\n }", + pos="8,15!", + + kind="infix", + ]; + + target2 [ + label="{ link | mgmt } | { \n dut2 \n\n }", + pos="8,12!", + + kind="infix", + ]; + + host:mgmt1 -- target1:mgmt [kind=mgmt, color="lightgrey"] + host:data -- target1:data [color=black, fontcolor=black, taillabel="10.10.10.1/24"] + + host:mgmt2 -- target2:mgmt [kind=mgmt, color="lightgrey"] + + target1:link -- target2:link [color=black, fontcolor=black, headlabel="10.10.10.2/24"] +} \ No newline at end of file diff --git a/test/case/ietf_interfaces/iface_enable_disable/topology.png b/test/case/ietf_interfaces/iface_enable_disable/topology.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c67ca9327f02303354f1e96970290233588bf1 GIT binary patch literal 20743 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 Q*^r{SS4TZ%kBRU92donY`v3p{ literal 0 HcmV?d00001