2022-11-22 15:01:59

by Aaron Conole

[permalink] [raw]
Subject: [RFC net-next 0/6] Allow excluding sw flow key from upcalls

Userspace applications can choose to completely ignore the kernel provided
flow key and instead regenerate a fresh key for processing in userspace.
Currently, userspace ovs-vswitchd does this in some instances (for example,
MISS upcall command). This means that kernel spends time to copy and send
the flow key into userspace without any benefit to the system.

Introduce a way for userspace to tell kernel not to send the flow key.
This lets userspace and kernel space save time and memory pressure.

This patch set is quite a bit larger because it introduces the ability to
decode a sw flow key into a compatible datapath-string. We use this as a
method of implementing a test to show that the feature is working by
decoding and dumping the flow (to make sure we capture the correct packet).

Aaron Conole (6):
openvswitch: exclude kernel flow key from upcalls
selftests: openvswitch: add interface support
selftests: openvswitch: add flow dump support
selftests: openvswitch: adjust datapath NL message
selftests: openvswitch: add upcall support
selftests: openvswitch: add exclude support for packet commands

include/uapi/linux/openvswitch.h | 6 +
net/openvswitch/datapath.c | 26 +-
net/openvswitch/datapath.h | 2 +
.../selftests/net/openvswitch/openvswitch.sh | 101 +-
.../selftests/net/openvswitch/ovs-dpctl.py | 1069 ++++++++++++++++-
5 files changed, 1183 insertions(+), 21 deletions(-)

--
2.34.3


2022-11-22 15:03:36

by Aaron Conole

[permalink] [raw]
Subject: [RFC net-next 2/6] selftests: openvswitch: add interface support

Includes an associated test to generate netns and connect
interfaces, with the option to include tcp tracing.

This will be used in the future when flow support is added
for additional test cases.

Signed-off-by: Aaron Conole <[email protected]>
---
.../selftests/net/openvswitch/openvswitch.sh | 54 ++++++++
.../selftests/net/openvswitch/ovs-dpctl.py | 120 ++++++++++++++++--
2 files changed, 165 insertions(+), 9 deletions(-)

diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools/testing/selftests/net/openvswitch/openvswitch.sh
index 7ce46700a3ae..ce14913150fe 100755
--- a/tools/testing/selftests/net/openvswitch/openvswitch.sh
+++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh
@@ -70,6 +70,49 @@ ovs_add_dp () {
on_exit "ovs_sbx $sbxname python3 $ovs_base/ovs-dpctl.py del-dp $1;"
}

