1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3 4import datetime 5import random 6import re 7import time 8from lib.py import ksft_disruptive 9from lib.py import ksft_run, ksft_pr, ksft_exit 10from lib.py import ksft_eq, ksft_ne, ksft_ge, ksft_in, ksft_lt, ksft_true, ksft_raises 11from lib.py import NetDrvEpEnv 12from lib.py import EthtoolFamily, NetdevFamily 13from lib.py import KsftSkipEx, KsftFailEx 14from lib.py import rand_port, rand_ports 15from lib.py import cmd, ethtool, ip, defer, CmdExitFailure, wait_file 16from lib.py import GenerateTraffic 17 18 19def _rss_key_str(key): 20 return ":".join(["{:02x}".format(x) for x in key]) 21 22 23def _rss_key_rand(length): 24 return [random.randint(0, 255) for _ in range(length)] 25 26 27def _rss_key_check(cfg, data=None, context=0): 28 if data is None: 29 data = get_rss(cfg, context=context) 30 if 'rss-hash-key' not in data: 31 return 32 non_zero = [x for x in data['rss-hash-key'] if x != 0] 33 ksft_eq(bool(non_zero), True, comment=f"RSS key is all zero {data['rss-hash-key']}") 34 35 36def get_rss(cfg, context=0): 37 return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0] 38 39 40def get_drop_err_sum(cfg): 41 stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0] 42 cnt = 0 43 for key in ['errors', 'dropped', 'over_errors', 'fifo_errors', 44 'length_errors', 'crc_errors', 'missed_errors', 45 'frame_errors']: 46 cnt += stats["stats64"]["rx"][key] 47 return cnt, stats["stats64"]["tx"]["carrier_changes"] 48 49 50def ethtool_create(cfg, act, opts): 51 output = ethtool(f"{act} {cfg.ifname} {opts}").stdout 52 # Output will be something like: "New RSS context is 1" or 53 # "Added rule with ID 7", we want the integer from the end 54 return int(output.split()[-1]) 55 56 57def require_ntuple(cfg): 58 features = ethtool(f"-k {cfg.ifname}", json=True)[0] 59 if not features["ntuple-filters"]["active"]: 60 # ntuple is more of a capability than a config knob, don't bother 61 # trying to enable it (until some driver actually needs it). 62 raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"])) 63 64 65def require_context_cnt(cfg, need_cnt): 66 # There's no good API to get the context count, so the tests 67 # which try to add a lot opportunisitically set the count they 68 # discovered. Careful with test ordering! 69 if need_cnt and cfg.context_cnt and cfg.context_cnt < need_cnt: 70 raise KsftSkipEx(f"Test requires at least {need_cnt} contexts, but device only has {cfg.context_cnt}") 71 72 73# Get Rx packet counts for all queues, as a simple list of integers 74# if @prev is specified the prev counts will be subtracted 75def _get_rx_cnts(cfg, prev=None): 76 cfg.wait_hw_stats_settle() 77 data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True) 78 data = [x for x in data if x['queue-type'] == "rx"] 79 max_q = max([x["queue-id"] for x in data]) 80 queue_stats = [0] * (max_q + 1) 81 for q in data: 82 queue_stats[q["queue-id"]] = q["rx-packets"] 83 if prev and q["queue-id"] < len(prev): 84 queue_stats[q["queue-id"]] -= prev[q["queue-id"]] 85 return queue_stats 86 87 88def _send_traffic_check(cfg, port, name, params): 89 # params is a dict with 3 possible keys: 90 # - "target": required, which queues we expect to get iperf traffic 91 # - "empty": optional, which queues should see no traffic at all 92 # - "noise": optional, which queues we expect to see low traffic; 93 # used for queues of the main context, since some background 94 # OS activity may use those queues while we're testing 95 # the value for each is a list, or some other iterable containing queue ids. 96 97 cnts = _get_rx_cnts(cfg) 98 GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 99 cnts = _get_rx_cnts(cfg, prev=cnts) 100 101 directed = sum(cnts[i] for i in params['target']) 102 103 ksft_ge(directed, 20000, f"traffic on {name}: " + str(cnts)) 104 if params.get('noise'): 105 ksft_lt(sum(cnts[i] for i in params['noise']), directed / 2, 106 f"traffic on other queues ({name})':" + str(cnts)) 107 if params.get('empty'): 108 ksft_eq(sum(cnts[i] for i in params['empty']), 0, 109 f"traffic on inactive queues ({name}): " + str(cnts)) 110 111 112def _ntuple_rule_check(cfg, rule_id, ctx_id): 113 """Check that ntuple rule references RSS context ID""" 114 text = ethtool(f"-n {cfg.ifname} rule {rule_id}").stdout 115 pattern = f"RSS Context (ID: )?{ctx_id}" 116 ksft_true(re.search(pattern, text), "RSS context not referenced in ntuple rule") 117 118 119def test_rss_key_indir(cfg): 120 """Test basics like updating the main RSS key and indirection table.""" 121 122 qcnt = len(_get_rx_cnts(cfg)) 123 if qcnt < 3: 124 raise KsftSkipEx("Device has fewer than 3 queues (or doesn't support queue stats)") 125 126 data = get_rss(cfg) 127 want_keys = ['rss-hash-key', 'rss-hash-function', 'rss-indirection-table'] 128 for k in want_keys: 129 if k not in data: 130 raise KsftFailEx("ethtool results missing key: " + k) 131 if not data[k]: 132 raise KsftFailEx(f"ethtool results empty for '{k}': {data[k]}") 133 134 _rss_key_check(cfg, data=data) 135 key_len = len(data['rss-hash-key']) 136 137 # Set the key 138 key = _rss_key_rand(key_len) 139 ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key)) 140 141 data = get_rss(cfg) 142 ksft_eq(key, data['rss-hash-key']) 143 144 # Set the indirection table and the key together 145 key = _rss_key_rand(key_len) 146 ethtool(f"-X {cfg.ifname} equal 3 hkey " + _rss_key_str(key)) 147 reset_indir = defer(ethtool, f"-X {cfg.ifname} default") 148 149 data = get_rss(cfg) 150 _rss_key_check(cfg, data=data) 151 ksft_eq(0, min(data['rss-indirection-table'])) 152 ksft_eq(2, max(data['rss-indirection-table'])) 153 154 # Reset indirection table and set the key 155 key = _rss_key_rand(key_len) 156 ethtool(f"-X {cfg.ifname} default hkey " + _rss_key_str(key)) 157 data = get_rss(cfg) 158 _rss_key_check(cfg, data=data) 159 ksft_eq(0, min(data['rss-indirection-table'])) 160 ksft_eq(qcnt - 1, max(data['rss-indirection-table'])) 161 162 # Set the indirection table 163 ethtool(f"-X {cfg.ifname} equal 2") 164 data = get_rss(cfg) 165 ksft_eq(0, min(data['rss-indirection-table'])) 166 ksft_eq(1, max(data['rss-indirection-table'])) 167 168 # Check we only get traffic on the first 2 queues 169 170 # Retry a few times in case the flows skew to a single queue. 171 attempts = 3 172 for attempt in range(attempts): 173 cnts = _get_rx_cnts(cfg) 174 GenerateTraffic(cfg).wait_pkts_and_stop(20000) 175 cnts = _get_rx_cnts(cfg, prev=cnts) 176 if cnts[0] >= 5000 and cnts[1] >= 5000: 177 break 178 ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) 179 180 # 2 queues, 20k packets, must be at least 5k per queue 181 ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts)) 182 ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts)) 183 # The other queues should be unused 184 ksft_eq(sum(cnts[2:]), 0, "traffic on unused queues: " + str(cnts)) 185 186 # Restore, and check traffic gets spread again 187 reset_indir.exec() 188 189 for attempt in range(attempts): 190 cnts = _get_rx_cnts(cfg) 191 GenerateTraffic(cfg).wait_pkts_and_stop(20000) 192 cnts = _get_rx_cnts(cfg, prev=cnts) 193 if qcnt > 4: 194 if sum(cnts[:2]) < sum(cnts[2:]): 195 break 196 else: 197 if cnts[2] >= 3500: 198 break 199 ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts)) 200 201 if qcnt > 4: 202 # First two queues get less traffic than all the rest 203 ksft_lt(sum(cnts[:2]), sum(cnts[2:]), 204 "traffic distributed: " + str(cnts)) 205 else: 206 # When queue count is low make sure third queue got significant pkts 207 ksft_ge(cnts[2], 3500, "traffic distributed: " + str(cnts)) 208 209 210def test_rss_queue_reconfigure(cfg, main_ctx=True): 211 """Make sure queue changes can't override requested RSS config. 212 213 By default main RSS table should change to include all queues. 214 When user sets a specific RSS config the driver should preserve it, 215 even when queue count changes. Driver should refuse to deactivate 216 queues used in the user-set RSS config. 217 """ 218 219 if not main_ctx: 220 require_ntuple(cfg) 221 222 # Start with 4 queues, an arbitrary known number. 223 try: 224 qcnt = len(_get_rx_cnts(cfg)) 225 ethtool(f"-L {cfg.ifname} combined 4") 226 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 227 except: 228 raise KsftSkipEx("Not enough queues for the test or qstat not supported") 229 230 if main_ctx: 231 ctx_id = 0 232 ctx_ref = "" 233 else: 234 ctx_id = ethtool_create(cfg, "-X", "context new") 235 ctx_ref = f"context {ctx_id}" 236 defer(ethtool, f"-X {cfg.ifname} {ctx_ref} delete") 237 238 # Indirection table should be distributing to all queues. 239 data = get_rss(cfg, context=ctx_id) 240 ksft_eq(0, min(data['rss-indirection-table'])) 241 ksft_eq(3, max(data['rss-indirection-table'])) 242 243 # Increase queues, indirection table should be distributing to all queues. 244 # It's unclear whether tables of additional contexts should be reset, too. 245 if main_ctx: 246 ethtool(f"-L {cfg.ifname} combined 5") 247 data = get_rss(cfg) 248 ksft_eq(0, min(data['rss-indirection-table'])) 249 ksft_eq(4, max(data['rss-indirection-table'])) 250 ethtool(f"-L {cfg.ifname} combined 4") 251 252 # Configure the table explicitly 253 port = rand_port() 254 ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 0 1") 255 if main_ctx: 256 other_key = 'empty' 257 defer(ethtool, f"-X {cfg.ifname} default") 258 else: 259 other_key = 'noise' 260 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 261 ntuple = ethtool_create(cfg, "-N", flow) 262 defer(ethtool, f"-N {cfg.ifname} delete {ntuple}") 263 264 _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3), 265 other_key: (1, 2) }) 266 267 # We should be able to increase queues, but table should be left untouched 268 ethtool(f"-L {cfg.ifname} combined 5") 269 data = get_rss(cfg, context=ctx_id) 270 ksft_eq({0, 3}, set(data['rss-indirection-table'])) 271 272 _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3), 273 other_key: (1, 2, 4) }) 274 275 # Setting queue count to 3 should fail, queue 3 is used 276 try: 277 ethtool(f"-L {cfg.ifname} combined 3") 278 except CmdExitFailure: 279 pass 280 else: 281 raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})") 282 283 if not main_ctx: 284 ethtool(f"-L {cfg.ifname} combined 4") 285 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 1" 286 try: 287 # this targets queue 4, which doesn't exist 288 ntuple2 = ethtool_create(cfg, "-N", flow) 289 defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}") 290 except CmdExitFailure: 291 pass 292 else: 293 raise Exception(f"Driver didn't prevent us from targeting a nonexistent queue (context {ctx_id})") 294 # change the table to target queues 0 and 2 295 ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 1 0") 296 # ntuple rule therefore targets queues 1 and 3 297 try: 298 ntuple2 = ethtool_create(cfg, "-N", flow) 299 except CmdExitFailure: 300 ksft_pr("Driver does not support rss + queue offset") 301 return 302 303 defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}") 304 # should replace existing filter 305 ksft_eq(ntuple, ntuple2) 306 _send_traffic_check(cfg, port, ctx_ref, { 'target': (1, 3), 307 'noise' : (0, 2) }) 308 # Setting queue count to 3 should fail, queue 3 is used 309 try: 310 ethtool(f"-L {cfg.ifname} combined 3") 311 except CmdExitFailure: 312 pass 313 else: 314 raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})") 315 316 317def test_rss_resize(cfg): 318 """Test resizing of the RSS table. 319 320 Some devices dynamically increase and decrease the size of the RSS 321 indirection table based on the number of enabled queues. 322 When that happens driver must maintain the balance of entries 323 (preferably duplicating the smaller table). 324 """ 325 326 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}}) 327 ch_max = channels['combined-max'] 328 qcnt = channels['combined-count'] 329 330 if ch_max < 2: 331 raise KsftSkipEx(f"Not enough queues for the test: {ch_max}") 332 333 ethtool(f"-L {cfg.ifname} combined 2") 334 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 335 336 ethtool(f"-X {cfg.ifname} weight 1 7") 337 defer(ethtool, f"-X {cfg.ifname} default") 338 339 ethtool(f"-L {cfg.ifname} combined {ch_max}") 340 data = get_rss(cfg) 341 ksft_eq(0, min(data['rss-indirection-table'])) 342 ksft_eq(1, max(data['rss-indirection-table'])) 343 344 ksft_eq(7, 345 data['rss-indirection-table'].count(1) / 346 data['rss-indirection-table'].count(0), 347 f"Table imbalance after resize: {data['rss-indirection-table']}") 348 349 350def test_hitless_key_update(cfg): 351 """Test that flows may be rehashed without impacting traffic. 352 353 Some workloads may want to rehash the flows in response to an imbalance. 354 Most effective way to do that is changing the RSS key. Check that changing 355 the key does not cause link flaps or traffic disruption. 356 357 Disrupting traffic for key update is not a bug, but makes the key 358 update unusable for rehashing under load. 359 """ 360 data = get_rss(cfg) 361 key_len = len(data['rss-hash-key']) 362 363 ethnl = EthtoolFamily() 364 key = random.randbytes(key_len) 365 366 tgen = GenerateTraffic(cfg) 367 try: 368 errors0, carrier0 = get_drop_err_sum(cfg) 369 t0 = datetime.datetime.now() 370 ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, "hkey": key}) 371 t1 = datetime.datetime.now() 372 errors1, carrier1 = get_drop_err_sum(cfg) 373 finally: 374 tgen.wait_pkts_and_stop(5000) 375 376 ksft_lt((t1 - t0).total_seconds(), 0.15) 377 ksft_eq(errors1 - errors0, 0) 378 ksft_eq(carrier1 - carrier0, 0) 379 380 381def test_rss_context_dump(cfg): 382 """ 383 Test dumping RSS contexts. This tests mostly exercises the kernel APIs. 384 """ 385 386 # Get a random key of the right size 387 data = get_rss(cfg) 388 if 'rss-hash-key' in data: 389 key_data = _rss_key_rand(len(data['rss-hash-key'])) 390 key = _rss_key_str(key_data) 391 else: 392 key_data = [] 393 key = "ba:ad" 394 395 ids = [] 396 try: 397 ids.append(ethtool_create(cfg, "-X", f"context new")) 398 defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete") 399 400 ids.append(ethtool_create(cfg, "-X", f"context new weight 1 1")) 401 defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete") 402 403 ids.append(ethtool_create(cfg, "-X", f"context new hkey {key}")) 404 defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete") 405 except CmdExitFailure: 406 if not ids: 407 raise KsftSkipEx("Unable to add any contexts") 408 ksft_pr(f"Added only {len(ids)} out of 3 contexts") 409 410 expect_tuples = set([(cfg.ifname, -1)] + [(cfg.ifname, ctx_id) for ctx_id in ids]) 411 412 # Dump all 413 ctxs = cfg.ethnl.rss_get({}, dump=True) 414 tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs] 415 ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump") 416 ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname]) 417 ksft_eq(expect_tuples, ctx_tuples) 418 419 # Sanity-check the results 420 for data in ctxs: 421 ksft_ne(set(data.get('indir', [1])), {0}, "indir table is all zero") 422 ksft_ne(set(data.get('hkey', [1])), {0}, "key is all zero") 423 424 # More specific checks 425 if len(ids) > 1 and data.get('context') == ids[1]: 426 ksft_eq(set(data['indir']), {0, 1}, 427 "ctx1 - indir table mismatch") 428 if len(ids) > 2 and data.get('context') == ids[2]: 429 ksft_eq(data['hkey'], bytes(key_data), "ctx2 - key mismatch") 430 431 # Ifindex filter 432 ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True) 433 tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs] 434 ctx_tuples = set(tuples) 435 ksft_eq(len(tuples), len(ctx_tuples), "duplicates in context dump") 436 ksft_eq(expect_tuples, ctx_tuples) 437 438 # Skip ctx 0 439 expect_tuples.remove((cfg.ifname, -1)) 440 441 ctxs = cfg.ethnl.rss_get({'start-context': 1}, dump=True) 442 tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs] 443 ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump") 444 ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname]) 445 ksft_eq(expect_tuples, ctx_tuples) 446 447 # And finally both with ifindex and skip main 448 ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}, 'start-context': 1}, dump=True) 449 ctx_tuples = set([(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]) 450 ksft_eq(expect_tuples, ctx_tuples) 451 452 453def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None): 454 """ 455 Test separating traffic into RSS contexts. 456 The queues will be allocated 2 for each context: 457 ctx0 ctx1 ctx2 ctx3 458 [0 1] [2 3] [4 5] [6 7] ... 459 """ 460 461 require_ntuple(cfg) 462 463 requested_ctx_cnt = ctx_cnt 464 465 # Try to allocate more queues when necessary 466 qcnt = len(_get_rx_cnts(cfg)) 467 if qcnt < 2 + 2 * ctx_cnt: 468 try: 469 ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}") 470 ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}") 471 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 472 except: 473 raise KsftSkipEx("Not enough queues for the test") 474 475 ports = rand_ports(ctx_cnt) 476 477 # Use queues 0 and 1 for normal traffic 478 ethtool(f"-X {cfg.ifname} equal 2") 479 defer(ethtool, f"-X {cfg.ifname} default") 480 481 for i in range(ctx_cnt): 482 want_cfg = f"start {2 + i * 2} equal 2" 483 create_cfg = want_cfg if create_with_cfg else "" 484 485 try: 486 ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}") 487 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 488 except CmdExitFailure: 489 # try to carry on and skip at the end 490 if i == 0: 491 raise 492 ksft_pr(f"Failed to create context {i + 1}, trying to test what we got") 493 ctx_cnt = i 494 if cfg.context_cnt is None: 495 cfg.context_cnt = ctx_cnt 496 break 497 498 _rss_key_check(cfg, context=ctx_id) 499 500 if not create_with_cfg: 501 ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}") 502 _rss_key_check(cfg, context=ctx_id) 503 504 # Sanity check the context we just created 505 data = get_rss(cfg, ctx_id) 506 ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data)) 507 ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data)) 508 509 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}" 510 ntuple = ethtool_create(cfg, "-N", flow) 511 defer(ethtool, f"-N {cfg.ifname} delete {ntuple}") 512 513 _ntuple_rule_check(cfg, ntuple, ctx_id) 514 515 for i in range(ctx_cnt): 516 _send_traffic_check(cfg, ports[i], f"context {i}", 517 { 'target': (2+i*2, 3+i*2), 518 'noise': (0, 1), 519 'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt)) }) 520 521 if requested_ctx_cnt != ctx_cnt: 522 raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}") 523 524 525def test_rss_context4(cfg): 526 test_rss_context(cfg, 4) 527 528 529def test_rss_context32(cfg): 530 test_rss_context(cfg, 32) 531 532 533def test_rss_context4_create_with_cfg(cfg): 534 test_rss_context(cfg, 4, create_with_cfg=True) 535 536 537def test_rss_context_queue_reconfigure(cfg): 538 test_rss_queue_reconfigure(cfg, main_ctx=False) 539 540 541def test_rss_context_out_of_order(cfg, ctx_cnt=4): 542 """ 543 Test separating traffic into RSS contexts. 544 Contexts are removed in semi-random order, and steering re-tested 545 to make sure removal doesn't break steering to surviving contexts. 546 Test requires 3 contexts to work. 547 """ 548 549 require_ntuple(cfg) 550 require_context_cnt(cfg, 4) 551 552 # Try to allocate more queues when necessary 553 qcnt = len(_get_rx_cnts(cfg)) 554 if qcnt < 2 + 2 * ctx_cnt: 555 try: 556 ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}") 557 ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}") 558 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 559 except: 560 raise KsftSkipEx("Not enough queues for the test") 561 562 ntuple = [] 563 ctx = [] 564 ports = rand_ports(ctx_cnt) 565 566 def remove_ctx(idx): 567 ntuple[idx].exec() 568 ntuple[idx] = None 569 ctx[idx].exec() 570 ctx[idx] = None 571 572 def check_traffic(): 573 for i in range(ctx_cnt): 574 if ctx[i]: 575 expected = { 576 'target': (2+i*2, 3+i*2), 577 'noise': (0, 1), 578 'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt)) 579 } 580 else: 581 expected = { 582 'target': (0, 1), 583 'empty': range(2, 2+2*ctx_cnt) 584 } 585 586 _send_traffic_check(cfg, ports[i], f"context {i}", expected) 587 588 # Use queues 0 and 1 for normal traffic 589 ethtool(f"-X {cfg.ifname} equal 2") 590 defer(ethtool, f"-X {cfg.ifname} default") 591 592 for i in range(ctx_cnt): 593 ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2") 594 ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")) 595 596 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}" 597 ntuple_id = ethtool_create(cfg, "-N", flow) 598 ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")) 599 600 check_traffic() 601 602 # Remove middle context 603 remove_ctx(ctx_cnt // 2) 604 check_traffic() 605 606 # Remove first context 607 remove_ctx(0) 608 check_traffic() 609 610 # Remove last context 611 remove_ctx(-1) 612 check_traffic() 613 614 615def test_rss_context_overlap(cfg, other_ctx=0): 616 """ 617 Test contexts overlapping with each other. 618 Use 4 queues for the main context, but only queues 2 and 3 for context 1. 619 """ 620 621 require_ntuple(cfg) 622 if other_ctx: 623 require_context_cnt(cfg, 2) 624 625 queue_cnt = len(_get_rx_cnts(cfg)) 626 if queue_cnt < 4: 627 try: 628 ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 629 ethtool(f"-L {cfg.ifname} combined 4") 630 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 631 except: 632 raise KsftSkipEx("Not enough queues for the test") 633 634 if other_ctx == 0: 635 ethtool(f"-X {cfg.ifname} equal 4") 636 defer(ethtool, f"-X {cfg.ifname} default") 637 else: 638 other_ctx = ethtool_create(cfg, "-X", "context new") 639 ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4") 640 defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete") 641 642 ctx_id = ethtool_create(cfg, "-X", "context new") 643 ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2") 644 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 645 646 port = rand_port() 647 if other_ctx: 648 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {other_ctx}" 649 ntuple_id = ethtool_create(cfg, "-N", flow) 650 ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 651 652 # Test the main context 653 cnts = _get_rx_cnts(cfg) 654 GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 655 cnts = _get_rx_cnts(cfg, prev=cnts) 656 657 ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts)) 658 ksft_ge(sum(cnts[ :2]), 7000, "traffic on main context (1/2): " + str(cnts)) 659 ksft_ge(sum(cnts[2:4]), 7000, "traffic on main context (2/2): " + str(cnts)) 660 if other_ctx == 0: 661 ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 662 663 # Now create a rule for context 1 and make sure traffic goes to a subset 664 if other_ctx: 665 ntuple.exec() 666 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 667 ntuple_id = ethtool_create(cfg, "-N", flow) 668 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 669 670 cnts = _get_rx_cnts(cfg) 671 GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 672 cnts = _get_rx_cnts(cfg, prev=cnts) 673 674 directed = sum(cnts[2:4]) 675 ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts)) 676 ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts)) 677 if other_ctx == 0: 678 ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 679 680 681def test_rss_context_overlap2(cfg): 682 test_rss_context_overlap(cfg, True) 683 684 685def test_flow_add_context_missing(cfg): 686 """ 687 Test that we are not allowed to add a rule pointing to an RSS context 688 which was never created. 689 """ 690 691 require_ntuple(cfg) 692 693 # Find a context which doesn't exist 694 for ctx_id in range(1, 100): 695 try: 696 get_rss(cfg, context=ctx_id) 697 except CmdExitFailure: 698 break 699 700 with ksft_raises(CmdExitFailure) as cm: 701 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}" 702 ntuple_id = ethtool_create(cfg, "-N", flow) 703 ethtool(f"-N {cfg.ifname} delete {ntuple_id}") 704 if cm.exception: 705 ksft_in('Invalid argument', cm.exception.cmd.stderr) 706 707 708def test_delete_rss_context_busy(cfg): 709 """ 710 Test that deletion returns -EBUSY when an rss context is being used 711 by an ntuple filter. 712 """ 713 714 require_ntuple(cfg) 715 716 # create additional rss context 717 ctx_id = ethtool_create(cfg, "-X", "context new") 718 ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 719 720 # utilize context from ntuple filter 721 port = rand_port() 722 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 723 ntuple_id = ethtool_create(cfg, "-N", flow) 724 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 725 726 # attempt to delete in-use context 727 try: 728 ctx_deleter.exec_only() 729 ctx_deleter.cancel() 730 raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}") 731 except CmdExitFailure: 732 pass 733 734 735def test_rss_ntuple_addition(cfg): 736 """ 737 Test that the queue offset (ring_cookie) of an ntuple rule is added 738 to the queue number read from the indirection table. 739 """ 740 741 require_ntuple(cfg) 742 743 queue_cnt = len(_get_rx_cnts(cfg)) 744 if queue_cnt < 4: 745 try: 746 ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 747 ethtool(f"-L {cfg.ifname} combined 4") 748 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 749 except: 750 raise KsftSkipEx("Not enough queues for the test") 751 752 # Use queue 0 for normal traffic 753 ethtool(f"-X {cfg.ifname} equal 1") 754 defer(ethtool, f"-X {cfg.ifname} default") 755 756 # create additional rss context 757 ctx_id = ethtool_create(cfg, "-X", "context new equal 2") 758 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 759 760 # utilize context from ntuple filter 761 port = rand_port() 762 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2" 763 try: 764 ntuple_id = ethtool_create(cfg, "-N", flow) 765 except CmdExitFailure: 766 raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported") 767 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 768 769 _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3), 770 'empty' : (1,), 771 'noise' : (0,) }) 772 773 774def test_rss_default_context_rule(cfg): 775 """ 776 Allocate a port, direct this port to context 0, then create a new RSS 777 context and steer all TCP traffic to it (context 1). Verify that: 778 * Traffic to the specific port continues to use queues of the main 779 context (0/1). 780 * Traffic to any other TCP port is redirected to the new context 781 (queues 2/3). 782 """ 783 784 require_ntuple(cfg) 785 786 queue_cnt = len(_get_rx_cnts(cfg)) 787 if queue_cnt < 4: 788 try: 789 ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 790 ethtool(f"-L {cfg.ifname} combined 4") 791 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 792 except Exception as exc: 793 raise KsftSkipEx("Not enough queues for the test") from exc 794 795 # Use queues 0 and 1 for the main context 796 ethtool(f"-X {cfg.ifname} equal 2") 797 defer(ethtool, f"-X {cfg.ifname} default") 798 799 # Create a new RSS context that uses queues 2 and 3 800 ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 2") 801 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 802 803 # Generic low-priority rule: redirect all TCP traffic to the new context. 804 # Give it an explicit higher location number (lower priority). 805 flow_generic = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} context {ctx_id} loc 1" 806 ethtool(f"-N {cfg.ifname} {flow_generic}") 807 defer(ethtool, f"-N {cfg.ifname} delete 1") 808 809 ports = rand_ports(2) 810 # Specific high-priority rule for a random port that should stay on context 0. 811 # Assign loc 0 so it is evaluated before the generic rule. 812 port_main = ports[0] 813 flow_main = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_main} context 0 loc 0" 814 ethtool(f"-N {cfg.ifname} {flow_main}") 815 defer(ethtool, f"-N {cfg.ifname} delete 0") 816 817 _ntuple_rule_check(cfg, 1, ctx_id) 818 819 # Verify that traffic matching the specific rule still goes to queues 0/1 820 _send_traffic_check(cfg, port_main, "context 0", 821 { 'target': (0, 1), 822 'empty' : (2, 3) }) 823 824 # And that traffic for any other port is steered to the new context 825 port_other = ports[1] 826 _send_traffic_check(cfg, port_other, f"context {ctx_id}", 827 { 'target': (2, 3), 828 'noise' : (0, 1) }) 829 830 831@ksft_disruptive 832def test_rss_context_persist_ifupdown(cfg, pre_down=False): 833 """ 834 Test that RSS contexts and their associated ntuple filters persist across 835 an interface down/up cycle. 836 837 """ 838 839 require_ntuple(cfg) 840 841 qcnt = len(_get_rx_cnts(cfg)) 842 if qcnt < 6: 843 try: 844 ethtool(f"-L {cfg.ifname} combined 6") 845 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 846 except Exception as exc: 847 raise KsftSkipEx("Not enough queues for the test") from exc 848 849 ethtool(f"-X {cfg.ifname} equal 2") 850 defer(ethtool, f"-X {cfg.ifname} default") 851 852 ifup = defer(ip, f"link set dev {cfg.ifname} up") 853 if pre_down: 854 ip(f"link set dev {cfg.ifname} down") 855 856 try: 857 ctx1_id = ethtool_create(cfg, "-X", "context new start 2 equal 2") 858 defer(ethtool, f"-X {cfg.ifname} context {ctx1_id} delete") 859 except CmdExitFailure as exc: 860 raise KsftSkipEx("Create context not supported with interface down") from exc 861 862 ctx2_id = ethtool_create(cfg, "-X", "context new start 4 equal 2") 863 defer(ethtool, f"-X {cfg.ifname} context {ctx2_id} delete") 864 865 port_ctx2 = rand_port() 866 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_ctx2} context {ctx2_id}" 867 ntuple_id = ethtool_create(cfg, "-N", flow) 868 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 869 870 if not pre_down: 871 ip(f"link set dev {cfg.ifname} down") 872 ifup.exec() 873 874 wait_file(f"/sys/class/net/{cfg.ifname}/carrier", 875 lambda x: x.strip() == "1", deadline=20) 876 877 remote_addr = cfg.remote_addr_v[cfg.addr_ipver] 878 for _ in range(10): 879 if cmd(f"ping -c 1 -W 1 {remote_addr}", fail=False).ret == 0: 880 break 881 time.sleep(1) 882 else: 883 raise KsftSkipEx("Cannot reach remote host after interface up") 884 885 ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True) 886 887 data1 = [c for c in ctxs if c.get('context') == ctx1_id] 888 ksft_eq(len(data1), 1, f"Context {ctx1_id} should persist after ifup") 889 890 data2 = [c for c in ctxs if c.get('context') == ctx2_id] 891 ksft_eq(len(data2), 1, f"Context {ctx2_id} should persist after ifup") 892 893 _ntuple_rule_check(cfg, ntuple_id, ctx2_id) 894 895 cnts = _get_rx_cnts(cfg) 896 GenerateTraffic(cfg).wait_pkts_and_stop(20000) 897 cnts = _get_rx_cnts(cfg, prev=cnts) 898 899 main_traffic = sum(cnts[0:2]) 900 ksft_ge(main_traffic, 18000, f"Main context traffic distribution: {cnts}") 901 ksft_lt(sum(cnts[2:6]), 500, f"Other context queues should be mostly empty: {cnts}") 902 903 _send_traffic_check(cfg, port_ctx2, f"context {ctx2_id}", 904 {'target': (4, 5), 905 'noise': (0, 1), 906 'empty': (2, 3)}) 907 908 909def test_rss_context_persist_create_and_ifdown(cfg): 910 """ 911 Create RSS contexts then cycle the interface down and up. 912 """ 913 test_rss_context_persist_ifupdown(cfg, pre_down=False) 914 915 916def test_rss_context_persist_ifdown_and_create(cfg): 917 """ 918 Bring interface down first, then create RSS contexts and bring up. 919 """ 920 test_rss_context_persist_ifupdown(cfg, pre_down=True) 921 922 923def main() -> None: 924 with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 925 cfg.context_cnt = None 926 cfg.ethnl = EthtoolFamily() 927 cfg.netdevnl = NetdevFamily() 928 929 ksft_run([test_rss_key_indir, test_rss_queue_reconfigure, 930 test_rss_resize, test_hitless_key_update, 931 test_rss_context, test_rss_context4, test_rss_context32, 932 test_rss_context_dump, test_rss_context_queue_reconfigure, 933 test_rss_context_overlap, test_rss_context_overlap2, 934 test_rss_context_out_of_order, test_rss_context4_create_with_cfg, 935 test_flow_add_context_missing, 936 test_delete_rss_context_busy, test_rss_ntuple_addition, 937 test_rss_default_context_rule, 938 test_rss_context_persist_create_and_ifdown, 939 test_rss_context_persist_ifdown_and_create], 940 args=(cfg, )) 941 ksft_exit() 942 943 944if __name__ == "__main__": 945 main() 946