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