+ovs_add_if () {
+ info "Adding IF to DP: br:$2 if:$3"
+ ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py add-if "$2" "$3" || return 1
+}
+
+ovs_del_if () {
+ info "Deleting IF from DP: br:$2 if:$3"
+ ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py del-if "$2" "$3" || return 1
+}
+
+ovs_netns_spawn_daemon() {
+ sbx=$1
+ shift
+ netns=$1
+ shift
+ info "spawning cmd: $*"
+ ip netns exec $netns $* >> $ovs_dir/stdout 2>> $ovs_dir/stderr &
+ pid=$!
+ ovs_sbx "$sbx" on_exit "kill -TERM $pid 2>/dev/null"
+}
+
+ovs_add_netns_and_veths () {
+ info "Adding netns attached: sbx:$1 dp:$2 {$3, $4, $5}"
+
+ ovs_sbx "$1" ip netns add "$3" || return 1
+ on_exit "ovs_sbx $1 ip netns del $3"
+ ovs_sbx "$1" ip link add "$4" type veth peer name "$5" || return 1
+ on_exit "ovs_sbx $1 ip link del $4 >/dev/null 2>&1"
+ ovs_sbx "$1" ip link set "$4" up || return 1
+ ovs_sbx "$1" ip link set "$5" netns "$3" || return 1
+ ovs_sbx "$1" ip netns exec "$3" ip link set "$5" up || return 1
+
+ if [ "$6" != "" ]; then
+ ovs_sbx "$1" ip netns exec "$4" ip addr add "$6" dev "$5" \
+ || return 1
+ fi
+ ovs_add_if "$1" "$2" "$4" || return 1
+ [ $TRACING -eq 1 ] && ovs_netns_spawn_daemon "$1" "$3" \
+ tcpdump -i any -s 65535 >> ${ovs_dir}/tcpdump_"$3".log
+
+ return 0
+}
+
usage() {
echo
echo "$0 [OPTIONS] [TEST]..."
@@ -101,6 +144,17 @@ test_netlink_checks () {
return 1
fi

+ ovs_add_netns_and_veths "test_netlink_checks" nv0 left left0 l0 || \
+ return 1
+ ovs_add_netns_and_veths "test_netlink_checks" nv0 right right0 r0 || \
+ return 1
+ [ $(python3 $ovs_base/ovs-dpctl.py show nv0 | grep port | \
+ wc -l) == 3 ] || \
+ return 1
+ ovs_del_if "test_netlink_checks" nv0 right0 || return 1
+ [ $(python3 $ovs_base/ovs-dpctl.py show nv0 | grep port | \
+ wc -l) == 2 ] || \
+ return 1
return 0
}

diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
index 3243c90d449e..338e9b2cd660 100644
--- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
+++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
@@ -170,6 +170,13 @@ class OvsDatapath(GenericNetlinkSocket):


class OvsVport(GenericNetlinkSocket):
+
+ OVS_VPORT_TYPE_NETDEV = 1
+ OVS_VPORT_TYPE_INTERNAL = 2
+ OVS_VPORT_TYPE_GRE = 3
+ OVS_VPORT_TYPE_VXLAN = 4
+ OVS_VPORT_TYPE_GENEVE = 5
+
class ovs_vport_msg(ovs_dp_msg):
nla_map = (
("OVS_VPORT_ATTR_UNSPEC", "none"),
@@ -197,17 +204,30 @@ class OvsVport(GenericNetlinkSocket):
)

def type_to_str(vport_type):
- if vport_type == 1:
+ if vport_type == OvsVport.OVS_VPORT_TYPE_NETDEV:
return "netdev"
- elif vport_type == 2:
+ elif vport_type == OvsVport.OVS_VPORT_TYPE_INTERNAL:
return "internal"
- elif vport_type == 3:
+ elif vport_type == OvsVport.OVS_VPORT_TYPE_GRE:
return "gre"
- elif vport_type == 4:
+ elif vport_type == OvsVport.OVS_VPORT_TYPE_VXLAN:
return "vxlan"
- elif vport_type == 5:
+ elif vport_type == OvsVport.OVS_VPORT_TYPE_GENEVE:
return "geneve"
- return "unknown:%d" % vport_type
+ raise ValueError("Unknown vport type:%d" % vport_type)
+
+ def str_to_type(vport_type):
+ if vport_type == "netdev":
+ return OvsVport.OVS_VPORT_TYPE_NETDEV
+ elif vport_type == "internal":
+ return OvsVport.OVS_VPORT_TYPE_INTERNAL
+ elif vport_type == "gre":
+ return OvsVport.OVS_VPORT_TYPE_INTERNAL
+ elif vport_type == "vxlan":
+ return OvsVport.OVS_VPORT_TYPE_VXLAN
+ elif vport_type == "geneve":
+ return OvsVport.OVS_VPORT_TYPE_GENEVE
+ raise ValueError("Unknown vport type: '%s'" % vport_type)

def __init__(self):
GenericNetlinkSocket.__init__(self)
@@ -238,8 +258,51 @@ class OvsVport(GenericNetlinkSocket):
raise ne
return reply

+ def attach(self, dpindex, vport_ifname, ptype):
+ msg = OvsVport.ovs_vport_msg()
+
+ msg["cmd"] = OVS_VPORT_CMD_NEW
+ msg["version"] = OVS_DATAPATH_VERSION
+ msg["reserved"] = 0
+ msg["dpifindex"] = dpindex
+ port_type = OvsVport.str_to_type(ptype)
+
+ msg["attrs"].append(["OVS_VPORT_ATTR_TYPE", port_type])
+ msg["attrs"].append(["OVS_VPORT_ATTR_NAME", vport_ifname])
+ msg["attrs"].append(["OVS_VPORT_ATTR_UPCALL_PID", [self.pid]])
+
+ try:
+ reply = self.nlm_request(
+ msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST | NLM_F_ACK
+ )
+ reply = reply[0]
+ except NetlinkError as ne:
+ raise ne
+ return reply
+
+ def detach(self, dpindex, vport_ifname):
+ msg = OvsVport.ovs_vport_msg()
+
+ msg["cmd"] = OVS_VPORT_CMD_DEL
+ msg["version"] = OVS_DATAPATH_VERSION
+ msg["reserved"] = 0
+ msg["dpifindex"] = dpindex
+ msg["attrs"].append(["OVS_VPORT_ATTR_NAME", vport_ifname])

-def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB()):
+ try:
+ reply = self.nlm_request(
+ msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST | NLM_F_ACK
+ )
+ reply = reply[0]
+ except NetlinkError as ne:
+ if ne.code == errno.ENODEV:
+ reply = None
+ else:
+ raise ne
+ return reply
+
+
+def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB(), vpl=OvsVport()):
dp_name = dp_lookup_rep.get_attr("OVS_DP_ATTR_NAME")
base_stats = dp_lookup_rep.get_attr("OVS_DP_ATTR_STATS")
megaflow_stats = dp_lookup_rep.get_attr("OVS_DP_ATTR_MEGAFLOW_STATS")
@@ -265,7 +328,6 @@ def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB()):
print(" features: 0x%X" % user_features)

