Skip to content

Commit 038f543

Browse files
amorenozNipaLocal
authored andcommitted
selftests: openvswitch: add psample test
Add a test to verify sampling packets via psample works. In order to do that, create a subcommand in ovs-dpctl.py to listen to on the psample multicast group and print samples. In order to also test simultaneous sFlow and psample actions, add missing parsing support for "userspace" action (via refactoring the one in sample). Signed-off-by: Adrian Moreno <[email protected]> Signed-off-by: NipaLocal <nipa@local>
1 parent 63213a2 commit 038f543

File tree

2 files changed

+231
-33
lines changed

2 files changed

+231
-33
lines changed

tools/testing/selftests/net/openvswitch/openvswitch.sh

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ tests="
2020
nat_related_v4 ip4-nat-related: ICMP related matches work with SNAT
2121
netlink_checks ovsnl: validate netlink attrs and settings
2222
upcall_interfaces ovs: test the upcall interfaces
23-
drop_reason drop: test drop reasons are emitted"
23+
drop_reason drop: test drop reasons are emitted
24+
psample psample: Sampling packets with psample"
2425

2526
info() {
2627
[ $VERBOSE = 0 ] || echo $*
@@ -170,6 +171,19 @@ ovs_drop_reason_count()
170171
return `echo "$perf_output" | grep "$pattern" | wc -l`
171172
}
172173

