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 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 ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts)) 659 ksft_ge(sum(cnts[ :2]), 7000, "traffic on main context (1/2): " + str(cnts)) 660 ksft_ge(sum(cnts[2:4]), 7000, "traffic on main context (2/2): " + str(cnts)) 661 if other_ctx == 0: 662 ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 663 664 # Now create a rule for context 1 and make sure traffic goes to a subset 665 if other_ctx: 666 ntuple.exec() 667 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 668 ntuple_id = ethtool_create(cfg, "-N", flow) 669 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 670 671 cnts = _get_rx_cnts(cfg) 672 GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 673 cnts = _get_rx_cnts(cfg, prev=cnts) 674 675 directed = sum(cnts[2:4]) 676 ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts)) 677 ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts)) 678 if other_ctx == 0: 679 ksft_eq(sum(cnts[4: ]), 0, "traffic on other queues: " + str(cnts)) 680 681 682def test_rss_context_overlap2(cfg): 683 test_rss_context_overlap(cfg, True) 684 685 686def test_flow_add_context_missing(cfg): 687 """ 688 Test that we are not allowed to add a rule pointing to an RSS context 689 which was never created. 690 """ 691 692 require_ntuple(cfg) 693 694 # Find a context which doesn't exist 695 for ctx_id in range(1, 100): 696 try: 697 get_rss(cfg, context=ctx_id) 698 except CmdExitFailure: 699 break 700 701 with ksft_raises(CmdExitFailure) as cm: 702 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}" 703 ntuple_id = ethtool_create(cfg, "-N", flow) 704 ethtool(f"-N {cfg.ifname} delete {ntuple_id}") 705 if cm.exception: 706 ksft_in('Invalid argument', cm.exception.cmd.stderr) 707 708 709def test_delete_rss_context_busy(cfg): 710 """ 711 Test that deletion returns -EBUSY when an rss context is being used 712 by an ntuple filter. 713 """ 714 715 require_ntuple(cfg) 716 717 # create additional rss context 718 ctx_id = ethtool_create(cfg, "-X", "context new") 719 ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 720 721 # utilize context from ntuple filter 722 port = rand_port() 723 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 724 ntuple_id = ethtool_create(cfg, "-N", flow) 725 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 726 727 # attempt to delete in-use context 728 try: 729 ctx_deleter.exec_only() 730 ctx_deleter.cancel() 731 raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}") 732 except CmdExitFailure: 733 pass 734 735 736def test_rss_ntuple_addition(cfg): 737 """ 738 Test that the queue offset (ring_cookie) of an ntuple rule is added 739 to the queue number read from the indirection table. 740 """ 741 742 require_ntuple(cfg) 743 744 queue_cnt = len(_get_rx_cnts(cfg)) 745 if queue_cnt < 4: 746 try: 747 ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 748 ethtool(f"-L {cfg.ifname} combined 4") 749 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 750 except: 751 raise KsftSkipEx("Not enough queues for the test") 752 753 # Use queue 0 for normal traffic 754 ethtool(f"-X {cfg.ifname} equal 1") 755 defer(ethtool, f"-X {cfg.ifname} default") 756 757 # create additional rss context 758 ctx_id = ethtool_create(cfg, "-X", "context new equal 2") 759 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 760 761 # utilize context from ntuple filter 762 port = rand_port() 763 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2" 764 try: 765 ntuple_id = ethtool_create(cfg, "-N", flow) 766 except CmdExitFailure: 767 raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported") 768 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 769 770 _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3), 771 'empty' : (1,), 772 'noise' : (0,) }) 773 774 775def test_rss_default_context_rule(cfg): 776 """ 777 Allocate a port, direct this port to context 0, then create a new RSS 778 context and steer all TCP traffic to it (context 1). Verify that: 779 * Traffic to the specific port continues to use queues of the main 780 context (0/1). 781 * Traffic to any other TCP port is redirected to the new context 782 (queues 2/3). 783 """ 784 785 require_ntuple(cfg) 786 787 queue_cnt = len(_get_rx_cnts(cfg)) 788 if queue_cnt < 4: 789 try: 790 ksft_pr(f"Increasing queue count {queue_cnt} -> 4") 791 ethtool(f"-L {cfg.ifname} combined 4") 792 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 793 except Exception as exc: 794 raise KsftSkipEx("Not enough queues for the test") from exc 795 796 # Use queues 0 and 1 for the main context 797 ethtool(f"-X {cfg.ifname} equal 2") 798 defer(ethtool, f"-X {cfg.ifname} default") 799 800 # Create a new RSS context that uses queues 2 and 3 801 ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 2") 802 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 803 804 # Generic low-priority rule: redirect all TCP traffic to the new context. 805 # Give it an explicit higher location number (lower priority). 806 flow_generic = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} context {ctx_id} loc 1" 807 ethtool(f"-N {cfg.ifname} {flow_generic}") 808 defer(ethtool, f"-N {cfg.ifname} delete 1") 809 810 ports = rand_ports(2) 811 # Specific high-priority rule for a random port that should stay on context 0. 812 # Assign loc 0 so it is evaluated before the generic rule. 813 port_main = ports[0] 814 flow_main = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_main} context 0 loc 0" 815 ethtool(f"-N {cfg.ifname} {flow_main}") 816 defer(ethtool, f"-N {cfg.ifname} delete 0") 817 818 _ntuple_rule_check(cfg, 1, ctx_id) 819 820 # Verify that traffic matching the specific rule still goes to queues 0/1 821 _send_traffic_check(cfg, port_main, "context 0", 822 { 'target': (0, 1), 823 'empty' : (2, 3) }) 824 825 # And that traffic for any other port is steered to the new context 826 port_other = ports[1] 827 _send_traffic_check(cfg, port_other, f"context {ctx_id}", 828 { 'target': (2, 3), 829 'noise' : (0, 1) }) 830 831 832def _set_flow_hash(cfg, fl_type, fields, context=0): 833 req = {"header": {"dev-index": cfg.ifindex}, 834 "flow-hash": {fl_type: fields}} 835 if context: 836 req["context"] = context 837 cfg.ethnl.rss_set(req) 838 839 840def _get_flow_hash(cfg, fl_type, context=0): 841 req = {"header": {"dev-index": cfg.ifindex}} 842 if context: 843 req["context"] = context 844 rss = cfg.ethnl.rss_get(req) 845 return rss.get("flow-hash", {}).get(fl_type, set()) 846 847 848def test_rss_context_flow_hash(cfg): 849 """ 850 Validate, with traffic, that an additional RSS context honors the 851 flow-hash field selection. If the driver lacks per-context field 852 configuration ("ops->rxfh_per_ctx_fields") fall back to setting the 853 fields on the main context, which the kernel applies device-wide. 854 """ 855 856 require_ntuple(cfg) 857 858 queue_cnt = len(_get_rx_cnts(cfg)) 859 if queue_cnt < 6: 860 try: 861 ksft_pr(f"Increasing queue count {queue_cnt} -> 6") 862 ethtool(f"-L {cfg.ifname} combined 6") 863 defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}") 864 except CmdExitFailure as exc: 865 raise KsftSkipEx("Not enough queues for the test") from exc 866 867 fl_type = f"tcp{cfg.addr_ipver}" 868 if not _get_flow_hash(cfg, fl_type): 869 raise KsftSkipEx(f"Device does not report flow-hash for {fl_type}") 870 871 # Reserve queues 0/1 for main, build a new context spanning 2..5 872 ethtool(f"-X {cfg.ifname} equal 2") 873 defer(ethtool, f"-X {cfg.ifname} default") 874 ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 4") 875 defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete") 876 877 port = rand_port() 878 flow = f"flow-type {fl_type} dst-ip {cfg.addr} dst-port {port} context {ctx_id}" 879 ntuple = ethtool_create(cfg, "-N", flow) 880 defer(ethtool, f"-N {cfg.ifname} delete {ntuple}") 881 882 ip_only = {"ip-src", "ip-dst"} 883 ip_l4 = ip_only | {"l4-b-0-1", "l4-b-2-3"} 884 885 # Try per-context flow-hash; fall back to main context if unsupported. 886 cfg_ctx = ctx_id 887 try: 888 orig = _get_flow_hash(cfg, fl_type, context=ctx_id) 889 _set_flow_hash(cfg, fl_type, ip_only, context=ctx_id) 890 except NlError: 891 ksft_pr("Per-context flow-hash not supported, using device-wide") 892 cfg_ctx = 0 893 orig = _get_flow_hash(cfg, fl_type) 894 _set_flow_hash(cfg, fl_type, ip_only) 895 defer(_set_flow_hash, cfg, fl_type, orig, context=cfg_ctx) 896 897 def measure(): 898 cnts = _get_rx_cnts(cfg) 899 GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000) 900 cnts = _get_rx_cnts(cfg, prev=cnts) 901 ctx_cnts = cnts[2:6] 902 directed = sum(ctx_cnts) 903 used = sum(1 for c in ctx_cnts if c > directed / 200) 904 return cnts, directed, used 905 906 # IP-only hash: iperf3 streams share src/dst IP, all should land on the 907 # same queue inside the context's range. 908 cnts, directed, used = measure() 909 ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP-only): {cnts}") 910 ksft_eq(used, 1, f"IP-only hash should use one queue in context {ctx_id}, got: {cnts}") 911 912 # IP+L4 hash: streams have distinct src ports, traffic should spread. 913 _set_flow_hash(cfg, fl_type, ip_l4, context=cfg_ctx) 914 915 cnts, directed, used = measure() 916 ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP+L4): {cnts}") 917 ksft_ge(used, 2, f"IP+L4 hash should spread across context {ctx_id} queues, got: {cnts}") 918 919 920@ksft_disruptive 921def test_rss_context_persist_ifupdown(cfg, pre_down=False): 922 """ 923 Test that RSS contexts and their associated ntuple filters persist across 924 an interface down/up cycle. 925 926 """ 927 928 require_ntuple(cfg) 929 930 qcnt = len(_get_rx_cnts(cfg)) 931 if qcnt < 6: 932 try: 933 ethtool(f"-L {cfg.ifname} combined 6") 934 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}") 935 except Exception as exc: 936 raise KsftSkipEx("Not enough queues for the test") from exc 937 938 ethtool(f"-X {cfg.ifname} equal 2") 939 defer(ethtool, f"-X {cfg.ifname} default") 940 941 ifup = defer(ip, f"link set dev {cfg.ifname} up") 942 if pre_down: 943 ip(f"link set dev {cfg.ifname} down") 944 945 try: 946 ctx1_id = ethtool_create(cfg, "-X", "context new start 2 equal 2") 947 defer(ethtool, f"-X {cfg.ifname} context {ctx1_id} delete") 948 except CmdExitFailure as exc: 949 raise KsftSkipEx("Create context not supported with interface down") from exc 950 951 ctx2_id = ethtool_create(cfg, "-X", "context new start 4 equal 2") 952 defer(ethtool, f"-X {cfg.ifname} context {ctx2_id} delete") 953 954 port_ctx2 = rand_port() 955 flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_ctx2} context {ctx2_id}" 956 ntuple_id = ethtool_create(cfg, "-N", flow) 957 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}") 958 959 if not pre_down: 960 ip(f"link set dev {cfg.ifname} down") 961 ifup.exec() 962 963 wait_file(f"/sys/class/net/{cfg.ifname}/carrier", 964 lambda x: x.strip() == "1", deadline=20) 965 966 remote_addr = cfg.remote_addr_v[cfg.addr_ipver] 967 for _ in range(10): 968 if cmd(f"ping -c 1 -W 1 {remote_addr}", fail=False).ret == 0: 969 break 970 time.sleep(1) 971 else: 972 raise KsftSkipEx("Cannot reach remote host after interface up") 973 974 ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True) 975 976 data1 = [c for c in ctxs if c.get('context') == ctx1_id] 977 ksft_eq(len(data1), 1, f"Context {ctx1_id} should persist after ifup") 978 979 data2 = [c for c in ctxs if c.get('context') == ctx2_id] 980 ksft_eq(len(data2), 1, f"Context {ctx2_id} should persist after ifup") 981 982 _ntuple_rule_check(cfg, ntuple_id, ctx2_id) 983 984 cnts = _get_rx_cnts(cfg) 985 GenerateTraffic(cfg).wait_pkts_and_stop(20000) 986 cnts = _get_rx_cnts(cfg, prev=cnts) 987 988 main_traffic = sum(cnts[0:2]) 989 ksft_ge(main_traffic, 18000, f"Main context traffic distribution: {cnts}") 990 ksft_lt(sum(cnts[2:6]), 500, f"Other context queues should be mostly empty: {cnts}") 991 992 _send_traffic_check(cfg, port_ctx2, f"context {ctx2_id}", 993 {'target': (4, 5), 994 'noise': (0, 1), 995 'empty': (2, 3)}) 996 997 998def test_rss_context_persist_create_and_ifdown(cfg): 999 """ 1000 Create RSS contexts then cycle the interface down and up. 1001 """ 1002 test_rss_context_persist_ifupdown(cfg, pre_down=False) 1003 1004 1005def test_rss_context_persist_ifdown_and_create(cfg): 1006 """ 1007 Bring interface down first, then create RSS contexts and bring up. 1008 """ 1009 test_rss_context_persist_ifupdown(cfg, pre_down=True) 1010 1011 1012def main() -> None: 1013 with NetDrvEpEnv(__file__, nsim_test=False) as cfg: 1014 cfg.context_cnt = None 1015 cfg.ethnl = EthtoolFamily() 1016 cfg.netdevnl = NetdevFamily() 1017 1018 ksft_run([test_rss_key_indir, test_rss_queue_reconfigure, 1019 test_rss_resize, test_hitless_key_update, 1020 test_rss_context, test_rss_context4, test_rss_context32, 1021 test_rss_context_dump, test_rss_context_queue_reconfigure, 1022 test_rss_context_overlap, test_rss_context_overlap2, 1023 test_rss_context_out_of_order, test_rss_context4_create_with_cfg, 1024 test_flow_add_context_missing, 1025 test_delete_rss_context_busy, test_rss_ntuple_addition, 1026 test_rss_default_context_rule, 1027 test_rss_context_flow_hash, 1028 test_rss_context_persist_create_and_ifdown, 1029 test_rss_context_persist_ifdown_and_create], 1030 args=(cfg, )) 1031 ksft_exit() 1032 1033 1034if __name__ == "__main__": 1035 main() 1036