# port print out
- vpl = OvsVport()
for iface in ndb.interfaces:
rep = vpl.info(iface.ifname, ifindex)
if rep is not None:
@@ -312,9 +374,25 @@ def main(argv):
deldpcmd = subparsers.add_parser("del-dp")
deldpcmd.add_argument("deldp", help="Datapath Name")

+ addifcmd = subparsers.add_parser("add-if")
+ addifcmd.add_argument("dpname", help="Datapath Name")
+ addifcmd.add_argument("addif", help="Interface name for adding")
+ addifcmd.add_argument(
+ "-t",
+ "--ptype",
+ type=str,
+ default="netdev",
+ choices=["netdev", "internal"],
+ help="Interface type (default netdev)",
+ )
+ delifcmd = subparsers.add_parser("del-if")
+ delifcmd.add_argument("dpname", help="Datapath Name")
+ delifcmd.add_argument("delif", help="Interface name for adding")
+
args = parser.parse_args()

ovsdp = OvsDatapath()
+ ovsvp = OvsVport()
ndb = NDB()

if hasattr(args, "showdp"):
@@ -328,7 +406,7 @@ def main(argv):

if rep is not None:
found = True
- print_ovsdp_full(rep, iface.index, ndb)
+ print_ovsdp_full(rep, iface.index, ndb, ovsvp)

if not found:
msg = "No DP found"
@@ -343,6 +421,30 @@ def main(argv):
print("DP '%s' added" % args.adddp)
elif hasattr(args, "deldp"):
ovsdp.destroy(args.deldp)
+ elif hasattr(args, "addif"):
+ rep = ovsdp.info(args.dpname, 0)
+ if rep is None:
+ print("DP '%s' not found." % args.dpname)
+ return 1
+ rep = ovsvp.attach(rep["dpifindex"], args.addif, args.ptype)
+ msg = "vport '%s'" % args.addif
+ if rep and rep["error"] == 0:
+ msg += " added."
+ else:
+ msg += " failed to add."
+ print(msg)
+ elif hasattr(args, "delif"):
+ rep = ovsdp.info(args.dpname, 0)
+ if rep is None:
+ print("DP '%s' not found." % args.dpname)
+ return 1
+ rep = ovsvp.detach(rep["dpifindex"], args.delif)
+ msg = "vport '%s'" % args.delif
+ if rep and rep["error"] == 0:
+ msg += " removed."
+ else:
+ msg += " failed to remove."
+ print(msg)

return 0

--
2.34.3

2022-11-22 15:15:01

by Aaron Conole

[permalink] [raw]
Subject: [RFC net-next 5/6] selftests: openvswitch: add upcall support

Future tests can make use of CMD_MISS events to do things like
cross validated packet contents with the flow key that was
generated by flow key extraction. This will also be used in
an upcoming commit to allow removing the flow key from upcall
messages.

Signed-off-by: Aaron Conole <[email protected]>
---
.../selftests/net/openvswitch/ovs-dpctl.py | 140 +++++++++++++++++-
1 file changed, 133 insertions(+), 7 deletions(-)

diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
index fe14da358901..94204af48d28 100644
--- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
+++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
@@ -8,6 +8,8 @@ import argparse
import errno
import ipaddress
import logging
+import multiprocessing
+import struct
import sys
import time

@@ -58,6 +60,53 @@ class ovs_dp_msg(genlmsg):
fields = genlmsg.fields + (("dpifindex", "I"),)