174+
ovs_test_flow_fails () {
175+
ERR_MSG="Flow actions may not be safe on all matching packets"
176+
177+
PRE_TEST=$(dmesg | grep -c "${ERR_MSG}")
178+
ovs_add_flow $@ &> /dev/null $@ && return 1
179+
POST_TEST=$(dmesg | grep -c "${ERR_MSG}")
180+
181+
if [ "$PRE_TEST" == "$POST_TEST" ]; then
182+
return 1
183+
fi
184+
return 0
185+
}
186+
173187
usage() {
174188
echo
175189
echo "$0 [OPTIONS] [TEST]..."
@@ -184,6 +198,87 @@ usage() {
184198
exit 1
185199
}
186200

201+
202+
# psample test
203+
# - samples packets with psample
204+
test_psample() {
205+
sbx_add "test_psample" || return $?
206+
207+
# Add a datapath with per-vport dispatching.
208+
ovs_add_dp "test_psample" psample -V 2:1 || return 1
209+
210+
info "create namespaces"
211+
ovs_add_netns_and_veths "test_psample" "psample" \
212+
client c0 c1 172.31.110.10/24 -u || return 1
213+
ovs_add_netns_and_veths "test_psample" "psample" \
214+
server s0 s1 172.31.110.20/24 -u || return 1
215+
216+
# Check if psample actions can be configured.
217+
ovs_add_flow "test_psample" psample \
218+
'in_port(1),eth(),eth_type(0x0806),arp()' 'sample(sample=100%,group_id=1,cookie=0102)'
219+
if [ $? == 1 ]; then
220+
info "no support for psample - skipping"
221+
ovs_exit_sig
222+
return $ksft_skip
223+
fi
224+
225+
ovs_del_flows "test_psample" psample
226+
227+
# Allow ARP
228+
ovs_add_flow "test_psample" psample \
229+
'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1
230+
ovs_add_flow "test_psample" psample \
231+
'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1
232+
233+
# Test action verification.
234+
OLDIFS=$IFS
235+
IFS='*'
236+
min_key='in_port(1),eth(),eth_type(0x800),ipv4()'
237+
for testcase in \
238+
"cookie to large"*"sample(sample=100%,group_id=1,cookie=1615141312111009080706050403020100)" \
239+
"no group or action"*"sample(sample=100%)" \
240+
"no group or action with cookie"*"sample(sample=100%,cookie=deadbeef)";
241+
do
242+
set -- $testcase;
243+
ovs_test_flow_fails "test_psample" psample $min_key $2
244+
if [ $? == 1 ]; then
245+
info "failed - $1"
246+
return 1
247+
fi
248+
done
249+
IFS=$OLDIFS
250+
251+
# Sample all traffic. In this case the sample action only has psample
252+
# arguments.
253+
ovs_add_flow "test_psample" psample \
254+
"in_port(1),eth(),eth_type(0x0800),ipv4(src=172.31.110.10,proto=1),icmp()" "sample(sample=100%,group_id=1,cookie=c0ffee),2"
255+
256+
# Sample all traffic. In this case the sample action has both psample
257+
# arguments and an upcall emulating simultaneous psample and
258+
# sFlow / IPFIX.
259+
nlpid=$(grep -E "listening on upcall packet handler" $ovs_dir/s0.out | cut -d ":" -f 2 | tr -d ' ')
260+
ovs_add_flow "test_psample" psample \
261+
"in_port(2),eth(),eth_type(0x0800),ipv4(src=172.31.110.20,proto=1),icmp()" "sample(sample=100%,group_id=2,cookie=eeff0c,actions(userspace(pid=${nlpid},userdata=eeff0c))),1"
262+
263+
# Record psample data.
264+
python3 $ovs_base/ovs-dpctl.py psample >$ovs_dir/psample.out 2>$ovs_dir/psample.err &
265+
pid=$!
266+
on_exit "ovs_sbx test_psample kill -TERM $pid 2>/dev/null"
267+
268+
# Send a single ping.
269+
sleep 1
270+
ovs_sbx "test_psample" ip netns exec client ping -I c1 172.31.110.20 -c 1 || return 1
271+
sleep 1
272+
273+
# We should have received one userspace action upcall and 2 psample packets.
274+
grep -E "userspace action command" $ovs_dir/s0.out >/dev/null 2>&1 || return 1
275+
276+
grep -E "rate:1,group:1,cookie:c0ffee" $ovs_dir/psample.out >/dev/null 2>&1 || return 1
277+
grep -E "rate:1,group:2,cookie:eeff0c" $ovs_dir/psample.out >/dev/null 2>&1 || return 1
278+
279+
return 0
280+
}
281+
187282
# drop_reason test
188283
# - drop packets and verify the right drop reason is reported
189284
test_drop_reason() {

tools/testing/selftests/net/openvswitch/ovs-dpctl.py

Lines changed: 135 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
from pyroute2.netlink import genlmsg
2828
from pyroute2.netlink import nla
2929
from pyroute2.netlink import nlmsg_atoms
30-
from pyroute2.netlink.exceptions import NetlinkError
30+
from pyroute2.netlink.event import EventSocket
3131
from pyroute2.netlink.generic import GenericNetlinkSocket
32+
from pyroute2.netlink.nlsocket import Marshal
33+
from pyroute2.netlink.exceptions import NetlinkError
3234
import pyroute2
3335

3436
except ModuleNotFoundError:
@@ -269,6 +271,47 @@ def parse_extract_field(
269271
return str_skipped, data
270272

271273

274+
def parse_attributes(actstr, attributes):
275+
"""Parses actstr according to attribute description. attributes must be
276+
a list of tuples (name, attribute, parse_func), e.g:
277+
("pid", OVS_USERSPACE_ATTR_PID, int)
278+
279+
Returns a list of parsed attributes followed by the remaining string.
280+
"""
281+
attrs = []
282+
for (key, attr, func) in attributes:
283+
if not actstr.startswith(key):
284+
continue
285+
286+
actstr = actstr[len(key) :]
287+
288+
if not func:
289+
attrs.append([attr])
290+
continue
291+
292+
# The length of complex attributes cannot be determined
293+
# beforehand and must be reported by the parsing func.
294+
delim = actstr[0]
295+
actstr = actstr[1:]
296+
if delim == "=":
297+
pos = strcspn(actstr, ",)")
298+
datum = func(actstr[:pos])
299+
elif delim == "(":
300+
datum, pos = func(actstr)
301+
302+
attrs.append([attr, datum])
303+
actstr = actstr[pos:]
304+
305+
if delim == "(":
306+
actstr = actstr[1:]
307+
308+
actstr = actstr[strspn(actstr, ", ") :]
309+
310+
if actstr[0] != ")":
311+
raise ValueError("Action str: '%s' unbalanced" % actstr)
312+
313+
return attrs, actstr[1:]
314+
272315
class ovs_dp_msg(genlmsg):
273316
# include the OVS version
274317
# We need a custom header rather than just being able to rely on
@@ -357,41 +400,19 @@ def percent_to_rate(percent):
357400
percent = float(percent.strip('%'))
358401
return int(math.floor(UINT32_MAX * (percent / 100.0) + .5))
359402

360-
for (key, attr, func) in (
403+
attrs_desc = (
361404
("sample", "OVS_SAMPLE_ATTR_PROBABILITY", percent_to_rate),
362405
("group_id", "OVS_SAMPLE_ATTR_PSAMPLE_GROUP", int),
363406
("cookie", "OVS_SAMPLE_ATTR_PSAMPLE_COOKIE",
364407
lambda x: list(bytearray.fromhex(x))),
365408
("actions", "OVS_SAMPLE_ATTR_ACTIONS", parse_nested_actions),
366-
):
367-
if not actstr.startswith(key):
368-
continue
369-
370-
actstr = actstr[len(key) :]
371-
372-
if not func:
373-
self["attrs"].append([attr, None])
374-
continue
375-
376-
# The length of complex attributes cannot be determined
377-
# beforehand and must be reported by the parsing func.
378-
delim = actstr[0]
379-
actstr = actstr[1:]
380-
if delim == "=":
381-
pos = strcspn(actstr, ",)")
382-
datum = func(actstr[:pos])
383-
elif delim == "(":
384-
datum, pos = func(actstr)
385-
386-
self["attrs"].append([attr, datum])
387-
actstr = actstr[pos:]
388-
actstr = actstr[strspn(actstr, ", ") :]
389-
390-
if actstr[0] != ")":
391-
raise ValueError("Action str: '%s' unbalanced" % actstr)
409+
)
392410

393-
return actstr[1:]
411+
attrs, actstr = parse_attributes(actstr, attrs_desc)
412+
for attr in attrs:
413+
self["attrs"].append(attr)
394414

415+
return actstr
395416

396417
class ctact(nla):
397418
nla_flags = NLA_F_NESTED
@@ -521,6 +542,18 @@ def dpstr(self, more=False):
521542
print_str += ")"
522543
return print_str
523544

545+
def parse(self, actstr):
546+
attrs_desc = (
547+
("pid", "OVS_USERSPACE_ATTR_PID", int),
548+
("userdata", "OVS_USERSPACE_ATTR_USERDATA",
549+
lambda x: list(bytearray.fromhex(x))),
550+
("egress_tun_port", "OVS_USERSPACE_ATTR_EGRESS_TUN_PORT", int)
551+
)
552+
attrs, actstr = parse_attributes(actstr, attrs_desc)
553+
for attr in attrs:
554+
self["attrs"].append(attr)
555+
return actstr
556+
524557
def dpstr(self, more=False):
525558
print_str = ""
526559

@@ -730,6 +763,11 @@ def parse(self, actstr):
730763
self["attrs"].append(["OVS_ACTION_ATTR_SAMPLE", sampleact])
731764
parsed = True
732765

766+
elif parse_starts_block(actstr, "userspace(", False):
767+
uact = self.userspace()
768+
actstr = uact.parse(actstr[len("userpsace(") : ])
769+
self["attrs"].append(["OVS_ACTION_ATTR_USERSPACE", uact])
770+
parsed = True
733771

734772
actstr = actstr[strspn(actstr, ", ") :]
735773
while parencount > 0:
@@ -2112,10 +2150,70 @@ def miss(self, packetmsg):
21122150
print("MISS upcall[%d/%s]: %s" % (seq, pktpres, keystr), flush=True)
21132151

21142152
def execute(self, packetmsg):
2115-
print("userspace execute command")
2153+
print("userspace execute command", flush=True)
21162154

21172155
def action(self, packetmsg):
2118-
print("userspace action command")
2156+
print("userspace action command", flush=True)
2157+
2158+
2159+
class psample_sample(genlmsg):
2160+
nla_map = (
2161+
("PSAMPLE_ATTR_IIFINDEX", "none"),
2162+
("PSAMPLE_ATTR_OIFINDEX", "none"),
2163+
("PSAMPLE_ATTR_ORIGSIZE", "none"),
2164+
("PSAMPLE_ATTR_SAMPLE_GROUP", "uint32"),
2165+
("PSAMPLE_ATTR_GROUP_SEQ", "none"),
2166+
("PSAMPLE_ATTR_SAMPLE_RATE", "uint32"),
2167+
("PSAMPLE_ATTR_DATA", "array(uint8)"),
2168+
("PSAMPLE_ATTR_GROUP_REFCOUNT", "none"),
2169+
("PSAMPLE_ATTR_TUNNEL", "none"),
2170+
("PSAMPLE_ATTR_PAD", "none"),
2171+
("PSAMPLE_ATTR_OUT_TC", "none"),
2172+
("PSAMPLE_ATTR_OUT_TC_OCC", "none"),
2173+
("PSAMPLE_ATTR_LATENCY", "none"),
2174+
("PSAMPLE_ATTR_TIMESTAMP", "none"),
2175+
("PSAMPLE_ATTR_PROTO", "none"),
2176+
("PSAMPLE_ATTR_USER_COOKIE", "array(uint8)"),
2177+
)
2178+
2179+
def dpstr(self):
2180+
fields = []
2181+
data = ""
2182+
for (attr, value) in self["attrs"]:
2183+
if attr == "PSAMPLE_ATTR_SAMPLE_GROUP":
2184+
fields.append("group:%d" % value)
2185+
if attr == "PSAMPLE_ATTR_SAMPLE_RATE":
2186+
fields.append("rate:%d" % value)
2187+
if attr == "PSAMPLE_ATTR_USER_COOKIE":
2188+
value = "".join(format(x, "02x") for x in value)
2189+
fields.append("cookie:%s" % value)
2190+
if attr == "PSAMPLE_ATTR_DATA" and len(value) > 0:
2191+
data = "data:%s" % "".join(format(x, "02x") for x in value)
2192+
2193+
return ("%s %s" % (",".join(fields), data)).strip()
2194+
2195+
2196+
class psample_msg(Marshal):
2197+
PSAMPLE_CMD_SAMPLE = 0
2198+
PSAMPLE_CMD_GET_GROUP = 1
2199+
PSAMPLE_CMD_NEW_GROUP = 2
2200+
PSAMPLE_CMD_DEL_GROUP = 3
2201+
PSAMPLE_CMD_SET_FILTER = 4
2202+
msg_map = {PSAMPLE_CMD_SAMPLE: psample_sample}
2203+
2204+
2205+
class Psample(EventSocket):
2206+
genl_family = "psample"
2207+
mcast_groups = ["packets"]
2208+
marshal_class = psample_msg
2209+
2210+
def read_samples(self):
2211+
while True:
2212+
try:
2213+
for msg in self.get():
2214+
print(msg.dpstr(), flush=True)
2215+
except NetlinkError as ne:
2216+
raise ne
21192217

21202218

21212219
def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB(), vpl=OvsVport()):
@@ -2175,7 +2273,7 @@ def main(argv):
21752273
help="Increment 'verbose' output counter.",
21762274
default=0,
21772275
)
2178-
subparsers = parser.add_subparsers()
2276+
subparsers = parser.add_subparsers(dest="subcommand")
21792277

21802278
showdpcmd = subparsers.add_parser("show")
21812279
showdpcmd.add_argument(
@@ -2232,6 +2330,8 @@ def main(argv):
22322330
delfscmd = subparsers.add_parser("del-flows")
22332331
delfscmd.add_argument("flsbr", help="Datapath name")
22342332

2333+
subparsers.add_parser("psample")
2334+
22352335
args = parser.parse_args()
22362336

22372337
if args.verbose > 0:
@@ -2246,6 +2346,9 @@ def main(argv):
22462346

22472347
sys.setrecursionlimit(100000)
22482348

2349+
if args.subcommand == "psample":
2350+
Psample().read_samples()
2351+
22492352
if hasattr(args, "showdp"):
22502353
found = False
22512354
for iface in ndb.interfaces:

0 commit comments

Comments
 (0)