diff options
Diffstat (limited to 'resources')
-rw-r--r-- | resources/api/vpp/supported_crcs.yaml | 8 | ||||
-rw-r--r-- | resources/libraries/bash/function/common.sh | 4 | ||||
-rw-r--r-- | resources/libraries/python/FlowUtil.py | 570 | ||||
-rw-r--r-- | resources/libraries/python/Trace.py | 15 | ||||
-rw-r--r-- | resources/libraries/robot/shared/default.robot | 1 | ||||
-rw-r--r-- | resources/libraries/robot/shared/traffic.robot | 55 |
6 files changed, 650 insertions, 3 deletions
diff --git a/resources/api/vpp/supported_crcs.yaml b/resources/api/vpp/supported_crcs.yaml index fefdc07ea6..840abe8084 100644 --- a/resources/api/vpp/supported_crcs.yaml +++ b/resources/api/vpp/supported_crcs.yaml @@ -96,6 +96,14 @@ det44_session_dump: '0xe45a3af7' # dev # TODO: Which test to run to verify det44_* messages? # dhcp_proxy_dump / details # honeycomb + flow_add: '0xf946ed84' # dev + flow_add_reply: '0x8587dc85' # dev + flow_enable: '0x2024be69' # dev + flow_enable_reply: '0xe8d4e804' # dev + flow_disable: '0x2024be69' #dev + flow_disable_reply: '0xe8d4e804' #dev + flow_del: '0xb6b9b02c' #dev + flow_del_reply: '0xe8d4e804' #dev geneve_add_del_tunnel2: '0x8c2a9999' # dev geneve_add_del_tunnel2_reply: '0x5383d31f' # dev geneve_tunnel_details: '0x6b16eb24' # dev diff --git a/resources/libraries/bash/function/common.sh b/resources/libraries/bash/function/common.sh index 85f6e08b68..510ccb0072 100644 --- a/resources/libraries/bash/function/common.sh +++ b/resources/libraries/bash/function/common.sh @@ -936,6 +936,10 @@ function select_tags () { *"1n-vbox"*) test_tag_array+=("!avf") test_tag_array+=("!vhost") + test_tag_array+=("!flow") + ;; + *"1n_tx2"*) + test_tag_array+=("!flow") ;; *"2n-skx"*) test_tag_array+=("!ipsechw") diff --git a/resources/libraries/python/FlowUtil.py b/resources/libraries/python/FlowUtil.py new file mode 100644 index 0000000000..1f647a8e11 --- /dev/null +++ b/resources/libraries/python/FlowUtil.py @@ -0,0 +1,570 @@ +# copyright (c) 2021 Intel and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Flow Utilities Library.""" + +from ipaddress import ip_address + +from resources.libraries.python.topology import Topology +from resources.libraries.python.ssh import exec_cmd_no_error +from resources.libraries.python.PapiExecutor import PapiSocketExecutor + +class FlowUtil: + """Utilities for flow configuration.""" + + @staticmethod + def vpp_create_ip4_n_tuple_flow( + node, src_ip, dst_ip, src_port, dst_port, + proto, action, value=0): + """Create IP4_N_TUPLE flow. + + :param node: DUT node. + :param src_ip: Source IP4 address. + :param dst_ip: Destination IP4 address. + :param src_port: Source port. + :param dst_port: Destination port. + :param proto: TCP or UDP. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type src_port: int + :type dst_port: int + :type proto: str + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip4_n_tuple" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_N_TUPLE + + if proto == u"TCP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_TCP + elif proto == u"UDP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + else: + raise ValueError(f"proto error: {proto}") + + pattern = { + u'src_addr': {u'addr': src_ip, u'mask': u"255.255.255.255"}, + u'dst_addr': {u'addr': dst_ip, u'mask': u"255.255.255.255"}, + u'src_port': {u'port': src_port, u'mask': 0xFFFF}, + u'dst_port': {u'port': dst_port, u'mask': 0xFFFF}, + u'protocol': {u'prot': flow_proto} + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip6_n_tuple_flow( + node, src_ip, dst_ip, src_port, dst_port, + proto, action, value=0): + """Create IP6_N_TUPLE flow. + + :param node: DUT node. + :param src_ip: Source IP6 address. + :param dst_ip: Destination IP6 address. + :param src_port: Source port. + :param dst_port: Destination port. + :param proto: TCP or UDP. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type src_port: int + :type dst_port: int + :type proto: str + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip6_n_tuple" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP6_N_TUPLE + + if proto == u"TCP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_TCP + elif proto == u"UDP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + else: + raise ValueError(f"proto error: {proto}") + + pattern = { + u'src_addr': {u'addr': src_ip, \ + u'mask': u"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"}, + u'dst_addr': {u'addr': dst_ip, \ + u'mask': u"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"}, + u'src_port': {u'port': src_port, u'mask': 0xFFFF}, + u'dst_port': {u'port': dst_port, u'mask': 0xFFFF}, + u'protocol': {u'prot': flow_proto} + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip4_flow( + node, src_ip, dst_ip, proto, action, value=0): + """Create IP4 flow. + + :param node: DUT node. + :param src_ip: Source IP4 address. + :param dst_ip: Destination IP4 address. + :param proto: TCP or UDP. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type proto: str + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip4" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4 + + if proto == u"TCP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_TCP + elif proto == u"UDP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + else: + raise ValueError(f"proto error: {proto}") + + pattern = { + u'src_addr': {u'addr': src_ip, u'mask': u"255.255.255.255"}, + u'dst_addr': {u'addr': dst_ip, u'mask': u"255.255.255.255"}, + u'protocol': {u'prot': flow_proto} + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip6_flow( + node, src_ip, dst_ip, proto, action, value=0): + """Create IP6 flow. + + :param node: DUT node. + :param src_ip: Source IP6 address. + :param dst_ip: Destination IP6 address. + :param proto: TCP or UDP. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type proto: str + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip6" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP6 + + if proto == u"TCP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_TCP + elif proto == u"UDP": + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + else: + raise ValueError(f"proto error: {proto}") + + pattern = { + u'src_addr': {u'addr': src_ip, \ + u'mask': u"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"}, + u'dst_addr': {'addr': dst_ip, \ + u'mask': u"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"}, + u'protocol': {u'prot': flow_proto} + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip4_gtpu_flow( + node, src_ip, dst_ip, teid, action, value=0): + """Create IP4_GTPU flow. + + :param node: DUT node. + :param src_ip: Source IP4 address. + :param dst_ip: Destination IP4 address. + :param teid: Tunnel endpoint identifier. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type teid: int + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip4_gtpu" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_GTPU + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + + pattern = { + u'src_addr': {u'addr': src_ip, u'mask': u"255.255.255.255"}, + u'dst_addr': {u'addr': dst_ip, u'mask': u"255.255.255.255"}, + u'protocol': {u'prot': flow_proto}, + u'teid': teid + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip4_ipsec_flow(node, proto, spi, action, value=0): + """Create IP4_IPSEC flow. + + :param node: DUT node. + :param proto: TCP or UDP. + :param spi: Security Parameters Index. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type proto: str + :type spi: int + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + if proto == u"ESP": + flow = u"ip4_ipsec_esp" + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_ESP + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_IPSEC_ESP + elif proto == u"AH": + flow = u"ip4_ipsec_ah" + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_AH + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_IPSEC_AH + else: + raise ValueError(f"proto error: {proto}") + + pattern = { + u'protocol': {u'prot': flow_proto}, + u'spi': spi + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip4_l2tp_flow(node, session_id, action, value=0): + """Create IP4_L2TPV3OIP flow. + + :param node: DUT node. + :param session_id: PPPoE session ID + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type session_id: int + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + """ + from vpp_papi import VppEnum + + flow = u"ip4_l2tpv3oip" + flow_proto = 115 # IP_API_PROTO_L2TP + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_L2TPV3OIP + + pattern = { + u'protocol': {u'prot': flow_proto}, + u'session_id': session_id + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_create_ip4_vxlan_flow(node, src_ip, dst_ip, vni, action, value=0): + """Create IP4_VXLAN flow. + + :param node: DUT node. + :param src_ip: Source IP4 address. + :param dst_ip: Destination IP4 address. + :param vni: Virtual network instance. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type src_ip: str + :type dst_ip: str + :type vni: int + :type action: str + :type value: int + :returns: flow_index. + """ + from vpp_papi import VppEnum + + flow = u"ip4_vxlan" + flow_type = VppEnum.vl_api_flow_type_t.FLOW_TYPE_IP4_VXLAN + flow_proto = VppEnum.vl_api_ip_proto_t.IP_API_PROTO_UDP + + pattern = { + u'src_addr': {u'addr': src_ip, u'mask': u"255.255.255.255"}, + u'dst_addr': {u'addr': dst_ip, u'mask': u"255.255.255.255"}, + u'dst_port': {u'port': 4789, 'mask': 0xFFFF}, + u'protocol': {u'prot': flow_proto}, + u'vni': vni + } + + flow_index = FlowUtil.vpp_flow_add( + node, flow, flow_type, pattern, action, value) + + return flow_index + + @staticmethod + def vpp_flow_add(node, flow, flow_type, pattern, action, value=0): + """Flow add. + + :param node: DUT node. + :param flow: Name of flow. + :param flow_type: Type of flow. + :param pattern: Pattern of flow. + :param action: Mark, drop or redirect-to-queue. + :param value: Action value. + + :type node: dict + :type node: str + :type flow_type: str + :type pattern: dict + :type action: str + :type value: int + :returns: flow_index. + :rtype: int + :raises ValueError: If action type is not supported. + """ + from vpp_papi import VppEnum + + cmd = u"flow_add" + + if action == u"redirect-to-queue": + flow_rule = { + u'type': flow_type, + u'actions': VppEnum.vl_api_flow_action_t.\ + FLOW_ACTION_REDIRECT_TO_QUEUE, + u'redirect_queue': value, + u'flow': {flow : pattern} + } + elif action == u"mark": + flow_rule = { + u'type': flow_type, + u'actions': VppEnum.vl_api_flow_action_t.FLOW_ACTION_MARK, + u'mark_flow_id': value, + u'flow': {flow : pattern} + } + elif action == u"drop": + flow_rule = { + u'type': flow_type, + u'actions': VppEnum.vl_api_flow_action_t.FLOW_ACTION_DROP, + u'flow': {flow : pattern} + } + else: + raise ValueError(f"Unsupported action type: {action}") + + err_msg = f"Failed to create {flow} flow on host {node[u'host']}." + args = dict(flow=flow_rule) + flow_index = -1 + with PapiSocketExecutor(node) as papi_exec: + reply = papi_exec.add(cmd, **args).get_reply(err_msg) + flow_index = reply[u"flow_index"] + + return flow_index + + @staticmethod + def vpp_flow_enable(node, interface, flow_index=0): + """Flow enable. + + :param node: DUT node. + :param interface: Interface sw_if_index. + :param flow_index: Flow index. + + :type node: dict + :type interface: int + :type flow_index: int + :returns: Nothing. + """ + cmd = u"flow_enable" + sw_if_index = Topology.get_interface_sw_index(node, interface) + args = dict( + flow_index=int(flow_index), + hw_if_index=int(sw_if_index) + ) + + err_msg = u"Failed to enable flow on host {node[u'host']}" + with PapiSocketExecutor(node) as papi_exec: + papi_exec.add(cmd, **args).get_reply(err_msg) + + @staticmethod + def vpp_flow_disable(node, interface, flow_index=0): + """Flow disable. + + :param node: DUT node. + :param interface: Interface sw_if_index. + :param flow_index: Flow index. + + :type node: dict + :type interface: int + :type flow_index: int + :returns: Nothing. + """ + cmd = u"flow_disable" + sw_if_index = Topology.get_interface_sw_index(node, interface) + args = dict( + flow_index=int(flow_index), + hw_if_index=int(sw_if_index) + ) + + err_msg = u"Failed to disable flow on host {node[u'host']}" + with PapiSocketExecutor(node) as papi_exec: + papi_exec.add(cmd, **args).get_reply(err_msg) + + @staticmethod + def vpp_flow_del(node, flow_index=0): + """Flow delete. + + :param node: DUT node. + :param flow_index: Flow index. + + :type node: dict + :type flow_index: int + :returns: Nothing. + """ + cmd = u"flow_del" + args = dict( + flow_index=int(flow_index) + ) + + err_msg = u"Failed to delete flow on host {node[u'host']}" + with PapiSocketExecutor(node) as papi_exec: + papi_exec.add(cmd, **args).get_reply(err_msg) + + @staticmethod + def vpp_show_flow_entry(node): + """Show flow entry. + + :param node: DUT node. + + :type node: dict + :returns: flow entry. + :rtype: str + """ + cmd = u"vppctl show flow entry" + + err_msg = u"Failed to show flow on host {node[u'host']}" + stdout, _ = exec_cmd_no_error( + node, cmd, sudo=False, message=err_msg, retries=120 + ) + + return stdout.strip() + + @staticmethod + def vpp_verify_flow_action( + node, action, value, + src_mac=u"11:22:33:44:55:66", dst_mac=u"11:22:33:44:55:66", + src_ip=None, dst_ip=None): + """Verify the correctness of the flow action. + + :param node: DUT node. + :param action: Action. + :param value: Action value. + :param src_mac: Source mac address. + :param dst_mac: Destination mac address. + :param src_ip: Source IP address. + :param dst_ip: Destination IP address. + + :type node: dict + :type action: str + :type value: int + :type src_mac: str + :type dst_mac: str + :type src_ip: str + :type dst_ip: str + :returns: Nothing. + :raises RuntimeError: If the verification of flow action fails. + :raises ValueError: If action type is not supported. + """ + err_msg = f"Failed to show trace on host {node[u'host']}" + cmd = u"vppctl show trace" + stdout, _ = exec_cmd_no_error( + node, cmd, sudo=False, message=err_msg, retries=120 + ) + + err_info = f"Verify flow {action} failed" + + if src_ip is None: + expected_str = f"{src_mac} -> {dst_mac}" + else: + src_ip = ip_address(src_ip) + dst_ip = ip_address(dst_ip) + expected_str = f"{src_ip} -> {dst_ip}" + + if action == u"drop": + if expected_str in stdout: + raise RuntimeError(err_info) + elif action == u"redirect-to-queue": + if f"queue {value}" not in stdout \ + and f"qid {value}" not in stdout: + raise RuntimeError(err_info) + if expected_str not in stdout: + raise RuntimeError(err_info) + elif action == u"mark": + if u"PKT_RX_FDIR" not in stdout and u"flow-id 1" not in stdout: + raise RuntimeError(err_info) + if expected_str not in stdout: + raise RuntimeError(err_info) + else: + raise ValueError(f"Unsupported action type: {action}") diff --git a/resources/libraries/python/Trace.py b/resources/libraries/python/Trace.py index f54ae1060e..f82ab95f2e 100644 --- a/resources/libraries/python/Trace.py +++ b/resources/libraries/python/Trace.py @@ -37,12 +37,21 @@ class Trace: @staticmethod def clear_packet_trace_on_all_duts(nodes): - """Clear VPP packet trace. + """Clear VPP packet trace on all duts. :param nodes: Nodes where the packet trace will be cleared. :type nodes: dict """ for node in nodes.values(): if node[u"type"] == NodeType.DUT: - PapiSocketExecutor.run_cli_cmd_on_all_sockets( - node, u"clear trace") + Trace.clear_packet_trace_on_dut(node) + + @staticmethod + def clear_packet_trace_on_dut(node): + """Clear VPP packet trace on dut. + + :param node: Node where the packet trace will be cleared. + :type node: dict + """ + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, u"clear trace") diff --git a/resources/libraries/robot/shared/default.robot b/resources/libraries/robot/shared/default.robot index 93aa3976a2..58bbb97acb 100644 --- a/resources/libraries/robot/shared/default.robot +++ b/resources/libraries/robot/shared/default.robot @@ -25,6 +25,7 @@ | Library | resources.libraries.python.CpuUtils | Library | resources.libraries.python.CoreDumpUtil | Library | resources.libraries.python.DUTSetup +| Library | resources.libraries.python.FlowUtil | Library | resources.libraries.python.L2Util | Library | resources.libraries.python.InterfaceUtil | Library | resources.libraries.python.IPUtil diff --git a/resources/libraries/robot/shared/traffic.robot b/resources/libraries/robot/shared/traffic.robot index bd8edcf751..af348bb12e 100644 --- a/resources/libraries/robot/shared/traffic.robot +++ b/resources/libraries/robot/shared/traffic.robot @@ -652,3 +652,58 @@ | | ... | --tun_vni ${tun_vni} | --tun_src_ip ${tun_src_ip} | | ... | --tun_dst_ip ${tun_dst_ip} | | Run Traffic Script On Node | geneve_tunnel.py | ${node} | ${args} + +| Send flow packet and verify action +| | [Documentation] | Send packet and verify the correctness of flow action. +| | +| | ... | *Arguments:* +| | +| | ... | _NOTE:_ Arguments are based on topology: +| | ... | TG(if1)->(if1)DUT +| | +| | ... | - tg_node - Node to execute scripts on (TG). Type: dictionary +| | ... | - tx_interface - TG Interface 1. Type: string +| | ... | - tx_dst_mac - MAC address of DUT-if1. Type: string +| | ... | - flow_type - Flow packet type. Type: string +| | ... | - proto - Flow packet protocol. Type: string +| | ... | - src_ip - Source ip address. Type: string +| | ... | - dst_ip - Destination IP address. Type: string +| | ... | - src_port - Source port. Type: int +| | ... | - dst_port - Destination port. Type: int +| | ... | - value - Additional packet value. Type: integer +| | ... | - traffic_script - Traffic script that send packet. Type: string +| | ... | - action - drop, mark or redirect-to-queue. Type: string +| | ... | - action_value - action value. Type: integer +| | +| | ... | *Return:* +| | ... | - No value returned +| | +| | ... | *Example:* +| | ... | \| Send flow packet and verify actions \| ${nodes['TG']} \| eth2 \ +| | ... | \| 08:00:27:a2:52:5b \| IP4 \| UDP \ +| | ... | \| src_ip=1.1.1.1 \| dst_ip=2.2.2.2 \ +| | ... | \| src_port=${100} \| dst_port=${200} \ +| | ... | \| traffic_script=send_flow_packet \ +| | ... | \|action=mark \| action_value=${3} \| +| | +| | [Arguments] | ${tg_node} | ${tx_interface} | ${tx_dst_mac} +| | ... | ${flow_type} | ${proto} +| | ... | ${src_ip}=${None} | ${dst_ip}=${None} +| | ... | ${src_port}=${None} | ${dst_port}=${None} +| | ... | ${value}=${None} +| | ... | ${traffic_script}=send_flow_packet +| | ... | ${action}=redirect-to-queue +| | ... | ${action_value}=${3} +| | +| | ${tx_src_mac}= | Get Interface Mac | ${tg_node} | ${tx_interface} +| | ${tx_if_name}= | Get interface name | ${tg_node} | ${tx_interface} +| | ${args}= | Catenate +| | ... | --tg_if1_mac ${tx_src_mac} | --dut_if1_mac ${tx_dst_mac} +| | ... | --tx_if ${tx_if_name} | --flow_type ${flow_type} | --proto ${proto} +| | ... | --src_ip ${src_ip} | --dst_ip ${dst_ip} +| | ... | --src_port ${src_port} | --dst_port ${dst_port} +| | ... | --value ${value} +| | Run Traffic Script On Node | ${traffic_script}.py | ${tg_node} | ${args} +| | Vpp Verify Flow action | ${dut1} | ${action} | ${action_value} +| | ... | ${tx_src_mac} | ${tx_dst_mac} +| | ... | ${src_ip} | ${dst_ip} |