+class OvsPacket(GenericNetlinkSocket):
+ OVS_PACKET_CMD_MISS = 1 # Flow table miss
+ OVS_PACKET_CMD_ACTION = 2 # USERSPACE action
+ OVS_PACKET_CMD_EXECUTE = 3 # Apply actions to packet
+
+ class ovs_packet_msg(ovs_dp_msg):
+ nla_map = (
+ ('OVS_PACKET_ATTR_UNSPEC', 'none'),
+ ('OVS_PACKET_ATTR_PACKET', 'array(uint8)'),
+ ('OVS_PACKET_ATTR_KEY', 'nested'),
+ ('OVS_PACKET_ATTR_ACTIONS', 'nested'),
+ ('OVS_PACKET_ATTR_USERDATA', 'nested'),
+ ('OVS_PACKET_ATTR_EGRESS_TUN_KEY', 'nested'),
+ ('OVS_PACKET_ATTR_UNUSED1', 'none'),
+ ('OVS_PACKET_ATTR_UNUSED2', 'none'),
+ ('OVS_PACKET_ATTR_PROBE', 'none'),
+ ('OVS_PACKET_ATTR_MRU', 'uint16'),
+ ('OVS_PACKET_ATTR_LEN', 'uint32'),
+ ('OVS_PACKET_ATTR_HASH', 'uint64'),
+ )
+
+ def __init__(self):
+ GenericNetlinkSocket.__init__(self)
+ print("Binding to packet family")
+ self.bind(OVS_PACKET_FAMILY, OvsPacket.ovs_packet_msg)
+ print("Port", self.epid)
+
+ def upcall_handler(self, up=None):
+ print("listening on upcall packet handler:", self.epid)
+ while True:
+ try:
+ msgs = self.get()
+ for msg in msgs:
+ if not up:
+ continue
+ if msg["cmd"] == OvsPacket.OVS_PACKET_CMD_MISS:
+ up.miss(msg)
+ elif msg["cmd"] == OvsPacket.OVS_PACKET_CMD_ACTION:
+ up.action(msg)
+ elif msg["cmd"] == OvsPacket.OVS_PACKET_CMD_EXECUTE:
+ up.execute(msg)
+ else:
+ print("Unkonwn cmd: %d" % msg["cmd"])
+ except NetlinkError as ne:
+ raise ne
+
+
class OvsDatapath(GenericNetlinkSocket):

OVS_DP_F_VPORT_PIDS = 1 << 1
@@ -122,7 +171,7 @@ class OvsDatapath(GenericNetlinkSocket):

return reply

- def create(self, dpname, shouldUpcall=False, versionStr=None):
+ def create(self, dpname, shouldUpcall=False, versionStr=None, p=OvsPacket()):
msg = OvsDatapath.dp_cmd_msg()
msg["cmd"] = OVS_DP_CMD_NEW
if versionStr is None:
@@ -139,9 +188,19 @@ class OvsDatapath(GenericNetlinkSocket):
else:
dpfeatures = OvsDatapath.OVS_DP_F_VPORT_PIDS

- msg["attrs"].append(["OVS_DP_ATTR_USER_FEATURES", dpfeatures])
if not shouldUpcall:
msg["attrs"].append(["OVS_DP_ATTR_UPCALL_PID", [0]])
+ else:
+ if versionStr is None or versionStr.find(":") == -1:
+ dpfeatures |= OvsDatapath.OVS_DP_F_DISPATCH_UPCALL_PER_CPU
+ dpfeatures &= ~OvsDatapath.OVS_DP_F_VPORT_PIDS
+
+ nproc = multiprocessing.cpu_count()
+ procarray = []
+ for i in range(1, nproc):
+ procarray += [int(p.epid)]
+ msg["attrs"].append(["OVS_DP_ATTR_UPCALL_PID", procarray])
+ msg["attrs"].append(["OVS_DP_ATTR_USER_FEATURES", dpfeatures])

