Subject: Re: [ovs-dev] [PATCH net-next 1/4] selftests: openvswitch: add an initial flow programming case

From: Aaron Conole <[email-masked]>
Date: Thu, 27 Jul 2023 11:34:02 -0400

Adrian Moreno <[email-masked]> writes:

> On 6/28/23 18:27, Aaron Conole wrote:
>> The openvswitch self-tests can test much of the control side of
>> the module (ie: what a vswitchd implementation would process),
>> but the actual packet forwarding cases aren't supported, making
>> the testing of limited value.
>> Add some flow parsing and an initial ARP based test case using
>> arping utility.  This lets us display flows, add some basic
>> output flows with simple matches, and test against a known good
>> forwarding case.
>> Signed-off-by: Aaron Conole <[email-masked]>
>> ---
>> NOTE: 3 lines flag the line-length checkpatch warning, but there didn't
>>        seem to bea good way of breaking the lines smaller for 2 of them.
>>        The third would still flag, even if broken at what looks like a
>>        good point to break it.
>>   .../selftests/net/openvswitch/openvswitch.sh  |  51 +++
>>   .../selftests/net/openvswitch/ovs-dpctl.py    | 408 ++++++++++++++++++
>>   2 files changed, 459 insertions(+)
>> diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh
>> b/tools/testing/selftests/net/openvswitch/openvswitch.sh
>> index 3117a4be0cd04..5cdacb3c8c925 100755
>> --- a/tools/testing/selftests/net/openvswitch/openvswitch.sh
>> +++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh
>> @@ -11,6 +11,7 @@ VERBOSE=0
>>   TRACING=0
>>     tests="
>> +	arp_ping				eth-arp: Basic arp ping between two NS
>>   	netlink_checks				ovsnl: validate netlink attrs and settings
>>   	upcall_interfaces			ovs: test the upcall interfaces"
>>   @@ -127,6 +128,16 @@ ovs_add_netns_and_veths () {
>>   	return 0
>>   }
>>   +ovs_add_flow () {
>> +	info "Adding flow to DP: sbx:$1 br:$2 flow:$3 act:$4"
>> +	ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py add-flow "$2" "$3" "$4"
>> +	if [ $? -ne 0 ]; then
>> +		echo "Flow [ $3 : $4 ] failed" >> ${ovs_dir}/debug.log
>> +		return 1
>> +	fi
>> +	return 0
>> +}
>> +
>>   usage() {
>>   	echo
>>   	echo "$0 [OPTIONS] [TEST]..."
>> @@ -141,6 +152,46 @@ usage() {
>>   	exit 1
>>   }
>>   +# arp_ping test
>> +# - client has 1500 byte MTU
>> +# - server has 1500 byte MTU
>> +# - send ARP ping between two ns
>> +test_arp_ping () {
>> +
>> +	which arping >/dev/null 2>&1 || return $ksft_skip
>> +
>> +	sbx_add "test_arp_ping" || return $?
>> +
>> +	ovs_add_dp "test_arp_ping" arpping || return 1
>> +
>> +	info "create namespaces"
>> +	for ns in client server; do
>> +		ovs_add_netns_and_veths "test_arp_ping" "arpping" "$ns" \
>> +		    "${ns:0:1}0" "${ns:0:1}1" || return 1
>> +	done
>> +
>> +	# Setup client namespace
>> +	ip netns exec client ip addr add 172.31.110.10/24 dev c1
>> +	ip netns exec client ip link set c1 up
>> + HW_CLIENT=`ip netns exec client ip link show dev c1 | grep -E
> 'link/ether [0-9a-f:]+' | awk '{print $2;}'`
>> +	info "Client hwaddr: $HW_CLIENT"
>> +
>> +	# Setup server namespace
>> +	ip netns exec server ip addr add 172.31.110.20/24 dev s1
>> +	ip netns exec server ip link set s1 up
>> + HW_SERVER=`ip netns exec server ip link show dev s1 | grep -E
> 'link/ether [0-9a-f:]+' | awk '{print $2;}'`
>> +	info "Server hwaddr: $HW_SERVER"
>> +
>> +	ovs_add_flow "test_arp_ping" arpping \
>> +
> "in_port(1),eth(),eth_type(0x0806),arp(sip=172.31.110.10,tip=172.31.110.20,sha=$HW_CLIENT,tha=ff:ff:ff:ff:ff:ff)"
> '2' || return 1
>> +	ovs_add_flow "test_arp_ping" arpping \
>> +		"in_port(2),eth(),eth_type(0x0806),arp()" '1' || return 1
>> +
>> +	ovs_sbx "test_arp_ping" ip netns exec client arping -I c1 172.31.110.20 -c 1 || return 1
>> +
>> +	return 0
>> +}
>> +
>>   # netlink_validation
>>   # - Create a dp
>>   # - check no warning with "old version" simulation
>> diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
> b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
>> index 1c8b36bc15d48..799bfb3064b90 100644
>> --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
>> +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
>> @@ -9,9 +9,12 @@ import errno
>>   import ipaddress
>>   import logging
>>   import multiprocessing
>> +import re
>>   import struct
>>   import sys
>>   import time
>> +import types
>> +import uuid
>>     try:
>>       from pyroute2 import NDB
>> @@ -59,6 +62,104 @@ def macstr(mac):
>>       return outstr
>>     +def strspn(str1, str2):
>> +    tot = 0
>> +    for char in str1:
>> +        if str2.find(char) == -1:
>> +            return tot
>> +        tot += 1
>> +    return tot
>> +
>> +
>> +def intparse(statestr, defmask="0xffffffff"):
>> +    totalparse = strspn(statestr, "0123456789abcdefABCDEFx/")
>> +    # scan until "/"
>> +    count = strspn(statestr, "x0123456789abcdefABCDEF")
>> +
>> +    firstnum = statestr[:count]
>> +    if firstnum[-1] == "/":
>> +        firstnum = firstnum[:-1]
>> +    k = int(firstnum, 0)
>> +
>> +    m = None
>> +    if defmask is not None:
>> +        secondnum = defmask
>> +        if statestr[count] == "/":
>> +            secondnum = statestr[count + 1 :]  # this is wrong...
>> +        m = int(secondnum, 0)
>> +
>> +    return statestr[totalparse + 1 :], k, m
>> +
>> +
>> +def parse_flags(flag_str, flag_vals):
>> +    bitResult = 0
>> +    maskResult = 0
>> +
>> +    if len(flag_str) == 0:
>> +        return flag_str, bitResult, maskResult
>> +
>> +    if flag_str[0].isdigit():
>> +        idx = 0
>> +        while flag_str[idx].isdigit() or flag_str[idx] == "x":
>> +            idx += 1
>> +        digits = flag_str[:idx]
>> +        flag_str = flag_str[idx:]
>> +
>> +        bitResult = int(digits, 0)
>> +        maskResult = int(digits, 0)
>> +
>> +    while len(flag_str) > 0 and (flag_str[0] == "+" or flag_str[0] == "-"):
>> +        if flag_str[0] == "+":
>> +            setFlag = True
>> +        elif flag_str[0] == "-":
>> +            setFlag = False
>> +
>> +        flag_str = flag_str[1:]
>> +
>> +        flag_len = 0
>> +        while (
>> +            flag_str[flag_len] != "+"
>> +            and flag_str[flag_len] != "-"
>> +            and flag_str[flag_len] != ","
>> +            and flag_str[flag_len] != ")"
>> +        ):
>> +            flag_len += 1
>> +
>> +        flag = flag_str[0:flag_len]
>> +
>> +        if flag in flag_vals:
>> +            if maskResult & flag_vals[flag]:
>> +                raise KeyError(
>> +                    "Flag %s set once, cannot be set in multiples" % flag
>> +                )
>> +
>> +            if setFlag:
>> +                bitResult |= flag_vals[flag]
>> +
>> +            maskResult |= flag_vals[flag]
>> +        else:
>> +            raise KeyError("Missing flag value: %s" % flag)
>> +
>> +        flag_str = flag_str[flag_len:]
>> +
>> +    return flag_str, bitResult, maskResult
>> +
>> +
>> +def parse_ct_state(statestr):
>> +    ct_flags = {
>> +        "new": 1 << 0,
>> +        "est": 1 << 1,
>> +        "rel": 1 << 2,
>> +        "rpl": 1 << 3,
>> +        "inv": 1 << 4,
>> +        "trk": 1 << 5,
>> +        "snat": 1 << 6,
>> +        "dnat": 1 << 7,
>> +    }
>> +
>> +    return parse_flags(statestr, ct_flags)
>> +
>> +
>>   def convert_mac(mac_str, mask=False):
>>       if mac_str is None or mac_str == "":
>>           mac_str = "00:00:00:00:00:00"
>> @@ -79,6 +180,61 @@ def convert_ipv4(ip, mask=False):
>>       return int(ipaddress.IPv4Address(ip))
>>     +def parse_starts_block(block_str, scanstr, returnskipped,
>> scanregex=False):
>> +    if scanregex:
>> +        m = re.search(scanstr, block_str)
>> +        if m is None:
>> +            if returnskipped:
>> +                return block_str
>> +            return False
>> +        if returnskipped:
>> +            block_str = block_str[len(m.group(0)) :]
>> +            return block_str
>> +        return True
>> +
>> +    if block_str.startswith(scanstr):
>> +        if returnskipped:
>> +            block_str = block_str[len(scanstr) :]
>> +        else:
>> +            return True
>> +
>> +    if returnskipped:
>> +        return block_str
>> +
>> +    return False
>> +
>> +
>> +def parse_extract_field(
>> +    block_str, fieldstr, scanfmt, convert, masked=False, defval=None
>> +):
>> +    if fieldstr and not block_str.startswith(fieldstr):
>> +        return block_str, defval
>> +
>> +    if fieldstr:
>> +        str_skiplen = len(fieldstr)
>> +        str_skipped = block_str[str_skiplen:]
>> +        if str_skiplen == 0:
>> +            return str_skipped, defval
>> +    else:
>> +        str_skiplen = 0
>> +        str_skipped = block_str
>> +
>> +    m = re.search(scanfmt, str_skipped)
>> +    if m is None:
>> +        raise ValueError("Bad fmt string")
>> +
>> +    data = m.group(0)
>> +    if convert:
>> +        data = convert(m.group(0))
>> +
>> +    str_skipped = str_skipped[len(m.group(0)) :]
>> +    if masked:
>> +        if str_skipped[0] == "/":
>> +            raise ValueError("Masking support TBD...")
>> +
>> +    return str_skipped, data
>> +
>> +
>>   class ovs_dp_msg(genlmsg):
>>       # include the OVS version
>>       # We need a custom header rather than just being able to rely on
>> @@ -278,6 +434,52 @@ class ovsactions(nla):
>>             return print_str
>>   +    def parse(self, actstr):
>> +        parsed = False
>> +        while len(actstr) != 0:
>> +            if actstr.startswith("drop"):
>> +                # for now, drops have no explicit action, so we
>> +                # don't need to set any attributes.  The final
>> +                # act of the processing chain will just drop the packet
>> +                return
>> +
>> +            elif parse_starts_block(actstr, "^(\d+)", False, True):
>> +                actstr, output = parse_extract_field(
>> +                    actstr, None, "(\d+)", lambda x: int(x), False, "0"
>> +                )
>> +                actstr = actstr[strspn(actstr, ", ") :]
>> +                self["attrs"].append(["OVS_ACTION_ATTR_OUTPUT", output])
>> +                parsed = True
>> +            elif parse_starts_block(actstr, "recirc(", False):
>> +                actstr, recircid = parse_extract_field(
>> +                    actstr,
>> +                    "recirc(",
>> +                    "([0-9a-fA-Fx]+)",
>> +                    lambda x: int(x, 0),
>> +                    False,
>> +                    0,
>> +                )
>> +                actstr = actstr[strspn(actstr, "), ") :]
>> +                self["attrs"].append(["OVS_ACTION_ATTR_RECIRC", recircid])
>> +                parsed = True
>> +
>> +            parse_flat_map = (
>> +                ("ct_clear", "OVS_ACTION_ATTR_CT_CLEAR"),
>> +                ("pop_vlan", "OVS_ACTION_ATTR_POP_VLAN"),
>> +                ("pop_eth", "OVS_ACTION_ATTR_POP_ETH"),
>> +                ("pop_nsh", "OVS_ACTION_ATTR_POP_NSH"),
>> +            )
>> +
>> +            for flat_act in parse_flat_map:
>> +                if parse_starts_block(actstr, flat_act[0], False):
>> +                    actstr += len(flat_act[0])
>> +                    self["attrs"].append([flat_act[1]])
>> +                    actstr = actstr[strspn(actstr, ", ") :]
>> +                    parsed = True
>> +
>> +            if not parsed:
>> +                raise ValueError("Action str: '%s' not supported" % actstr)
>> +
>>     class ovskey(nla):
>>       nla_flags = NLA_F_NESTED
>> @@ -347,6 +549,53 @@ class ovskey(nla):
>>                   init=init,
>>               )
>>   +        def parse(self, flowstr, typeInst):
>> +            if not flowstr.startswith(self.proto_str):
>> +                return None, None
>> +
>> +            k = typeInst()
>> +            m = typeInst()
>> +
>> +            flowstr = flowstr[len(self.proto_str) :]
>> +            if flowstr.startswith("("):
>> +                flowstr = flowstr[1:]
>> +
>> +            keybits = b""
>> +            maskbits = b""
>> +            for f in self.fields_map:
>> +                if flowstr.startswith(f[1]):
>> +                    # the following assumes that the field looks
>> +                    # something like 'field.' where '.' is a
>> +                    # character that we don't exactly care about.
>> +                    flowstr = flowstr[len(f[1]) + 1 :]
>> +                    splitchar = 0
>> +                    for c in flowstr:
>> +                        if c == "," or c == ")":
>> +                            break
>> +                        splitchar += 1
>> +                    data = flowstr[:splitchar]
>> +                    flowstr = flowstr[splitchar:]
>> +                else:
>> +                    data = None
>> +
>> +                if len(f) > 4:
>> +                    func = f[4]
>> +                else:
>> +                    func = f[3]
>> +                k[f[0]] = func(data)
>> +                if len(f) > 4:
>> +                    m[f[0]] = func(data, True)
>> +                else:
>> +                    m[f[0]] = func(data)
>> +
>> +                flowstr = flowstr[strspn(flowstr, ", ") :]
>> +                if len(flowstr) == 0:
>> +                    return flowstr, k, m
>> +
>> +            flowstr = flowstr[strspn(flowstr, "), ") :]
>> +
>> +            return flowstr, k, m
>> +
>>           def dpstr(self, masked=None, more=False):
>>               outstr = self.proto_str + "("
>>               first = False
>> @@ -810,6 +1059,71 @@ class ovskey(nla):
>>       class ovs_key_mpls(nla):
>>           fields = (("lse", ">I"),)
>>   +    def parse(self, flowstr, mask=None):
>> +        for field in (
>> +            ("OVS_KEY_ATTR_PRIORITY", "skb_priority", intparse),
>> +            ("OVS_KEY_ATTR_SKB_MARK", "skb_mark", intparse),
>> +            ("OVS_KEY_ATTR_RECIRC_ID", "recirc_id", intparse),
>> +            ("OVS_KEY_ATTR_DP_HASH", "dp_hash", intparse),
>> +            ("OVS_KEY_ATTR_CT_STATE", "ct_state", parse_ct_state),
>> +            ("OVS_KEY_ATTR_CT_ZONE", "ct_zone", intparse),
>> +            ("OVS_KEY_ATTR_CT_MARK", "ct_mark", intparse),
>> +            ("OVS_KEY_ATTR_IN_PORT", "in_port", intparse),
>> +            (
>> +                "OVS_KEY_ATTR_ETHERNET",
>> +                "eth",
>> +                ovskey.ethaddr,
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_ETHERTYPE",
>> +                "eth_type",
>> +                lambda x: intparse(x, "0xffff"),
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_IPV4",
>> +                "ipv4",
>> +                ovskey.ovs_key_ipv4,
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_IPV6",
>> +                "ipv6",
>> +                ovskey.ovs_key_ipv6,
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_ARP",
>> +                "arp",
>> +                ovskey.ovs_key_arp,
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_TCP",
>> +                "tcp",
>> +                ovskey.ovs_key_tcp,
>> +            ),
>> +            (
>> +                "OVS_KEY_ATTR_TCP_FLAGS",
>> +                "tcp_flags",
>> +                lambda x: parse_flags(x, None),
>> +            ),
>> +        ):
>> +            fld = field[1] + "("
>> +            if not flowstr.startswith(fld):
>> +                continue
>> +
>> +            if not isinstance(field[2], types.FunctionType):
>> +                nk = field[2]()
>> +                flowstr, k, m = nk.parse(flowstr, field[2])
>> +            else:
>> +                flowstr = flowstr[len(fld) :]
>> +                flowstr, k, m = field[2](flowstr)
>> +
>> +            if m and mask is not None:
>> +                mask["attrs"].append([field[0], m])
>> +            self["attrs"].append([field[0], k])
>> +
>> +            flowstr = flowstr[strspn(flowstr, "),") :]
>> +
>> +        return flowstr
>> +
>>       def dpstr(self, mask=None, more=False):
>>           print_str = ""
>>   @@ -1358,11 +1672,92 @@ class OvsFlow(GenericNetlinkSocket):
>>                 return print_str
>>   +        def parse(self, flowstr, actstr, dpidx=0):
>> +            OVS_UFID_F_OMIT_KEY = 1 << 0
>> +            OVS_UFID_F_OMIT_MASK = 1 << 1
>> +            OVS_UFID_F_OMIT_ACTIONS = 1 << 2
>> +
>> +            self["cmd"] = 0
>> +            self["version"] = 0
>> +            self["reserved"] = 0
>> +            self["dpifindex"] = 0
>> +
>> +            if flowstr.startswith("ufid:"):
>> +                count = 5
>> +                while flowstr[count] != ",":
>> +                    count += 1
>> +                ufidstr = flowstr[5:count]
>> +                flowstr = flowstr[count + 1 :]
>> +            else:
>> +                ufidstr = str(uuid.uuid4())
>> +            uuidRawObj = uuid.UUID(ufidstr).fields
>> +
>> +            self["attrs"].append(
>> +                [
>> +                    "OVS_FLOW_ATTR_UFID",
>> +                    [
>> +                        uuidRawObj[0],
>> +                        uuidRawObj[1] << 16 | uuidRawObj[2],
>> +                        uuidRawObj[3] << 24
>> +                        | uuidRawObj[4] << 16
>> +                        | uuidRawObj[5] & (0xFF << 32) >> 32,
>> +                        uuidRawObj[5] & (0xFFFFFFFF),
>> +                    ],
>> +                ]
>> +            )
>> +            self["attrs"].append(
>> +                [
>> +                    "OVS_FLOW_ATTR_UFID_FLAGS",
>> +                    int(
>> +                        OVS_UFID_F_OMIT_KEY
>> +                        | OVS_UFID_F_OMIT_MASK
>> +                        | OVS_UFID_F_OMIT_ACTIONS
>> +                    ),
>> +                ]
>> +            )
>> +
>> +            k = ovskey()
>> +            m = ovskey()
>> +            k.parse(flowstr, m)
>> +            self["attrs"].append(["OVS_FLOW_ATTR_KEY", k])
>> +            self["attrs"].append(["OVS_FLOW_ATTR_MASK", m])
>> +
>> +            a = ovsactions()
>> +            a.parse(actstr)
>> +            self["attrs"].append(["OVS_FLOW_ATTR_ACTIONS", a])
>> +
>>       def __init__(self):
>>           GenericNetlinkSocket.__init__(self)
>>             self.bind(OVS_FLOW_FAMILY, OvsFlow.ovs_flow_msg)
>>   +    def add_flow(self, dpifindex, flowmsg):
>> +        """
>> +        Send a new flow message to the kernel.
>> +
>> +        dpifindex should be a valid datapath obtained by calling
>> +        into the OvsDatapath lookup
>> +
>> +        flowmsg is a flow object obtained by calling a dpparse
>> +        """
>> +
>> +        flowmsg["cmd"] = OVS_FLOW_CMD_NEW
>> +        flowmsg["version"] = OVS_DATAPATH_VERSION
>> +        flowmsg["reserved"] = 0
>> +        flowmsg["dpifindex"] = dpifindex
>> +
>> +        try:
>> +            reply = self.nlm_request(
>> +                flowmsg,
>> +                msg_type=self.prid,
>> +                msg_flags=NLM_F_REQUEST | NLM_F_ACK,
>> +            )
>> +            reply = reply[0]
>> +        except NetlinkError as ne:
>> +            print(flowmsg)
>> +            raise ne
>> +        return reply
>> +
>>       def dump(self, dpifindex, flowspec=None):
>>           """
>>           Returns a list of messages containing flows.
>> @@ -1514,6 +1909,11 @@ def main(argv):
>>       dumpflcmd = subparsers.add_parser("dump-flows")
>>       dumpflcmd.add_argument("dumpdp", help="Datapath Name")
>>   +    addflcmd = subparsers.add_parser("add-flow")
>> +    addflcmd.add_argument("flbr", help="Datapath name")
>> +    addflcmd.add_argument("flow", help="Flow specification")
>> +    addflcmd.add_argument("acts", help="Flow actions")
>> +
>>       args = parser.parse_args()
>>         if args.verbose > 0:
>> @@ -1589,6 +1989,14 @@ def main(argv):
>>           rep = ovsflow.dump(rep["dpifindex"])
>>           for flow in rep:
>>               print(flow.dpstr(True if args.verbose > 0 else False))
>> +    elif hasattr(args, "flbr"):
>
> These checks on the attributes means every command must have
> attributes with different names. So if we then add del-br it must not
> have an attribute called "flbr". We could rename it to "fladdbr"
> (following the other commands) or match on the subcommand name, which
> would be cleaner imho and less error, e.g: all the datapath attributes
> can be called the same (see below).

I agree we can adjust this to use the subparser dest= and just check for
the specific command passed.  I will do that in a different series, just
to keep it as a separate thing.

>> +        rep = ovsdp.info(args.flbr, 0)
>> +        if rep is None:
>> +            print("DP '%s' not found." % args.dumpdp)
>
> "dumpdp" is not an attribute of this subcommand.

I'll fix this.

>> +            return 1
>> +        flow = OvsFlow.ovs_flow_msg()
>> +        flow.parse(args.flow, args.acts, rep["dpifindex"])
>> +        ovsflow.add_flow(rep["dpifindex"], flow)
>>         return 0
>>