diff mbox series

[net-next,1/4] selftests: openvswitch: add an initial flow programming case

Message ID 20230628162714.392047-2-aconole@redhat.com (mailing list archive)
State New
Headers show
Series selftests: openvswitch: add flow programming cases | expand

Commit Message

Aaron Conole June 28, 2023, 4:27 p.m. UTC
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 <aconole@redhat.com>
---
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(+)

Comments

Adrian Moreno July 7, 2023, 3:40 p.m. UTC | #1
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 <aconole@redhat.com>
> ---
> 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).


> +        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.

> +            return 1
> +        flow = OvsFlow.ovs_flow_msg()
> +        flow.parse(args.flow, args.acts, rep["dpifindex"])
> +        ovsflow.add_flow(rep["dpifindex"], flow)
>   
>       return 0
>
Aaron Conole July 27, 2023, 3:34 p.m. UTC | #2
Adrian Moreno <amorenoz@redhat.com> 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 <aconole@redhat.com>
>> ---
>> 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
>>
diff mbox series

Patch

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"):
+        rep = ovsdp.info(args.flbr, 0)
+        if rep is None:
+            print("DP '%s' not found." % args.dumpdp)
+            return 1
+        flow = OvsFlow.ovs_flow_msg()
+        flow.parse(args.flow, args.acts, rep["dpifindex"])
+        ovsflow.add_flow(rep["dpifindex"], flow)
 
     return 0