try:
reply = self.nlm_request(
@@ -238,9 +297,10 @@ class OvsVport(GenericNetlinkSocket):
return OvsVport.OVS_VPORT_TYPE_GENEVE
raise ValueError("Unknown vport type: '%s'" % vport_type)

- def __init__(self):
+ def __init__(self, packet=OvsPacket()):
GenericNetlinkSocket.__init__(self)
self.bind(OVS_VPORT_FAMILY, OvsVport.ovs_vport_msg)
+ self.upcall_packet = packet

def info(self, vport_name, dpifindex=0, portno=None):
msg = OvsVport.ovs_vport_msg()
@@ -278,7 +338,36 @@ class OvsVport(GenericNetlinkSocket):

msg["attrs"].append(["OVS_VPORT_ATTR_TYPE", port_type])
msg["attrs"].append(["OVS_VPORT_ATTR_NAME", vport_ifname])
- msg["attrs"].append(["OVS_VPORT_ATTR_UPCALL_PID", [self.pid]])
+ msg["attrs"].append(["OVS_VPORT_ATTR_UPCALL_PID",
+ [self.upcall_packet.epid]])
+
+ try:
+ reply = self.nlm_request(
+ msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST | NLM_F_ACK
+ )
+ reply = reply[0]
+ except NetlinkError as ne:
+ if ne.code == errno.EEXIST:
+ reply = None
+ else:
+ raise ne
+ return reply
+
+ def reset_upcall(self, dpindex, vport_ifname, p=None):
+ msg = OvsVport.ovs_vport_msg()
+
+ msg["cmd"] = OVS_VPORT_CMD_SET
+ msg["version"] = OVS_DATAPATH_VERSION
+ msg["reserved"] = 0
+ msg["dpifindex"] = dpindex
+ msg["attrs"].append(["OVS_VPORT_ATTR_NAME", vport_ifname])
+
+ if p == None:
+ p = self.upcall_packet
+ else:
+ self.upcall_packet = p
+
+ msg["attrs"].append(["OVS_VPORT_ATTR_UPCALL_PID", [p.epid]])

try:
reply = self.nlm_request(
@@ -310,6 +399,9 @@ class OvsVport(GenericNetlinkSocket):
raise ne
return reply

+ def upcall_handler(self, handler=None):
+ self.upcall_packet.upcall_handler(handler)
+

def macstr(mac):
outstr = ":".join(["%02X" % i for i in mac])
@@ -1064,6 +1156,26 @@ class OvsFlow(GenericNetlinkSocket):
raise ne
return rep

+ def miss(self, packetmsg):
+ seq = packetmsg["header"]["sequence_number"]
+ keystr = "(none)"
+ key_field = packetmsg.get_attr("OVS_PACKET_ATTR_KEY")
+ if key_field is not None:
+ keymsg = OvsFlow.ovs_flow_msg.nestedflow(data=key_field)
+ keymsg.decode()
+ keystr = keymsg.dpstr(None, True)
+
+ pktdata = packetmsg.get_attr("OVS_PACKET_ATTR_PACKET")
+ pktpres = "yes" if pktdata is not None else "no"
+
+ print("MISS upcall[%d/%s]: %s" % (seq, pktpres, keystr), flush = True)
+
+ def execute(self, packetmsg):
+ print("userspace execute command")
+
+ def action(self, packetmsg):
+ print("userspace action command")
+

def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB(), vpl=OvsVport()):
dp_name = dp_lookup_rep.get_attr("OVS_DP_ATTR_NAME")
@@ -1141,6 +1253,12 @@ def main(argv):
addifcmd = subparsers.add_parser("add-if")
addifcmd.add_argument("dpname", help="Datapath Name")
addifcmd.add_argument("addif", help="Interface name for adding")
+ addifcmd.add_argument(
+ "-u",
+ "--upcall",
+ action="store_true",
+ help="Leave open a reader for upcalls",
+ )
addifcmd.add_argument(
"-t",
"--ptype",
@@ -1162,8 +1280,9 @@ def main(argv):
if args.verbose > 1:
logging.basicConfig(level=logging.DEBUG)

+ ovspk = OvsPacket()
ovsdp = OvsDatapath()
- ovsvp = OvsVport()
+ ovsvp = OvsVport(ovspk)
ovsflow = OvsFlow()
ndb = NDB()

@@ -1186,11 +1305,13 @@ def main(argv):
msg += ":'%s'" % args.showdp
print(msg)
elif hasattr(args, "adddp"):
- rep = ovsdp.create(args.adddp, args.upcall, args.versioning)
+ rep = ovsdp.create(args.adddp, args.upcall, args.versioning, ovspk)
if rep is None:
print("DP '%s' already exists" % args.adddp)
else:
print("DP '%s' added" % args.adddp)
+ if args.upcall:
+ ovspk.upcall_handler(ovsflow)
elif hasattr(args, "deldp"):
ovsdp.destroy(args.deldp)
elif hasattr(args, "addif"):
@@ -1198,13 +1319,18 @@ def main(argv):
if rep is None:
print("DP '%s' not found." % args.dpname)
return 1
- rep = ovsvp.attach(rep["dpifindex"], args.addif, args.ptype)
+ dpindex = rep["dpifindex"]
+ rep = ovsvp.attach(dpindex, args.addif, args.ptype)
msg = "vport '%s'" % args.addif
if rep and rep["error"] == 0:
msg += " added."
else:
msg += " failed to add."
print(msg)
+ if args.upcall:
+ if rep is None:
+ rep = ovsvp.reset_upcall(dpindex, args.addif, ovspk)
+ ovsvp.upcall_handler(ovsflow)
elif hasattr(args, "delif"):
rep = ovsdp.info(args.dpname, 0)
if rep is None:
--
2.34.3