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