get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/129057/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 129057,
    "url": "http://patches.dpdk.org/api/patches/129057/?format=api",
    "web_url": "http://patches.dpdk.org/project/dpdk/patch/20230628134748.117697-3-rjarry@redhat.com/",
    "project": {
        "id": 1,
        "url": "http://patches.dpdk.org/api/projects/1/?format=api",
        "name": "DPDK",
        "link_name": "dpdk",
        "list_id": "dev.dpdk.org",
        "list_email": "dev@dpdk.org",
        "web_url": "http://core.dpdk.org",
        "scm_url": "git://dpdk.org/dpdk",
        "webscm_url": "http://git.dpdk.org/dpdk",
        "list_archive_url": "https://inbox.dpdk.org/dev",
        "list_archive_url_format": "https://inbox.dpdk.org/dev/{}",
        "commit_url_format": ""
    },
    "msgid": "<20230628134748.117697-3-rjarry@redhat.com>",
    "list_archive_url": "https://inbox.dpdk.org/dev/20230628134748.117697-3-rjarry@redhat.com",
    "date": "2023-06-28T13:47:50",
    "name": "[v2] usertools: add tool to generate balanced rss traffic flows",
    "commit_ref": null,
    "pull_url": null,
    "state": "accepted",
    "archived": true,
    "hash": "c182ee5071b4dcd245a293bc4c950eaa271f82d1",
    "submitter": {
        "id": 2850,
        "url": "http://patches.dpdk.org/api/people/2850/?format=api",
        "name": "Robin Jarry",
        "email": "rjarry@redhat.com"
    },
    "delegate": {
        "id": 1,
        "url": "http://patches.dpdk.org/api/users/1/?format=api",
        "username": "tmonjalo",
        "first_name": "Thomas",
        "last_name": "Monjalon",
        "email": "thomas@monjalon.net"
    },
    "mbox": "http://patches.dpdk.org/project/dpdk/patch/20230628134748.117697-3-rjarry@redhat.com/mbox/",
    "series": [
        {
            "id": 28691,
            "url": "http://patches.dpdk.org/api/series/28691/?format=api",
            "web_url": "http://patches.dpdk.org/project/dpdk/list/?series=28691",
            "date": "2023-06-28T13:47:50",
            "name": "[v2] usertools: add tool to generate balanced rss traffic flows",
            "version": 2,
            "mbox": "http://patches.dpdk.org/series/28691/mbox/"
        }
    ],
    "comments": "http://patches.dpdk.org/api/patches/129057/comments/",
    "check": "warning",
    "checks": "http://patches.dpdk.org/api/patches/129057/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<dev-bounces@dpdk.org>",
        "X-Original-To": "patchwork@inbox.dpdk.org",
        "Delivered-To": "patchwork@inbox.dpdk.org",
        "Received": [
            "from mails.dpdk.org (mails.dpdk.org [217.70.189.124])\n\tby inbox.dpdk.org (Postfix) with ESMTP id 5E4EA42D80;\n\tWed, 28 Jun 2023 15:48:33 +0200 (CEST)",
            "from mails.dpdk.org (localhost [127.0.0.1])\n\tby mails.dpdk.org (Postfix) with ESMTP id 3272240151;\n\tWed, 28 Jun 2023 15:48:33 +0200 (CEST)",
            "from us-smtp-delivery-124.mimecast.com\n (us-smtp-delivery-124.mimecast.com [170.10.129.124])\n by mails.dpdk.org (Postfix) with ESMTP id C87E5400EF\n for <dev@dpdk.org>; Wed, 28 Jun 2023 15:48:31 +0200 (CEST)",
            "from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com\n [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id\n us-mta-671-aGX07QApM4GGz0TNc2BXsw-1; Wed, 28 Jun 2023 09:48:25 -0400",
            "from smtp.corp.redhat.com (int-mx04.intmail.prod.int.rdu2.redhat.com\n [10.11.54.4])\n (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))\n (No client certificate requested)\n by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 49FCB92E4C7;\n Wed, 28 Jun 2023 13:48:18 +0000 (UTC)",
            "from ringo.home (unknown [10.39.208.42])\n by smtp.corp.redhat.com (Postfix) with ESMTP id 728EC200B680;\n Wed, 28 Jun 2023 13:48:16 +0000 (UTC)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1687960111;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding;\n bh=623G/z/Z6vMGQ/ZwrVtckwoViWAFlppl/dlhlVir9h0=;\n b=errTd8f7HD3yHtzKgcEQx+EflyKrp6nlaWESQtegPtV620tEll/XY60DUnzZ68u1Y++j2O\n +eizgKncpUkjJ9BRJ+7t4jDJdEr3ftZ1fRfNXUkhBY+h4FAE6wwziJn/ik6sEt7MK3cP9K\n 2QkSOuiWfaeIpTLtkM6LM9sE/aXKdms=",
        "X-MC-Unique": "aGX07QApM4GGz0TNc2BXsw-1",
        "From": "Robin Jarry <rjarry@redhat.com>",
        "To": "dev@dpdk.org",
        "Cc": "Robin Jarry <rjarry@redhat.com>, Olivier Matz <olivier.matz@6wind.com>,\n Jean-Mickael Guerin <jean-mickael.guerin@6wind.com>,\n 6WIND <grumly@6wind.com>, Anatoly Burakov <anatoly.burakov@intel.com>",
        "Subject": "[PATCH v2] usertools: add tool to generate balanced rss traffic flows",
        "Date": "Wed, 28 Jun 2023 15:47:50 +0200",
        "Message-ID": "<20230628134748.117697-3-rjarry@redhat.com>",
        "MIME-Version": "1.0",
        "X-Scanned-By": "MIMEDefang 3.1 on 10.11.54.4",
        "X-Mimecast-Spam-Score": "0",
        "X-Mimecast-Originator": "redhat.com",
        "Content-Transfer-Encoding": "8bit",
        "Content-Type": "text/plain; charset=\"US-ASCII\"; x-default=true",
        "X-BeenThere": "dev@dpdk.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "DPDK patches and discussions <dev.dpdk.org>",
        "List-Unsubscribe": "<https://mails.dpdk.org/options/dev>,\n <mailto:dev-request@dpdk.org?subject=unsubscribe>",
        "List-Archive": "<http://mails.dpdk.org/archives/dev/>",
        "List-Post": "<mailto:dev@dpdk.org>",
        "List-Help": "<mailto:dev-request@dpdk.org?subject=help>",
        "List-Subscribe": "<https://mails.dpdk.org/listinfo/dev>,\n <mailto:dev-request@dpdk.org?subject=subscribe>",
        "Errors-To": "dev-bounces@dpdk.org"
    },
    "content": "usage: dpdk-rss-flows.py [-h] [-s SPORT_RANGE] [-d DPORT_RANGE] [-r]\n                         [-k RSS_KEY] [-t RETA_SIZE] [-a] [-j]\n                         RX_QUEUES SRC DST\n\nCraft IP{v6}/{TCP/UDP} traffic flows that will evenly spread over a\ngiven number of RX queues according to the RSS algorithm.\n\npositional arguments:\n  RX_QUEUES             The number of RX queues to fill.\n  SRC                   The source IP network/address.\n  DST                   The destination IP network/address.\n\noptions:\n  -h, --help            show this help message and exit\n  -s SPORT_RANGE, --sport-range SPORT_RANGE\n                        The layer 4 (TCP/UDP) source port range. Can\n                        be a single fixed value or a range\n                        <start>-<end>.\n  -d DPORT_RANGE, --dport-range DPORT_RANGE\n                        The layer 4 (TCP/UDP) destination port range.\n                        Can be a single fixed value or a range\n                        <start>-<end>.\n  -r, --check-reverse-traffic\n                        The reversed traffic (source <-> dest) should\n                        also be evenly balanced in the queues.\n  -k RSS_KEY, --rss-key RSS_KEY\n                        The random 40-bytes key used to compute the\n                        RSS hash. This option supports either a well-\n                        known name or the hex value of the key (well-\n                        known names: \"intel\", \"mlx\", default:\n                        \"intel\").\n  -t RETA_SIZE, --reta-size RETA_SIZE\n                        Size of the redirection table or \"RETA\"\n                        (default: 128).\n  -a, --all-flows       Output ALL flows that can be created based on\n                        source and destination address/port ranges\n                        along their matched queue number. ATTENTION:\n                        this option can produce very long outputs\n                        depending on the address and port range sizes.\n  -j, --json            Output in parseable JSON format.\n\nExamples:\n\n  ~$ dpdk-rss-flows.py 8 28.0.0.0/24 40.0.0.0/24\n  SRC_IP      DST_IP       QUEUE\n  28.0.0.1    40.0.0.1     5\n  28.0.0.1    40.0.0.2     4\n  28.0.0.1    40.0.0.3     2\n  28.0.0.1    40.0.0.6     3\n  28.0.0.1    40.0.0.8     0\n  28.0.0.1    40.0.0.9     6\n  28.0.0.1    40.0.0.10    7\n  28.0.0.1    40.0.0.11    1\n\n  ~$ dpdk-rss-flows.py 8 28.0.0.0/24 40.0.0.0/24 -r\n  SRC_IP      DST_IP       QUEUE    QUEUE_REVERSE\n  28.0.0.1    40.0.0.1     5        3\n  28.0.0.1    40.0.0.2     4        2\n  28.0.0.1    40.0.0.8     0        6\n  28.0.0.1    40.0.0.9     6        7\n  28.0.0.1    40.0.0.16    2        4\n  28.0.0.1    40.0.0.19    3        5\n  28.0.0.1    40.0.0.24    1        0\n  28.0.0.1    40.0.0.25    7        1\n\n  ~$ dpdk-rss-flows.py 8 28.0.0.0/24 40.0.0.0/24 -s 32000-64000 -d 53\n  SRC_IP      SPORT    DST_IP      DPORT    QUEUE\n  28.0.0.1    32000    40.0.0.1    53       0\n  28.0.0.1    32001    40.0.0.1    53       1\n  28.0.0.1    32004    40.0.0.1    53       4\n  28.0.0.1    32005    40.0.0.1    53       5\n  28.0.0.1    32008    40.0.0.1    53       2\n  28.0.0.1    32009    40.0.0.1    53       3\n  28.0.0.1    32012    40.0.0.1    53       6\n  28.0.0.1    32013    40.0.0.1    53       7\n\n  ~$ dpdk-rss-flows.py 4 2a01:cb00:f8b:9700::/64 2620:52:0:2592::/64 -rj\n  [\n    {\n      \"queue\": 0,\n      \"queue_reverse\": 3,\n      \"src_ip\": \"2a01:cb00:f8b:9700::1\",\n      \"dst_ip\": \"2620:52:0:2592::1\",\n      \"src_port\": 0,\n      \"dst_port\": 0\n    },\n    {\n      \"queue\": 3,\n      \"queue_reverse\": 0,\n      \"src_ip\": \"2a01:cb00:f8b:9700::1\",\n      \"dst_ip\": \"2620:52:0:2592::2\",\n      \"src_port\": 0,\n      \"dst_port\": 0\n    },\n    {\n      \"queue\": 2,\n      \"queue_reverse\": 1,\n      \"src_ip\": \"2a01:cb00:f8b:9700::1\",\n      \"dst_ip\": \"2620:52:0:2592::3\",\n      \"src_port\": 0,\n      \"dst_port\": 0\n    },\n    {\n      \"queue\": 1,\n      \"queue_reverse\": 2,\n      \"src_ip\": \"2a01:cb00:f8b:9700::1\",\n      \"dst_ip\": \"2620:52:0:2592::1a\",\n      \"src_port\": 0,\n      \"dst_port\": 0\n    }\n  ]\n\nCc: Olivier Matz <olivier.matz@6wind.com>\nCc: Jean-Mickael Guerin <jean-mickael.guerin@6wind.com>\nSigned-off-by: Robin Jarry <rjarry@redhat.com>\nSigned-off-by: 6WIND <grumly@6wind.com>\nAcked-by: Anatoly Burakov <anatoly.burakov@intel.com>\n---\n\nNotes:\n    v2:\n    \n    * fixed author\n    * added i40e default key\n    * allow 52 bytes keys (for i40e, iavf and ice drivers)\n    * added -a/--all-flows option\n\n usertools/dpdk-rss-flows.py | 418 ++++++++++++++++++++++++++++++++++++\n usertools/meson.build       |   1 +\n 2 files changed, 419 insertions(+)\n create mode 100755 usertools/dpdk-rss-flows.py",
    "diff": "diff --git a/usertools/dpdk-rss-flows.py b/usertools/dpdk-rss-flows.py\nnew file mode 100755\nindex 000000000000..4cdc524ddcb4\n--- /dev/null\n+++ b/usertools/dpdk-rss-flows.py\n@@ -0,0 +1,418 @@\n+#!/usr/bin/env python3\n+# SPDX-License-Identifier: BSD-3-Clause\n+# Copyright (c) 2014 6WIND S.A.\n+# Copyright (c) 2023 Robin Jarry\n+\n+\"\"\"\n+Craft IP{v6}/{TCP/UDP} traffic flows that will evenly spread over a given\n+number of RX queues according to the RSS algorithm.\n+\"\"\"\n+\n+import argparse\n+import binascii\n+import ctypes\n+import ipaddress\n+import json\n+import struct\n+import typing\n+\n+\n+Address = typing.Union[ipaddress.IPv4Address, ipaddress.IPv6Address]\n+Network = typing.Union[ipaddress.IPv4Network, ipaddress.IPv6Network]\n+PortList = typing.Iterable[int]\n+\n+\n+class Packet:\n+    def __init__(self, ip_src: Address, ip_dst: Address, l4_sport: int, l4_dport: int):\n+        self.ip_src = ip_src\n+        self.ip_dst = ip_dst\n+        self.l4_sport = l4_sport\n+        self.l4_dport = l4_dport\n+\n+    def reverse(self):\n+        return Packet(\n+            ip_src=self.ip_dst,\n+            l4_sport=self.l4_dport,\n+            ip_dst=self.ip_src,\n+            l4_dport=self.l4_sport,\n+        )\n+\n+    def hash_data(self, use_l4_port: bool = False) -> bytes:\n+        data = self.ip_src.packed + self.ip_dst.packed\n+        if use_l4_port:\n+            data += struct.pack(\">H\", self.l4_sport)\n+            data += struct.pack(\">H\", self.l4_dport)\n+        return data\n+\n+\n+class TrafficTemplate:\n+    def __init__(\n+        self,\n+        ip_src: Network,\n+        ip_dst: Network,\n+        l4_sport_range: PortList,\n+        l4_dport_range: PortList,\n+    ):\n+        self.ip_src = ip_src\n+        self.ip_dst = ip_dst\n+        self.l4_sport_range = l4_sport_range\n+        self.l4_dport_range = l4_dport_range\n+\n+    def __iter__(self) -> typing.Iterator[Packet]:\n+        for ip_src in self.ip_src.hosts():\n+            for ip_dst in self.ip_dst.hosts():\n+                if ip_src == ip_dst:\n+                    continue\n+                for sport in self.l4_sport_range:\n+                    for dport in self.l4_dport_range:\n+                        yield Packet(ip_src, ip_dst, sport, dport)\n+\n+\n+class RSSAlgo:\n+    def __init__(\n+        self,\n+        queues_count: int,\n+        key: bytes,\n+        reta_size: int,\n+        use_l4_port: bool,\n+    ):\n+        self.queues_count = queues_count\n+        self.reta = tuple(i % queues_count for i in range(reta_size))\n+        self.key = key\n+        self.use_l4_port = use_l4_port\n+\n+    def toeplitz_hash(self, data: bytes) -> int:\n+        # see rte_softrss_* in lib/hash/rte_thash.h\n+        hash_value = ctypes.c_uint32(0)\n+\n+        for i, byte in enumerate(data):\n+            for j in range(8):\n+                bit = (byte >> (7 - j)) & 0x01\n+\n+                if bit == 1:\n+                    keyword = ctypes.c_uint32(0)\n+                    keyword.value |= self.key[i] << 24\n+                    keyword.value |= self.key[i + 1] << 16\n+                    keyword.value |= self.key[i + 2] << 8\n+                    keyword.value |= self.key[i + 3]\n+\n+                    if j > 0:\n+                        keyword.value <<= j\n+                        keyword.value |= self.key[i + 4] >> (8 - j)\n+\n+                    hash_value.value ^= keyword.value\n+\n+        return hash_value.value\n+\n+    def get_queue_index(self, packet: Packet) -> int:\n+        bytes_to_hash = packet.hash_data(self.use_l4_port)\n+\n+        # get the 32bit hash of the packet\n+        hash_value = self.toeplitz_hash(bytes_to_hash)\n+\n+        # determine the offset in the redirection table\n+        offset = hash_value & (len(self.reta) - 1)\n+\n+        return self.reta[offset]\n+\n+\n+def balanced_traffic(\n+    algo: RSSAlgo,\n+    traffic_template: TrafficTemplate,\n+    check_reverse_traffic: bool = False,\n+    all_flows: bool = False,\n+) -> typing.Iterator[typing.Tuple[int, int, Packet]]:\n+    queues = set()\n+    if check_reverse_traffic:\n+        queues_reverse = set()\n+\n+    for pkt in traffic_template:\n+        q = algo.get_queue_index(pkt)\n+\n+        # check if q is already filled\n+        if not all_flows and q in queues:\n+            continue\n+\n+        qr = algo.get_queue_index(pkt.reverse())\n+\n+        if check_reverse_traffic:\n+            # check if q is already filled\n+            if not all_flows and qr in queues_reverse:\n+                continue\n+            # mark this queue as matched\n+            queues_reverse.add(qr)\n+\n+        # mark this queue as filled\n+        queues.add(q)\n+\n+        yield q, qr, pkt\n+\n+        # stop when all queues have been filled\n+        if not all_flows and len(queues) == algo.queues_count:\n+            break\n+\n+\n+NO_PORT = (0,)\n+\n+# fmt: off\n+# rss_intel_key, see drivers/net/ixgbe/ixgbe_rxtx.c\n+RSS_KEY_INTEL = bytes(\n+    (\n+        0x6d, 0x5a, 0x56, 0xda, 0x25, 0x5b, 0x0e, 0xc2,\n+        0x41, 0x67, 0x25, 0x3d, 0x43, 0xa3, 0x8f, 0xb0,\n+        0xd0, 0xca, 0x2b, 0xcb, 0xae, 0x7b, 0x30, 0xb4,\n+        0x77, 0xcb, 0x2d, 0xa3, 0x80, 0x30, 0xf2, 0x0c,\n+        0x6a, 0x42, 0xb7, 0x3b, 0xbe, 0xac, 0x01, 0xfa,\n+    )\n+)\n+# rss_hash_default_key, see drivers/net/mlx5/mlx5_rxq.c\n+RSS_KEY_MLX = bytes(\n+    (\n+        0x2c, 0xc6, 0x81, 0xd1, 0x5b, 0xdb, 0xf4, 0xf7,\n+        0xfc, 0xa2, 0x83, 0x19, 0xdb, 0x1a, 0x3e, 0x94,\n+        0x6b, 0x9e, 0x38, 0xd9, 0x2c, 0x9c, 0x03, 0xd1,\n+        0xad, 0x99, 0x44, 0xa7, 0xd9, 0x56, 0x3d, 0x59,\n+        0x06, 0x3c, 0x25, 0xf3, 0xfc, 0x1f, 0xdc, 0x2a,\n+    )\n+)\n+# rss_key_default, see drivers/net/i40e/i40e_ethdev.c\n+# i40e is the only driver that takes 52 bytes keys\n+RSS_KEY_I40E = bytes(\n+    (\n+        0x6b, 0x79, 0x39, 0x44, 0x23, 0x50, 0x4c, 0xb5,\n+        0x5b, 0xea, 0x75, 0xb6, 0x30, 0x9f, 0x4f, 0x12,\n+        0x3d, 0xc0, 0xa2, 0xb8, 0x02, 0x4d, 0xdc, 0xdf,\n+        0x33, 0x9b, 0x8c, 0xa0, 0x4c, 0x4a, 0xf6, 0x4a,\n+        0x34, 0xfa, 0xc6, 0x05, 0x55, 0xd8, 0x58, 0x39,\n+        0x3a, 0x58, 0x99, 0x7d, 0x2e, 0xc9, 0x38, 0xe1,\n+        0x66, 0x03, 0x15, 0x81,\n+    )\n+)\n+# fmt: on\n+DEFAULT_DRIVER_KEYS = {\n+    \"intel\": RSS_KEY_INTEL,\n+    \"mlx\": RSS_KEY_MLX,\n+    \"i40e\": RSS_KEY_I40E,\n+}\n+\n+\n+def rss_key(value):\n+    if value in DEFAULT_DRIVER_KEYS:\n+        return DEFAULT_DRIVER_KEYS[value]\n+    try:\n+        key = binascii.unhexlify(value)\n+        if len(key) not in (40, 52):\n+            raise argparse.ArgumentTypeError(\"The key must be 40 or 52 bytes long\")\n+        return key\n+    except (TypeError, ValueError) as e:\n+        raise argparse.ArgumentTypeError(str(e)) from e\n+\n+\n+def port_range(value):\n+    try:\n+        if \"-\" in value:\n+            start, stop = value.split(\"-\")\n+            res = tuple(range(int(start), int(stop)))\n+        else:\n+            res = (int(value),)\n+        return res or NO_PORT\n+    except ValueError as e:\n+        raise argparse.ArgumentTypeError(str(e)) from e\n+\n+\n+def positive_int(value):\n+    try:\n+        i = int(value)\n+        if i <= 0:\n+            raise argparse.ArgumentTypeError(\"must be strictly positive\")\n+        return i\n+    except ValueError as e:\n+        raise argparse.ArgumentTypeError(str(e)) from e\n+\n+\n+def power_of_two(value):\n+    i = positive_int(value)\n+    if i & (i - 1) != 0:\n+        raise argparse.ArgumentTypeError(\"must be a power of two\")\n+    return i\n+\n+\n+def parse_args():\n+    parser = argparse.ArgumentParser(description=__doc__)\n+\n+    parser.add_argument(\n+        \"rx_queues\",\n+        metavar=\"RX_QUEUES\",\n+        type=positive_int,\n+        help=\"\"\"\n+        The number of RX queues to fill.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"ip_src\",\n+        metavar=\"SRC\",\n+        type=ipaddress.ip_network,\n+        help=\"\"\"\n+        The source IP network/address.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"ip_dst\",\n+        metavar=\"DST\",\n+        type=ipaddress.ip_network,\n+        help=\"\"\"\n+        The destination IP network/address.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-s\",\n+        \"--sport-range\",\n+        type=port_range,\n+        default=NO_PORT,\n+        help=\"\"\"\n+        The layer 4 (TCP/UDP) source port range.\n+        Can be a single fixed value or a range <start>-<end>.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-d\",\n+        \"--dport-range\",\n+        type=port_range,\n+        default=NO_PORT,\n+        help=\"\"\"\n+        The layer 4 (TCP/UDP) destination port range.\n+        Can be a single fixed value or a range <start>-<end>.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-r\",\n+        \"--check-reverse-traffic\",\n+        action=\"store_true\",\n+        help=\"\"\"\n+        The reversed traffic (source <-> dest) should also be evenly balanced\n+        in the queues.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-k\",\n+        \"--rss-key\",\n+        default=RSS_KEY_INTEL,\n+        type=rss_key,\n+        help=\"\"\"\n+        The random 40-bytes key used to compute the RSS hash. This option\n+        supports either a well-known name or the hex value of the key\n+        (well-known names: \"intel\", \"mlx\", default: \"intel\").\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-t\",\n+        \"--reta-size\",\n+        default=128,\n+        type=power_of_two,\n+        help=\"\"\"\n+        Size of the redirection table or \"RETA\" (default: 128).\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-a\",\n+        \"--all-flows\",\n+        action=\"store_true\",\n+        help=\"\"\"\n+        Output ALL flows that can be created based on source and destination\n+        address/port ranges along their matched queue number. ATTENTION: this\n+        option can produce very long outputs depending on the address and port\n+        range sizes.\n+        \"\"\",\n+    )\n+    parser.add_argument(\n+        \"-j\",\n+        \"--json\",\n+        action=\"store_true\",\n+        help=\"\"\"\n+        Output in parseable JSON format.\n+        \"\"\",\n+    )\n+\n+    args = parser.parse_args()\n+\n+    if args.ip_src.version != args.ip_dst.version:\n+        parser.error(\n+            f\"{args.ip_src} and {args.ip_dst} don't have the same protocol version\"\n+        )\n+    if args.reta_size < args.rx_queues:\n+        parser.error(\"RETA_SIZE must be greater than or equal to RX_QUEUES\")\n+\n+    return args\n+\n+\n+def main():\n+    args = parse_args()\n+    use_l4_port = args.sport_range != NO_PORT or args.dport_range != NO_PORT\n+\n+    algo = RSSAlgo(\n+        queues_count=args.rx_queues,\n+        key=args.rss_key,\n+        reta_size=args.reta_size,\n+        use_l4_port=use_l4_port,\n+    )\n+    template = TrafficTemplate(\n+        args.ip_src,\n+        args.ip_dst,\n+        args.sport_range,\n+        args.dport_range,\n+    )\n+\n+    results = balanced_traffic(\n+        algo, template, args.check_reverse_traffic, args.all_flows\n+    )\n+\n+    if args.json:\n+        flows = []\n+        for q, qr, pkt in results:\n+            flows.append(\n+                {\n+                    \"queue\": q,\n+                    \"queue_reverse\": qr,\n+                    \"src_ip\": str(pkt.ip_src),\n+                    \"dst_ip\": str(pkt.ip_dst),\n+                    \"src_port\": pkt.l4_sport,\n+                    \"dst_port\": pkt.l4_dport,\n+                }\n+            )\n+        print(json.dumps(flows, indent=2))\n+        return\n+\n+    if use_l4_port:\n+        header = [\"SRC_IP\", \"SPORT\", \"DST_IP\", \"DPORT\", \"QUEUE\"]\n+    else:\n+        header = [\"SRC_IP\", \"DST_IP\", \"QUEUE\"]\n+    if args.check_reverse_traffic:\n+        header.append(\"QUEUE_REVERSE\")\n+\n+    rows = [tuple(header)]\n+    widths = [len(h) for h in header]\n+\n+    for q, qr, pkt in results:\n+        if use_l4_port:\n+            row = [pkt.ip_src, pkt.l4_sport, pkt.ip_dst, pkt.l4_dport, q]\n+        else:\n+            row = [pkt.ip_src, pkt.ip_dst, q]\n+        if args.check_reverse_traffic:\n+            row.append(qr)\n+        cells = []\n+        for i, r in enumerate(row):\n+            r = str(r)\n+            if len(r) > widths[i]:\n+                widths[i] = len(r)\n+            cells.append(r)\n+        rows.append(tuple(cells))\n+\n+    fmt = [f\"%-{w}s\" for w in widths]\n+    fmt[-1] = \"%s\"  # avoid trailing whitespace\n+    fmt = \"    \".join(fmt)\n+    for row in rows:\n+        print(fmt % row)\n+\n+\n+if __name__ == \"__main__\":\n+    main()\ndiff --git a/usertools/meson.build b/usertools/meson.build\nindex b6271a207cce..0efa4a86d97c 100644\n--- a/usertools/meson.build\n+++ b/usertools/meson.build\n@@ -6,5 +6,6 @@ install_data([\n             'dpdk-pmdinfo.py',\n             'dpdk-telemetry.py',\n             'dpdk-hugepages.py',\n+            'dpdk-rss-flows.py',\n         ],\n         install_dir: 'bin')\n",
    "prefixes": [
        "v2"
    ]
}