* [PATCH] net/iavf: reject oversized frames in prep callback
From: Ciara Loftus @ 2026-06-09 14:03 UTC (permalink / raw)
To: dev; +Cc: Ciara Loftus, stable
Currently, only the segment count is checked for non-TSO packets. There
is no upper bound placed on the packet length, so packets larger than
`IAVF_FRAME_SIZE_MAX` pass the prepare callback unchallenged and are
submitted to the hardware. Sending such frames can trigger Malicious
Driver Detection (MDD) on the PF. Fix this by adding a packet length
size check on the non-TSO path.
Fixes: 19ee91c6bd9a ("net/iavf: check illegal packet sizes")
Cc: stable@dpdk.org
Signed-off-by: Ciara Loftus <ciara.loftus@intel.com>
---
drivers/net/intel/iavf/iavf_rxtx.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/net/intel/iavf/iavf_rxtx.c b/drivers/net/intel/iavf/iavf_rxtx.c
index decbc75142..a57af7faed 100644
--- a/drivers/net/intel/iavf/iavf_rxtx.c
+++ b/drivers/net/intel/iavf/iavf_rxtx.c
@@ -3227,9 +3227,9 @@ iavf_prep_pkts(__rte_unused void *tx_queue, struct rte_mbuf **tx_pkts,
m = tx_pkts[i];
ol_flags = m->ol_flags;
- /* Check condition for nb_segs > IAVF_TX_MAX_MTU_SEG. */
+ /* Validate segment count and packet length. */
if (!(ol_flags & (RTE_MBUF_F_TX_TCP_SEG | RTE_MBUF_F_TX_UDP_SEG))) {
- if (m->nb_segs > IAVF_TX_MAX_MTU_SEG) {
+ if (m->nb_segs > IAVF_TX_MAX_MTU_SEG || m->pkt_len > IAVF_FRAME_SIZE_MAX) {
rte_errno = EINVAL;
return i;
}
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v2] net/iavf: fix misleading AQ failure logging
From: Bruce Richardson @ 2026-06-09 14:05 UTC (permalink / raw)
To: Loftus, Ciara
Cc: Mandal, Anurag, dev@dpdk.org, Medvedkin, Vladimir,
stable@dpdk.org
In-Reply-To: <IA4PR11MB92781E302F372E466C89501C8E1D2@IA4PR11MB9278.namprd11.prod.outlook.com>
On Tue, Jun 09, 2026 at 01:30:47PM +0100, Loftus, Ciara wrote:
> > Subject: [PATCH v2] net/iavf: fix misleading AQ failure logging
> >
> > iavf_handle_virtchnl_msg() drains the admin receive queue in a loop
> > until iavf_clean_arq_element() reports that no descriptors are
> > pending. When the queue becomes empty, the base driver returns
> > IAVF_ERR_ADMIN_QUEUE_NO_WORK (-57), which is the documented
> > terminator for the drain loop, and is not an error.
> >
> > The current loop treats every non-IAVF_SUCCESS return as a failure
> > and logs it as follows:
> >
> > "Failed to read msg from AdminQ, ret: -57"
> >
> > This message floods the logs on every interrupt cycle and misleads
> > the triage during VF reset by chasing a real virtchnl problem
> > seeing these spurious -57 AQ failure lines in logs and assumes
> > the admin queue is broken, when in fact it has just been drained.
> >
> > This patch fixes the aforesaid issue by treating
> > IAVF_ERR_ADMIN_QUEUE_NO_WORK in virtchnl message drain as a normal
> > loop exit empty-queue condition and avoid logging it as an misleading
> > AQ failure.
> >
> > Fixes: 02d212ca3125 ("net/iavf: rename remaining avf strings")
Actually, I think the proper offending commit is earlier:
Fixes: 22b123a36d07 ("net/avf: initialize PMD")
> > Cc: stable@dpdk.org
> >
> > Signed-off-by: Anurag Mandal <anurag.mandal@intel.com>
>
> Thanks Anurag.
>
> Acked-by: Ciara Loftus <ciara.loftus@intel.com>
>
Patch applied to dpdk-next-net-intel.
Thanks,
/Bruce
^ permalink raw reply
* [PATCH v2] eal: add destructor to unregister tailq on unload
From: Stephen Hemminger @ 2026-06-09 14:26 UTC (permalink / raw)
To: dev
Cc: Stephen Hemminger, stable, Bruce Richardson, Neil Horman,
David Marchand
In-Reply-To: <20260607150418.30885-1-stephen@networkplumber.org>
EAL_REGISTER_TAILQ registers a static rte_tailq_elem from a
constructor but provides no destructor. If a library using the
macro is loaded with dlopen() and later unloaded with dlclose(),
the process-local list keeps a dangling pointer to the unmapped
elem, and the next dlopen() crashes in rte_eal_tailq_local_register()
while walking the list.
Add a new RTE_FINI destructor that is paired with the constructor
in the macro. rte_eal_tailq_unregister() drops the local entry on
unload. The shared mcfg->tailq_head[] slot is left reserved since
it is keyed by name and shared between processes;
rte_eal_tailq_update() now reattaches to that slot on re-register
instead of failing.
Bugzilla ID: 1081
Fixes: 873a61c7526b ("tailq: introduce dynamic register system")
Cc: stable@dpdk.org
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
Acked-by: Bruce Richardson <bruce.richardson@intel.com>
---
v2 - cover the case where name still is reserved
lib/eal/common/eal_common_tailqs.c | 13 +++++++++++++
lib/eal/include/rte_tailq.h | 16 ++++++++++++++++
2 files changed, 29 insertions(+)
diff --git a/lib/eal/common/eal_common_tailqs.c b/lib/eal/common/eal_common_tailqs.c
index c581f43b6f..9355c108f2 100644
--- a/lib/eal/common/eal_common_tailqs.c
+++ b/lib/eal/common/eal_common_tailqs.c
@@ -113,6 +113,11 @@ rte_eal_tailq_update(struct rte_tailq_elem *t)
if (rte_eal_process_type() == RTE_PROC_PRIMARY) {
/* primary process is the only one that creates */
t->head = rte_eal_tailq_create(t->name);
+
+ if (t->head == NULL) {
+ /* slot reserved by an earlier load -- reuse it */
+ t->head = rte_eal_tailq_lookup(t->name);
+ }
} else {
t->head = rte_eal_tailq_lookup(t->name);
}
@@ -148,6 +153,14 @@ rte_eal_tailq_register(struct rte_tailq_elem *t)
return -1;
}
+RTE_EXPORT_SYMBOL(rte_eal_tailq_unregister)
+void
+rte_eal_tailq_unregister(struct rte_tailq_elem *t)
+{
+ TAILQ_REMOVE(&rte_tailq_elem_head, t, next);
+ t->head = NULL;
+}
+
int
rte_eal_tailqs_init(void)
{
diff --git a/lib/eal/include/rte_tailq.h b/lib/eal/include/rte_tailq.h
index e7caed6812..d4d8bfd6d4 100644
--- a/lib/eal/include/rte_tailq.h
+++ b/lib/eal/include/rte_tailq.h
@@ -117,11 +117,27 @@ struct rte_tailq_head *rte_eal_tailq_lookup(const char *name);
*/
int rte_eal_tailq_register(struct rte_tailq_elem *t);
+/**
+ * Remove a tail queue element from the local list.
+ * This function is mainly used for EAL_REGISTER_TAILQ macro which pairs
+ * an RTE_FINI destructor with the existing RTE_INIT constructor.
+ * The destructor calls this function during dlclose() to prevent
+ * dangling pointers to unmapped library data.
+ *
+ * @param t
+ * The tailq element to remove from the EAL tailq list.
+ */
+void rte_eal_tailq_unregister(struct rte_tailq_elem *t);
+
#define EAL_REGISTER_TAILQ(t) \
RTE_INIT(tailqinitfn_ ##t) \
{ \
if (rte_eal_tailq_register(&t) < 0) \
rte_panic("Cannot initialize tailq: %s\n", t.name); \
+} \
+RTE_FINI(tailqfinifn_ ##t) \
+{ \
+ rte_eal_tailq_unregister(&t); \
}
/* This macro permits both remove and free var within the loop safely.*/
--
2.53.0
^ permalink raw reply related
* Fwd: [PATCH v1 2/2] dts: add build arguments to test run configuration
From: Koushik Bhargav Nimoji @ 2026-06-09 14:27 UTC (permalink / raw)
To: dev
In-Reply-To: <20260608132303.1099012-2-knimoji@iol.unh.edu>
[-- Attachment #1: Type: text/plain, Size: 6433 bytes --]
---------- Forwarded message ---------
From: Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
Date: Mon, Jun 8, 2026 at 9:23 AM
Subject: [PATCH v1 2/2] dts: add build arguments to test run configuration
To: <luca.vizzarro@arm.com>, <patrickrobb1997@gmail.com>
Cc: <abailey@iol.unh.edu>, <ahassick@iol.unh.edu>, <lylavoie@iol.unh.edu>,
Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
This patch adds the ability to specify build arguments when building DPDK
through DTS. Doing so allows users to build DPDK with the desired build
arguments, which allows for a more configurable DTS run.
Signed-off-by: Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
---
dts/configurations/test_run.example.yaml | 13 +++++++++++++
dts/framework/config/test_run.py | 2 ++
dts/framework/remote_session/dpdk.py | 12 ++++++++----
dts/framework/utils.py | 21 ++++++++++++++++++++-
4 files changed, 43 insertions(+), 5 deletions(-)
diff --git a/dts/configurations/test_run.example.yaml
b/dts/configurations/test_run.example.yaml
index ee641f5dce..0bd5151801 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -16,6 +16,8 @@
# `precompiled_build_dir` or `build_options` can be defined, but not
both.
# `compiler_wrapper`:
# Optional, adds a compiler wrapper if present.
+# `build_args`:
+# The additional build arguments to be used when building DPDK.
# `func_traffic_generator` & `perf_traffic_generator`:
# Define `func_traffic_generator` when `func` set to true.
# Define `perf_traffic_generator` when `perf` set to true.
@@ -40,6 +42,17 @@ dpdk:
# the combination of the following two makes CC="ccache gcc"
compiler: gcc
compiler_wrapper: ccache # see `Optional Fields`
+ # arguments to be used when building DPDK
+ # build_args:
+ # c_args:
+ # - O3
+ # - g
+ # b_coverage:
+ # - "true"
+ # buildtype:
+ # - release
+ # flags:
+ # - strip
func_traffic_generator:
type: SCAPY
# perf_traffic_generator:
diff --git a/dts/framework/config/test_run.py
b/dts/framework/config/test_run.py
index 76e24d1785..eab12041fc 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -191,6 +191,8 @@ class DPDKBuildOptionsConfiguration(FrozenModel):
#: This string will be put in front of the compiler when executing the
build. Useful for adding
#: wrapper commands, such as ``ccache``.
compiler_wrapper: str = ""
+ #: The build arguments to build dpdk with
+ build_args: dict[str, list[str]] = {}
class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration):
diff --git a/dts/framework/remote_session/dpdk.py
b/dts/framework/remote_session/dpdk.py
index 865f97f6ca..4dc0ceeaaf 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -100,8 +100,8 @@ def setup(self) -> None:
match self.config:
case
DPDKPrecompiledBuildConfiguration(precompiled_build_dir=build_dir):
self._set_remote_dpdk_build_dir(build_dir)
- case
DPDKUncompiledBuildConfiguration(build_options=build_options):
- self._configure_dpdk_build(build_options)
+ case DPDKUncompiledBuildConfiguration():
+ self._configure_dpdk_build(self.config.build_options)
self._build_dpdk()
def teardown(self) -> None:
@@ -277,16 +277,20 @@ def _build_dpdk(self) -> None:
`remote_dpdk_tree_path` has already been set on the SUT node.
"""
ctx = get_ctx()
+ build_options = getattr(self.config, "build_options")
# If the SUT is an ice driver device, make sure to build with 16B
descriptors.
if (
ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
- default_library="static", libdir="lib",
c_args="-DRTE_NET_INTEL_USE_16BYTE_DESC"
+ build_options.build_args,
+ default_library="static",
+ libdir="lib",
+ c_args="-DRTE_NET_INTEL_USE_16BYTE_DESC",
)
else:
- meson_args = MesonArgs(default_library="static", libdir="lib")
+ meson_args = MesonArgs(build_options.build_args,
default_library="static", libdir="lib")
if SETTINGS.code_coverage:
meson_args._add_arg("-Db_coverage=true")
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 38da88cd9c..e0ed35066c 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -99,10 +99,16 @@ class MesonArgs:
_default_library: str
- def __init__(self, default_library: str | None = None, **dpdk_args:
str | bool):
+ def __init__(
+ self,
+ dpdk_build_args: dict[str, list[str]],
+ default_library: str | None = None,
+ **dpdk_args: str | bool,
+ ):
"""Initialize the meson arguments.
Args:
+ dpdk_build_args: The DPDK build arguments specified in the
test run configuration file.
default_library: The default library type, Meson supports
``shared``, ``static`` and
``both``. Defaults to :data:`None`, in which case the
argument won't be used.
dpdk_args: The arguments found in ``meson_options.txt`` in
root DPDK directory.
@@ -121,6 +127,19 @@ def __init__(self, default_library: str | None = None,
**dpdk_args: str | bool):
)
)
+ arguments = []
+ for option, value in dpdk_build_args.items():
+ if option == "c_args":
+ values = " ".join(f"-{val}" for val in value)
+ arguments.append(f'-D{option}="{values}"')
+ elif option == "flags":
+ values = " ".join(f"--{val}" for val in value)
+ arguments.append(values)
+ else:
+ arguments.append(f" -D{option}={value[0]}")
+
+ self._dpdk_args = " ".join(arguments)
+
def __str__(self) -> str:
"""The actual args."""
return " ".join(f"{self._default_library}
{self._dpdk_args}".split())
--
2.54.0
[-- Attachment #2: Type: text/html, Size: 8448 bytes --]
^ permalink raw reply related
* Re: [PATCH 1/3] net/iavf: downgrade opcode 0 ARQ log to debug
From: Bruce Richardson @ 2026-06-09 14:28 UTC (permalink / raw)
To: Ciara Loftus; +Cc: dev, Talluri Chaitanyababu
In-Reply-To: <20260608145518.1705524-2-ciara.loftus@intel.com>
On Mon, Jun 08, 2026 at 02:55:16PM +0000, Ciara Loftus wrote:
> From: Talluri Chaitanyababu <chaitanyababux.talluri@intel.com>
>
> After admin queue reinitialisation, completions from uninitialised
> ARQ ring descriptor memory may arrive before any real PF response.
> These carry opcode 0 (`VIRTCHNL_OP_UNKNOWN`) and trigger a WARNING
> log on every poll iteration, flooding the log during reset recovery.
>
> Treat opcode 0 as a distinct case and log it at DEBUG level, while
> retaining WARNING for genuine opcode mismatches.
>
> Signed-off-by: Talluri Chaitanyababu <chaitanyababux.talluri@intel.com>
> ---
> drivers/net/intel/iavf/iavf_vchnl.c | 11 +++++++++--
> 1 file changed, 9 insertions(+), 2 deletions(-)
>
Should this be backported as a bugfix?
^ permalink raw reply
* Re: [PATCH v4 5/5] eal: avoid deadlock in async IPC alarm callback
From: Stephen Hemminger @ 2026-06-09 14:32 UTC (permalink / raw)
To: Burakov, Anatoly; +Cc: dev, Jianfeng Tan
In-Reply-To: <9f0b651c-13dd-44a1-bf72-860202f8cd99@intel.com>
On Tue, 9 Jun 2026 10:04:22 +0200
"Burakov, Anatoly" <anatoly.burakov@intel.com> wrote:
> On 6/5/2026 4:29 PM, Anatoly Burakov wrote:
> > async_reply_handle_thread_unsafe() can run while holding
> > pending_requests.lock and currently calls rte_eal_alarm_cancel().
> >
> > rte_eal_alarm_cancel() may spin-wait for an executing callback, which can
> > deadlock if that callback is blocked on the same lock.
> >
> > Remove callback-side alarm cancellation. It is safe to do so, because any
> > callback triggered without a pending request becomes a noop.
> >
> > Fixes: daf9bfca717e ("ipc: remove thread for async requests")
> > Cc: stable@dpdk.org
> >
> > Signed-off-by: Anatoly Burakov <anatoly.burakov@intel.com>
> > ---
>
> Okay, the AI review seems to keep flagging issues that are technically
> true in the patches, but are intentional and do get better once the
> complete patchset is applied.
>
> Looks like I need to merge some of the patches or rethink the order in
> which the fixes are applied to avoid these issues.
The automated AI review has limited scope; it never looks at patch set in total,
and doesn't have tools to read source. That is why for complex things I tend
to start a new session and give it everything. Does much better job then.
^ permalink raw reply
* [PATCH v2 1/2] dts: add code coverage reporting to DTS
From: Koushik Bhargav Nimoji @ 2026-06-09 14:36 UTC (permalink / raw)
To: luca.vizzarro, patrickrobb1997
Cc: dev, abailey, ahassick, lylavoie, Koushik Bhargav Nimoji
In-Reply-To: <20260522154637.952588-1-knimoji@iol.unh.edu>
Previously, DTS had no code coverage. This patch adds a command line
argument in order to build DPDK with code coverage enabled. This allows
users to create and view code coverage reports of what code and functions
were called during a DTS run.
Signed-off-by: Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
---
.mailmap | 1 +
doc/guides/tools/dts.rst | 15 +++++++++++++
dts/README.md | 5 +++++
dts/framework/remote_session/dpdk.py | 19 ++++++++++++++++
.../remote_session/remote_session.py | 5 ++++-
dts/framework/settings.py | 10 +++++++++
dts/framework/testbed_model/os_session.py | 8 +++++++
dts/framework/testbed_model/posix_session.py | 22 +++++++++++++++++++
dts/framework/utils.py | 8 +++++++
9 files changed, 92 insertions(+), 1 deletion(-)
diff --git a/.mailmap b/.mailmap
index e052b85213..a1209150ad 100644
--- a/.mailmap
+++ b/.mailmap
@@ -877,6 +877,7 @@ Klaus Degner <kd@allegro-packets.com>
Kommula Shiva Shankar <kshankar@marvell.com>
Konstantin Ananyev <konstantin.ananyev@huawei.com> <konstantin.v.ananyev@yandex.ru>
Konstantin Ananyev <konstantin.ananyev@huawei.com> <konstantin.ananyev@intel.com>
+Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
Krishna Murthy <krishna.j.murthy@intel.com>
Krzysztof Galazka <krzysztof.galazka@intel.com>
Krzysztof Kanas <kkanas@marvell.com> <krzysztof.kanas@caviumnetworks.com>
diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
index 5b9a348016..a838a317ee 100644
--- a/doc/guides/tools/dts.rst
+++ b/doc/guides/tools/dts.rst
@@ -352,6 +352,10 @@ DTS is run with ``main.py`` located in the ``dts`` directory using the ``poetry
--precompiled-build-dir DIR_NAME
[DTS_PRECOMPILED_BUILD_DIR] Define the subdirectory under the DPDK tree root directory or tarball where the pre-
compiled binaries are located. (default: None)
+ --code-coverage Builds DPDK on the SUT node with code coverage enabled. Generates a code coverage report which can be found on
+ the local filesystem at dts/output/coverage_reports/meson-logs/coveragereport/index.html, or the specified output
+ directory. To use code coverage, please ensure lcov v1.15 and gcov v8.0 or higher (included in gcc package) are
+ installed on the SUT node.
The brackets contain the names of environment variables that set the same thing.
@@ -367,6 +371,17 @@ Results are stored in the output dir by default
which be changed with the ``--output-dir`` command line argument.
The results contain basic statistics of passed/failed test cases and DPDK version.
+Code Coverage
+~~~~~~~~~~~~~
+
+DTS has the ablilty to track code usage during test runs, and generate an HTML
+coverage report with that data. This can be done by using the "--code-coverage"
+CLI parameter when running DTS.
+
+To use code coverage, please make sure the following dependencies are available
+on the SUT node:
+- lcov v1.15
+- gcov v8.0 or greater (included in gcc package)
Contributing to DTS
-------------------
diff --git a/dts/README.md b/dts/README.md
index d257b7a167..51f824e077 100644
--- a/dts/README.md
+++ b/dts/README.md
@@ -64,6 +64,11 @@ $ poetry run ./main.py
These commands will give you a bash shell inside a docker container
with all DTS Python dependencies installed.
+# Code Coverage
+
+To generate code coverage reports, ensure the SUT has lcov v1.15 and gcov v8.0 or greater
+installed, and that DTS is run using the '--code-coverage' argument.
+
## Visual Studio Code
Usage of VScode devcontainers is NOT required for developing on DTS and running DTS,
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index c3575cfcaf..865f97f6ca 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -29,6 +29,7 @@
from framework.logger import DTSLogger, get_dts_logger
from framework.params.eal import EalParams
from framework.remote_session.remote_session import CommandResult
+from framework.settings import SETTINGS
from framework.testbed_model.cpu import LogicalCore, LogicalCoreCount, LogicalCoreList, lcore_filter
from framework.testbed_model.node import Node
from framework.testbed_model.os_session import OSSession
@@ -107,7 +108,22 @@ def teardown(self) -> None:
"""Teardown the DPDK build on the target node.
Removes the DPDK tree and/or build directory/tarball depending on the configuration.
+ If code coverage is enabled, the coverage report and .info file are generated and
+ copied onto the local filesystem before teardown.
"""
+ if SETTINGS.code_coverage:
+ report_folder = PurePath(self.remote_dpdk_build_dir / "meson-logs")
+ output_dir = SETTINGS.output_dir
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
+
+ coverage_status = self._session.generate_coverage_report(self.remote_dpdk_build_dir)
+ if coverage_status:
+ self._session.copy_dir_from(report_folder, output_dir)
+ self._logger.info(
+ "Coverage HTML report generated, "
+ f"available at {output_dir}/meson-logs/coveragereports/index.html"
+ )
+
match self.config.dpdk_location:
case LocalDPDKTreeLocation():
self._node.main_session.remove_remote_dir(self.remote_dpdk_tree_path)
@@ -272,6 +288,9 @@ def _build_dpdk(self) -> None:
else:
meson_args = MesonArgs(default_library="static", libdir="lib")
+ if SETTINGS.code_coverage:
+ meson_args._add_arg("-Db_coverage=true")
+
self._session.build_dpdk(
self._env_vars,
meson_args,
diff --git a/dts/framework/remote_session/remote_session.py b/dts/framework/remote_session/remote_session.py
index 158325bb7f..d2440dc2d8 100644
--- a/dts/framework/remote_session/remote_session.py
+++ b/dts/framework/remote_session/remote_session.py
@@ -252,7 +252,10 @@ def copy_from(self, source_file: str | PurePath, destination_dir: str | Path) ->
destination_dir: The directory path on the local filesystem where the `source_file`
will be saved.
"""
- self.session.get(str(source_file), str(destination_dir))
+ source_file = PurePath(source_file)
+ destination_dir = Path(destination_dir)
+ local_path = destination_dir / source_file.name
+ self.session.get(str(source_file), str(local_path))
def copy_to(self, source_file: str | Path, destination_dir: str | PurePath) -> None:
"""Copy a file from local filesystem to the remote Node.
diff --git a/dts/framework/settings.py b/dts/framework/settings.py
index b08373b7ea..7df535bd84 100644
--- a/dts/framework/settings.py
+++ b/dts/framework/settings.py
@@ -159,6 +159,8 @@ class Settings:
re_run: int = 0
#:
random_seed: int | None = None
+ #:
+ code_coverage: bool = False
SETTINGS: Settings = Settings()
@@ -489,6 +491,14 @@ def _get_parser() -> _DTSArgumentParser:
)
_add_env_var_to_action(action)
+ action = parser.add_argument(
+ "--code-coverage",
+ action="store_true",
+ default=False,
+ help="Used to build DPDK with code coverage enabled.",
+ )
+ _add_env_var_to_action(action)
+
return parser
diff --git a/dts/framework/testbed_model/os_session.py b/dts/framework/testbed_model/os_session.py
index 2c267afed1..a48383d1f1 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -480,6 +480,14 @@ def build_dpdk(
timeout: Wait at most this long in seconds for the build execution to complete.
"""
+ @abstractmethod
+ def generate_coverage_report(self, remote_build_dir: PurePath | None) -> int:
+ """Generates a code coverage report for a DTS run.
+
+ Args:
+ remote_build_dir: The remote DPDK build directory
+ """
+
@abstractmethod
def get_dpdk_version(self, version_path: str | PurePath) -> str:
"""Inspect the DPDK version on the remote node.
diff --git a/dts/framework/testbed_model/posix_session.py b/dts/framework/testbed_model/posix_session.py
index dec952685a..36ac9be7cf 100644
--- a/dts/framework/testbed_model/posix_session.py
+++ b/dts/framework/testbed_model/posix_session.py
@@ -295,6 +295,28 @@ def build_dpdk(
except RemoteCommandExecutionError as e:
raise DPDKBuildError(f"DPDK build failed when doing '{e.command}'.")
+ def generate_coverage_report(self, remote_build_dir: PurePath | None):
+ """Overrides :meth:`~.os_session.OSSession.generate_coverage_report`."""
+ command_result = self.send_command(r"lcov --version | grep -oP '\d+\.\d+'")
+ lcov_version = float(
+ command_result.stdout if command_result.return_code == 0 and command_result else -1
+ )
+ command_result = self.send_command(
+ r"gcov --version | head -n 1 | grep -oP '\d+\.\d+' | tail -n 1"
+ )
+ gcov_version = float(
+ command_result.stdout if command_result.return_code == 0 and command_result else -1
+ )
+
+ if lcov_version == 1.15 and gcov_version >= 8.0:
+ self.send_command(f"ninja -C {remote_build_dir} coverage-html", timeout=600)
+ return True
+ else:
+ self._logger.info(
+ "Unable to generate code coverage report, ensure lcov v1.5 and at least gcov v8.0"
+ )
+ return False
+
def get_dpdk_version(self, build_dir: str | PurePath) -> str:
"""Overrides :meth:`~.os_session.OSSession.get_dpdk_version`."""
out = self.send_command(f"cat {self.join_remote_path(build_dir, 'VERSION')}", verify=True)
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 9917ffbfaa..38da88cd9c 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -125,6 +125,14 @@ def __str__(self) -> str:
"""The actual args."""
return " ".join(f"{self._default_library} {self._dpdk_args}".split())
+ def _add_arg(self, arg: str):
+ """Used to add a meson build argument to the DPDK build.
+
+ Args:
+ arg: The meson build argument to be added.
+ """
+ self._dpdk_args = self._dpdk_args + " " + arg
+
class TarCompressionFormat(StrEnum):
"""Compression formats that tar can use.
--
2.54.0
^ permalink raw reply related
* [PATCH v2 2/2] dts: add build arguments to test run configuration
From: Koushik Bhargav Nimoji @ 2026-06-09 14:36 UTC (permalink / raw)
To: luca.vizzarro, patrickrobb1997
Cc: dev, abailey, ahassick, lylavoie, Koushik Bhargav Nimoji
In-Reply-To: <20260609143647.1434076-1-knimoji@iol.unh.edu>
This patch adds the ability to specify build arguments when building DPDK
through DTS. Doing so allows users to build DPDK with the desired build
arguments, which allows for a more configurable DTS run.
Signed-off-by: Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
---
dts/configurations/test_run.example.yaml | 13 +++++++++++++
dts/framework/config/test_run.py | 2 ++
dts/framework/remote_session/dpdk.py | 12 ++++++++----
dts/framework/utils.py | 21 ++++++++++++++++++++-
4 files changed, 43 insertions(+), 5 deletions(-)
diff --git a/dts/configurations/test_run.example.yaml b/dts/configurations/test_run.example.yaml
index ee641f5dce..0bd5151801 100644
--- a/dts/configurations/test_run.example.yaml
+++ b/dts/configurations/test_run.example.yaml
@@ -16,6 +16,8 @@
# `precompiled_build_dir` or `build_options` can be defined, but not both.
# `compiler_wrapper`:
# Optional, adds a compiler wrapper if present.
+# `build_args`:
+# The additional build arguments to be used when building DPDK.
# `func_traffic_generator` & `perf_traffic_generator`:
# Define `func_traffic_generator` when `func` set to true.
# Define `perf_traffic_generator` when `perf` set to true.
@@ -40,6 +42,17 @@ dpdk:
# the combination of the following two makes CC="ccache gcc"
compiler: gcc
compiler_wrapper: ccache # see `Optional Fields`
+ # arguments to be used when building DPDK
+ # build_args:
+ # c_args:
+ # - O3
+ # - g
+ # b_coverage:
+ # - "true"
+ # buildtype:
+ # - release
+ # flags:
+ # - strip
func_traffic_generator:
type: SCAPY
# perf_traffic_generator:
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 76e24d1785..eab12041fc 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -191,6 +191,8 @@ class DPDKBuildOptionsConfiguration(FrozenModel):
#: This string will be put in front of the compiler when executing the build. Useful for adding
#: wrapper commands, such as ``ccache``.
compiler_wrapper: str = ""
+ #: The build arguments to build dpdk with
+ build_args: dict[str, list[str]] = {}
class DPDKUncompiledBuildConfiguration(BaseDPDKBuildConfiguration):
diff --git a/dts/framework/remote_session/dpdk.py b/dts/framework/remote_session/dpdk.py
index 865f97f6ca..4dc0ceeaaf 100644
--- a/dts/framework/remote_session/dpdk.py
+++ b/dts/framework/remote_session/dpdk.py
@@ -100,8 +100,8 @@ def setup(self) -> None:
match self.config:
case DPDKPrecompiledBuildConfiguration(precompiled_build_dir=build_dir):
self._set_remote_dpdk_build_dir(build_dir)
- case DPDKUncompiledBuildConfiguration(build_options=build_options):
- self._configure_dpdk_build(build_options)
+ case DPDKUncompiledBuildConfiguration():
+ self._configure_dpdk_build(self.config.build_options)
self._build_dpdk()
def teardown(self) -> None:
@@ -277,16 +277,20 @@ def _build_dpdk(self) -> None:
`remote_dpdk_tree_path` has already been set on the SUT node.
"""
ctx = get_ctx()
+ build_options = getattr(self.config, "build_options")
# If the SUT is an ice driver device, make sure to build with 16B descriptors.
if (
ctx.topology.sut_port_ingress
and ctx.topology.sut_port_ingress.config.os_driver == "ice"
):
meson_args = MesonArgs(
- default_library="static", libdir="lib", c_args="-DRTE_NET_INTEL_USE_16BYTE_DESC"
+ build_options.build_args,
+ default_library="static",
+ libdir="lib",
+ c_args="-DRTE_NET_INTEL_USE_16BYTE_DESC",
)
else:
- meson_args = MesonArgs(default_library="static", libdir="lib")
+ meson_args = MesonArgs(build_options.build_args, default_library="static", libdir="lib")
if SETTINGS.code_coverage:
meson_args._add_arg("-Db_coverage=true")
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 38da88cd9c..e0ed35066c 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -99,10 +99,16 @@ class MesonArgs:
_default_library: str
- def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
+ def __init__(
+ self,
+ dpdk_build_args: dict[str, list[str]],
+ default_library: str | None = None,
+ **dpdk_args: str | bool,
+ ):
"""Initialize the meson arguments.
Args:
+ dpdk_build_args: The DPDK build arguments specified in the test run configuration file.
default_library: The default library type, Meson supports ``shared``, ``static`` and
``both``. Defaults to :data:`None`, in which case the argument won't be used.
dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory.
@@ -121,6 +127,19 @@ def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
)
)
+ arguments = []
+ for option, value in dpdk_build_args.items():
+ if option == "c_args":
+ values = " ".join(f"-{val}" for val in value)
+ arguments.append(f'-D{option}="{values}"')
+ elif option == "flags":
+ values = " ".join(f"--{val}" for val in value)
+ arguments.append(values)
+ else:
+ arguments.append(f" -D{option}={value[0]}")
+
+ self._dpdk_args = " ".join(arguments)
+
def __str__(self) -> str:
"""The actual args."""
return " ".join(f"{self._default_library} {self._dpdk_args}".split())
--
2.54.0
^ permalink raw reply related
* [PATCH v1] dts: update dts check format script and resolve errors
From: Koushik Bhargav Nimoji @ 2026-06-09 14:45 UTC (permalink / raw)
To: luca.vizzarro, patrickrobb1997, dev
Cc: abailey, ahassick, lylavoie, Koushik Bhargav Nimoji
This patch updates the tool versions used in the dts-check-format.sh
script;by doing so, formatting and type hinting errors that weren't
previously visible have now appeared. This patch also resolves those
new formatting and type hinting errors.
Signed-off-by: Koushik Bhargav Nimoji <knimoji@iol.unh.edu>
---
dts/api/packet.py | 15 +-
dts/api/testpmd/__init__.py | 12 +-
dts/api/testpmd/types.py | 6 +-
dts/framework/config/__init__.py | 24 +-
dts/framework/config/test_run.py | 12 +-
dts/framework/context.py | 8 +-
.../interactive_remote_session.py | 2 +-
.../remote_session/interactive_shell.py | 6 +-
.../remote_session/remote_session.py | 4 +-
dts/framework/remote_session/shell_pool.py | 2 +-
dts/framework/settings.py | 8 +-
dts/framework/testbed_model/cpu.py | 2 +-
.../testbed_model/traffic_generator/scapy.py | 6 +-
.../testbed_model/traffic_generator/trex.py | 6 +-
dts/framework/utils.py | 9 +-
dts/poetry.lock | 368 ++++++++++++++----
dts/pyproject.toml | 8 +-
dts/tests/TestSuite_cryptodev_throughput.py | 4 +-
dts/tests/TestSuite_port_control.py | 3 +-
.../TestSuite_single_core_forward_perf.py | 2 +-
20 files changed, 369 insertions(+), 138 deletions(-)
diff --git a/dts/api/packet.py b/dts/api/packet.py
index 094a1b7a9d..3dda18e781 100644
--- a/dts/api/packet.py
+++ b/dts/api/packet.py
@@ -87,9 +87,9 @@ def send_packets_and_capture(
CapturingTrafficGenerator,
)
- assert isinstance(
- get_ctx().func_tg, CapturingTrafficGenerator
- ), "Cannot capture with a non-capturing traffic generator"
+ assert isinstance(get_ctx().func_tg, CapturingTrafficGenerator), (
+ "Cannot capture with a non-capturing traffic generator"
+ )
tg: CapturingTrafficGenerator = cast(CapturingTrafficGenerator, get_ctx().func_tg)
# TODO: implement @requires for types of traffic generator
packets = adjust_addresses(packets)
@@ -308,8 +308,7 @@ def _verify_l2_frame(received_packet: Ether, contains_l3: bool) -> bool:
if contains_l3:
expected_src_mac = get_ctx().topology.sut_port_egress.mac_address
log_debug(
- f"Comparing received src mac '{received_packet.src}' "
- f"with expected '{expected_src_mac}'."
+ f"Comparing received src mac '{received_packet.src}' with expected '{expected_src_mac}'."
)
if received_packet.src != expected_src_mac:
return False
@@ -344,9 +343,9 @@ def assess_performance_by_packet(
PerformanceTrafficGenerator,
)
- assert isinstance(
- get_ctx().perf_tg, PerformanceTrafficGenerator
- ), "Cannot send performance traffic with non-performance traffic generator"
+ assert isinstance(get_ctx().perf_tg, PerformanceTrafficGenerator), (
+ "Cannot send performance traffic with non-performance traffic generator"
+ )
tg: PerformanceTrafficGenerator = cast(PerformanceTrafficGenerator, get_ctx().perf_tg)
# TODO: implement @requires for types of traffic generator
return tg.calculate_traffic_and_stats(packet, duration, send_mpps)
diff --git a/dts/api/testpmd/__init__.py b/dts/api/testpmd/__init__.py
index e9187440bb..1bde66d876 100644
--- a/dts/api/testpmd/__init__.py
+++ b/dts/api/testpmd/__init__.py
@@ -76,7 +76,9 @@ def _requires_stopped_ports(func: TestPmdMethod) -> TestPmdMethod:
"""
@functools.wraps(func)
- def _wrapper(self: "TestPmd", *args: P.args, **kwargs: P.kwargs) -> Any:
+ def _wrapper(
+ self: "TestPmd", *args: P.args, **kwargs: P.kwargs
+ ) -> Callable[P, "TestPmd"] | Any:
if self.ports_started:
self._logger.debug("Ports need to be stopped to continue.")
self.stop_all_ports()
@@ -100,7 +102,9 @@ def _requires_started_ports(func: TestPmdMethod) -> TestPmdMethod:
"""
@functools.wraps(func)
- def _wrapper(self: "TestPmd", *args: P.args, **kwargs: P.kwargs) -> Any:
+ def _wrapper(
+ self: "TestPmd", *args: P.args, **kwargs: P.kwargs
+ ) -> Callable[P, "TestPmd"] | Any:
if not self.ports_started:
self._logger.debug("Ports need to be started to continue.")
self.start_all_ports()
@@ -128,7 +132,9 @@ def _add_remove_mtu(mtu: int = 1500) -> Callable[[TestPmdMethod], TestPmdMethod]
def decorator(func: TestPmdMethod) -> TestPmdMethod:
@functools.wraps(func)
- def wrapper(self: "TestPmd", *args: P.args, **kwargs: P.kwargs) -> Any:
+ def wrapper(
+ self: "TestPmd", *args: P.args, **kwargs: P.kwargs
+ ) -> Callable[P, TestPmd] | Any:
original_mtu = self.ports[0].mtu
self.set_port_mtu_all(mtu=mtu, verify=False)
retval = func(self, *args, **kwargs)
diff --git a/dts/api/testpmd/types.py b/dts/api/testpmd/types.py
index 0d322aece2..af3263682e 100644
--- a/dts/api/testpmd/types.py
+++ b/dts/api/testpmd/types.py
@@ -279,7 +279,7 @@ def from_list_string(cls, names: str) -> Self:
Returns:
An instance of this flag.
"""
- flag = cls(0)
+ flag: RSSOffloadTypesFlag = cls(0)
for name in names.split():
flag |= cls.from_str(name)
return flag
@@ -960,7 +960,7 @@ def from_list_string(cls, names: str) -> Self:
Returns:
An instance of this flag.
"""
- flag = cls(0)
+ flag: PacketOffloadFlag = cls(0)
for name in names.split():
flag |= cls.from_str(name)
return flag
@@ -1168,7 +1168,7 @@ def from_list_string(cls, names: str) -> Self:
Returns:
An instance of this flag.
"""
- flag = cls(0)
+ flag: RtePTypes = cls(0)
for name in names.split():
flag |= cls.from_str(name)
return flag
diff --git a/dts/framework/config/__init__.py b/dts/framework/config/__init__.py
index d2f0138e4a..a8861894b7 100644
--- a/dts/framework/config/__init__.py
+++ b/dts/framework/config/__init__.py
@@ -85,9 +85,9 @@ def validate_port_links(self) -> Self:
sut_node_port_peer = existing_port_links.get(
(self.test_run.system_under_test_node, link.sut_port), None
)
- assert (
- sut_node_port_peer is not None
- ), f"Invalid SUT node port specified for link port_topology.{link_idx}."
+ assert sut_node_port_peer is not None, (
+ f"Invalid SUT node port specified for link port_topology.{link_idx}."
+ )
assert sut_node_port_peer is False or sut_node_port_peer == link.right, (
f"The SUT node port for link port_topology.{link_idx} is "
@@ -97,9 +97,9 @@ def validate_port_links(self) -> Self:
tg_node_port_peer = existing_port_links.get(
(self.test_run.traffic_generator_node, link.tg_port), None
)
- assert (
- tg_node_port_peer is not None
- ), f"Invalid TG node port specified for link port_topology.{link_idx}."
+ assert tg_node_port_peer is not None, (
+ f"Invalid TG node port specified for link port_topology.{link_idx}."
+ )
assert tg_node_port_peer is False or sut_node_port_peer == link.left, (
f"The TG node port for link port_topology.{link_idx} is "
@@ -117,16 +117,16 @@ def validate_test_run_against_nodes(self) -> Self:
sut_node_name = self.test_run.system_under_test_node
sut_node = next((n for n in self.nodes if n.name == sut_node_name), None)
- assert (
- sut_node is not None
- ), f"The system_under_test_node {sut_node_name} is not a valid node name."
+ assert sut_node is not None, (
+ f"The system_under_test_node {sut_node_name} is not a valid node name."
+ )
tg_node_name = self.test_run.traffic_generator_node
tg_node = next((n for n in self.nodes if n.name == tg_node_name), None)
- assert (
- tg_node is not None
- ), f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ assert tg_node is not None, (
+ f"The traffic_generator_name {tg_node_name} is not a valid node name."
+ )
return self
diff --git a/dts/framework/config/test_run.py b/dts/framework/config/test_run.py
index 76e24d1785..3cd643981d 100644
--- a/dts/framework/config/test_run.py
+++ b/dts/framework/config/test_run.py
@@ -233,9 +233,9 @@ def test_suite_spec(self) -> "TestSuiteSpec":
from framework.test_suite import find_by_name
test_suite_spec = find_by_name(self.test_suite_name)
- assert (
- test_suite_spec is not None
- ), f"{self.test_suite_name} is not a valid test suite module name."
+ assert test_suite_spec is not None, (
+ f"{self.test_suite_name} is not a valid test suite module name."
+ )
return test_suite_spec
@cached_property
@@ -384,9 +384,9 @@ def convert_from_string(cls, data: Any) -> Any:
@model_validator(mode="after")
def verify_distinct_nodes(self) -> Self:
"""Verify that each side of the link has distinct nodes."""
- assert (
- self.left.node_type != self.right.node_type
- ), "Linking ports of the same node is unsupported."
+ assert self.left.node_type != self.right.node_type, (
+ "Linking ports of the same node is unsupported."
+ )
return self
diff --git a/dts/framework/context.py b/dts/framework/context.py
index 8f1021dc96..efe9af0645 100644
--- a/dts/framework/context.py
+++ b/dts/framework/context.py
@@ -60,9 +60,9 @@ def reset(self) -> None:
else _field.default
)
- assert (
- default is not MISSING
- ), "{LocalContext.__name__} must have defaults on all fields!"
+ assert default is not MISSING, (
+ "{LocalContext.__name__} must have defaults on all fields!"
+ )
setattr(self, _field.name, default)
@@ -108,7 +108,7 @@ def filter_cores(
) -> Callable[[type["TestProtocol"]], Callable]:
"""Decorates functions that require a temporary update to the lcore specifier."""
- def decorator(func: type["TestProtocol"]) -> Callable:
+ def decorator(func: type["TestProtocol"]) -> Callable[P, type["TestProtocol"]]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Any:
local_ctx = get_ctx().local
diff --git a/dts/framework/remote_session/interactive_remote_session.py b/dts/framework/remote_session/interactive_remote_session.py
index c8156b4345..fc42e862bc 100644
--- a/dts/framework/remote_session/interactive_remote_session.py
+++ b/dts/framework/remote_session/interactive_remote_session.py
@@ -109,7 +109,7 @@ def _connect(self) -> None:
self._logger.debug(traceback.format_exc())
self._logger.warning(e)
self._logger.info(
- f"Retrying interactive session connection: retry number {retry_attempt +1}"
+ f"Retrying interactive session connection: retry number {retry_attempt + 1}"
)
else:
break
diff --git a/dts/framework/remote_session/interactive_shell.py b/dts/framework/remote_session/interactive_shell.py
index a65cbce209..6bba58a4f6 100644
--- a/dts/framework/remote_session/interactive_shell.py
+++ b/dts/framework/remote_session/interactive_shell.py
@@ -50,7 +50,9 @@
def only_active(func: InteractiveShellMethod) -> InteractiveShellMethod:
"""This decorator will skip running the method if the SSH channel is not active."""
- def _wrapper(self: "InteractiveShell", *args: P.args, **kwargs: P.kwargs) -> R | None:
+ def _wrapper(
+ self: "InteractiveShell", *args: P.args, **kwargs: P.kwargs
+ ) -> Callable[P, "InteractiveShell"] | None:
if self._ssh_channel.active:
return func(self, *args, **kwargs)
return None
@@ -167,7 +169,7 @@ def start_application(self, prompt: str | None = None, add_to_shell_pool: bool =
break
except InteractiveSSHTimeoutError:
self._logger.info(
- f"Interactive shell failed to start (attempt {attempt+1} out of "
+ f"Interactive shell failed to start (attempt {attempt + 1} out of "
f"{self._init_attempts})"
)
else:
diff --git a/dts/framework/remote_session/remote_session.py b/dts/framework/remote_session/remote_session.py
index 158325bb7f..fb5f6fedf5 100644
--- a/dts/framework/remote_session/remote_session.py
+++ b/dts/framework/remote_session/remote_session.py
@@ -72,9 +72,7 @@ def __post_init__(self, init_stdout: str, init_stderr: str) -> None:
def __str__(self) -> str:
"""Format the command outputs."""
return (
- f"stdout: '{self.stdout}'\n"
- f"stderr: '{self.stderr}'\n"
- f"return_code: '{self.return_code}'"
+ f"stdout: '{self.stdout}'\nstderr: '{self.stderr}'\nreturn_code: '{self.return_code}'"
)
diff --git a/dts/framework/remote_session/shell_pool.py b/dts/framework/remote_session/shell_pool.py
index 241737eab3..710107c6cb 100644
--- a/dts/framework/remote_session/shell_pool.py
+++ b/dts/framework/remote_session/shell_pool.py
@@ -74,7 +74,7 @@ def unregister_shell(self, shell: "InteractiveShell") -> None:
def start_new_pool(self) -> None:
"""Start a new shell pool."""
- self._logger.debug(f"Starting new shell pool and advancing to level {self.pool_level+1}.")
+ self._logger.debug(f"Starting new shell pool and advancing to level {self.pool_level + 1}.")
self._pools.append(set())
def terminate_current_pool(self) -> None:
diff --git a/dts/framework/settings.py b/dts/framework/settings.py
index b08373b7ea..f329677804 100644
--- a/dts/framework/settings.py
+++ b/dts/framework/settings.py
@@ -249,9 +249,9 @@ def error(self, message) -> NoReturn:
if _is_from_env(action):
action_name = _get_action_name(action)
env_var_name = _get_env_var_name(action)
- assert (
- env_var_name is not None
- ), "Action was set from environment, but no environment variable name was found."
+ assert env_var_name is not None, (
+ "Action was set from environment, but no environment variable name was found."
+ )
env_var_value = os.environ.get(env_var_name)
message = message.replace(
@@ -260,7 +260,7 @@ def error(self, message) -> NoReturn:
)
print(f"{self.prog}: error: {message}\n", file=sys.stderr)
- self.exit(2, "For help and usage, " "run the command with the --help flag.\n")
+ self.exit(2, "For help and usage, run the command with the --help flag.\n")
class _EnvVarHelpFormatter(ArgumentDefaultsHelpFormatter):
diff --git a/dts/framework/testbed_model/cpu.py b/dts/framework/testbed_model/cpu.py
index 6e2ecca080..a9471709dd 100644
--- a/dts/framework/testbed_model/cpu.py
+++ b/dts/framework/testbed_model/cpu.py
@@ -105,7 +105,7 @@ def __init__(self, lcore_list: list[int] | list[str] | list[LogicalCore] | str)
# the input lcores may not be sorted
self._lcore_list.sort()
- self._lcore_str = f'{",".join(self._get_consecutive_lcores_range(self._lcore_list))}'
+ self._lcore_str = f"{','.join(self._get_consecutive_lcores_range(self._lcore_list))}"
@property
def lcore_list(self) -> list[int]:
diff --git a/dts/framework/testbed_model/traffic_generator/scapy.py b/dts/framework/testbed_model/traffic_generator/scapy.py
index c6e9006205..62853a34e4 100644
--- a/dts/framework/testbed_model/traffic_generator/scapy.py
+++ b/dts/framework/testbed_model/traffic_generator/scapy.py
@@ -314,9 +314,9 @@ def __init__(self, tg_node: Node, config: ScapyTrafficGeneratorConfig, **kwargs:
kwargs: Additional keyword arguments. Supported arguments correspond to the parameters
of :meth:`PythonShell.__init__` in this case.
"""
- assert (
- tg_node.config.os == OS.linux
- ), "Linux is the only supported OS for scapy traffic generation"
+ assert tg_node.config.os == OS.linux, (
+ "Linux is the only supported OS for scapy traffic generation"
+ )
super().__init__(tg_node=tg_node, config=config, **kwargs)
diff --git a/dts/framework/testbed_model/traffic_generator/trex.py b/dts/framework/testbed_model/traffic_generator/trex.py
index 22cd20dea9..0314dfc881 100644
--- a/dts/framework/testbed_model/traffic_generator/trex.py
+++ b/dts/framework/testbed_model/traffic_generator/trex.py
@@ -94,9 +94,9 @@ def __init__(self, tg_node: Node, config: TrexTrafficGeneratorConfig) -> None:
tg_node: TG node the TRex instance is operating on.
config: Traffic generator config provided for TRex instance.
"""
- assert (
- tg_node.config.os == OS.linux
- ), "Linux is the only supported OS for trex traffic generation"
+ assert tg_node.config.os == OS.linux, (
+ "Linux is the only supported OS for trex traffic generation"
+ )
super().__init__(tg_node=tg_node, config=config)
self._tg_node_config = tg_node.config
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index 9917ffbfaa..d16580ec85 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -154,7 +154,11 @@ def extension(self) -> str:
For other compression formats, the extension will be in the format
'tar.{compression format}'.
"""
- return f"{self.value}" if self == self.none else f"{type(self).none.value}.{self.value}"
+ return (
+ f"{self.value}"
+ if self == self.none
+ else f"{TarCompressionFormat.none.value}.{self.value}"
+ )
def convert_to_list_of_string(value: Any | list[Any]) -> list[str]:
@@ -207,7 +211,8 @@ def filter_func(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:
return None
target_tarball_path = dir_path.with_suffix(f".{compress_format.extension}")
- with tarfile.open(target_tarball_path, f"w:{compress_format.value}") as tar:
+ compression_format: Any = f"w:{compress_format.value}"
+ with tarfile.open(target_tarball_path, compression_format) as tar:
tar.add(dir_path, arcname=dir_path.name, filter=create_filter_function(exclude))
return target_tarball_path
diff --git a/dts/poetry.lock b/dts/poetry.lock
index 0ad5d32b85..eda7cc2fec 100644
--- a/dts/poetry.lock
+++ b/dts/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "aenum"
@@ -6,6 +6,7 @@ version = "3.1.15"
description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "aenum-3.1.15-py2-none-any.whl", hash = "sha256:27b1710b9d084de6e2e695dab78fe9f269de924b51ae2850170ee7e1ca6288a5"},
{file = "aenum-3.1.15-py3-none-any.whl", hash = "sha256:e0dfaeea4c2bd362144b87377e2c61d91958c5ed0b4daf89cb6f45ae23af6288"},
@@ -18,17 +19,62 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
+[[package]]
+name = "ast-serialize"
+version = "0.5.0"
+description = "Python bindings for mypy AST serialization"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c"},
+ {file = "ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590"},
+ {file = "ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642"},
+ {file = "ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6"},
+]
+
[[package]]
name = "bcrypt"
version = "4.2.1"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"},
{file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"},
@@ -67,6 +113,7 @@ version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -136,6 +183,7 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
+markers = {dev = "platform_python_implementation != \"PyPy\""}
[package.dependencies]
pycparser = "*"
@@ -146,6 +194,7 @@ version = "44.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
+groups = ["main", "dev"]
files = [
{file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"},
{file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"},
@@ -180,10 +229,10 @@ files = [
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
-pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
+pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
@@ -195,6 +244,7 @@ version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
+groups = ["main"]
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
@@ -206,6 +256,7 @@ version = "1.2.18"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+groups = ["main"]
files = [
{file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"},
{file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"},
@@ -215,7 +266,7 @@ files = [
wrapt = ">=1.10,<2"
[package.extras]
-dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"]
+dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"]
[[package]]
name = "fabric"
@@ -223,6 +274,7 @@ version = "3.2.2"
description = "High level SSH command execution"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "fabric-3.2.2-py3-none-any.whl", hash = "sha256:91c47c0be68b14936c88b34da8a1f55e5710fd28397dac5d4ff2e21558113a6f"},
{file = "fabric-3.2.2.tar.gz", hash = "sha256:8783ca42e3b0076f08b26901aac6b9d9b1f19c410074e7accfab902c184ff4a3"},
@@ -243,56 +295,177 @@ version = "2.2.0"
description = "Pythonic task execution"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"},
{file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
]
+[[package]]
+name = "librt"
+version = "0.11.0"
+description = "Mypyc runtime library"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+markers = "platform_python_implementation != \"PyPy\""
+files = [
+ {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"},
+ {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"},
+ {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"},
+ {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"},
+ {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"},
+ {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"},
+ {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"},
+ {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"},
+ {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"},
+ {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"},
+ {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"},
+ {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"},
+ {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"},
+ {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"},
+ {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"},
+ {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"},
+ {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"},
+ {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"},
+ {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"},
+ {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"},
+ {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"},
+ {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"},
+ {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"},
+ {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"},
+ {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"},
+ {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"},
+ {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"},
+ {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"},
+ {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"},
+ {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"},
+ {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"},
+ {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"},
+ {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"},
+ {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"},
+]
+
[[package]]
name = "mypy"
-version = "1.13.0"
+version = "2.1.0"
description = "Optional static typing for Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["dev"]
files = [
- {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
- {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
- {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
- {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
- {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
- {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
- {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
- {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
- {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
- {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
- {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
- {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
- {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
- {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
- {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
- {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
- {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
- {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
- {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
- {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
- {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
- {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
- {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
- {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
- {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
- {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
- {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
- {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
- {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
- {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
- {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
- {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
+ {file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"},
+ {file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"},
+ {file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"},
+ {file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"},
+ {file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"},
+ {file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"},
+ {file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"},
+ {file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"},
+ {file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"},
+ {file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"},
+ {file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"},
+ {file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"},
+ {file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"},
+ {file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"},
+ {file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"},
+ {file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"},
+ {file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"},
+ {file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"},
+ {file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"},
+ {file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"},
+ {file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"},
+ {file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"},
+ {file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"},
+ {file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"},
+ {file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"},
+ {file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"},
+ {file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"},
+ {file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"},
+ {file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"},
+ {file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"},
+ {file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"},
+ {file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"},
+ {file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"},
+ {file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"},
+ {file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"},
+ {file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"},
+ {file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"},
+ {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"},
+ {file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"},
+ {file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"},
+ {file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"},
+ {file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"},
+ {file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"},
+ {file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"},
]
[package.dependencies]
-mypy-extensions = ">=1.0.0"
+ast-serialize = ">=0.3.0,<1.0.0"
+librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""}
+mypy_extensions = ">=1.0.0"
+pathspec = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = ">=4.6.0"
+typing_extensions = [
+ {version = ">=4.6.0", markers = "python_version < \"3.15\""},
+ {version = ">=4.14.0", markers = "python_version >= \"3.15\""},
+]
[package.extras]
dmypy = ["psutil (>=4.0)"]
@@ -307,6 +480,7 @@ version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
+groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -318,6 +492,7 @@ version = "3.5.0"
description = "SSH2 protocol library"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"},
{file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"},
@@ -329,20 +504,39 @@ cryptography = ">=3.3"
pynacl = ">=1.5"
[package.extras]
-all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
-gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"]
+all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""]
+gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""]
invoke = ["invoke (>=2.0)"]
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"},
+ {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"},
+]
+
+[package.extras]
+hyperscan = ["hyperscan (>=0.7)"]
+optional = ["typing-extensions (>=4)"]
+re2 = ["google-re2 (>=1.1)"]
+
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
+markers = {dev = "platform_python_implementation != \"PyPy\""}
[[package]]
name = "pydantic"
@@ -350,6 +544,7 @@ version = "2.10.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
@@ -362,7 +557,7 @@ typing-extensions = ">=4.12.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
@@ -370,6 +565,7 @@ version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
@@ -482,6 +678,7 @@ version = "1.5.0"
description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
@@ -508,6 +705,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
@@ -566,29 +764,30 @@ files = [
[[package]]
name = "ruff"
-version = "0.8.2"
+version = "0.15.16"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
- {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"},
- {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"},
- {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"},
- {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"},
- {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"},
- {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"},
- {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"},
- {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"},
- {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"},
- {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"},
- {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"},
- {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"},
+ {file = "ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2"},
+ {file = "ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a"},
+ {file = "ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb"},
+ {file = "ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951"},
+ {file = "ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8"},
+ {file = "ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123"},
+ {file = "ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a"},
+ {file = "ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3"},
+ {file = "ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e"},
+ {file = "ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec"},
+ {file = "ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121"},
+ {file = "ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78"},
]
[[package]]
@@ -597,6 +796,7 @@ version = "2.6.1"
description = "Scapy: interactive packet manipulation tool"
optional = false
python-versions = "<4,>=3.7"
+groups = ["main"]
files = [
{file = "scapy-2.6.1-py3-none-any.whl", hash = "sha256:88a998572049b511a1f3e44f4aa7c62dd39c6ea2aa1bb58434f503956641789d"},
{file = "scapy-2.6.1.tar.gz", hash = "sha256:7600d7e2383c853e5c3a6e05d37e17643beebf2b3e10d7914dffcc3bc3c6e6c5"},
@@ -613,6 +813,7 @@ version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+groups = ["dev"]
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -624,6 +825,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -665,6 +868,7 @@ version = "2.0.0.10"
description = "Typing stubs for invoke"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "types-invoke-2.0.0.10.tar.gz", hash = "sha256:a54d7ecdc19e0c22cd2786ef2e64c2631715c78eba8a1bf40b511d0608f33a88"},
{file = "types_invoke-2.0.0.10-py3-none-any.whl", hash = "sha256:2404e4279601fa96e14ef68321fd10a660a828677aabdcaeef6a5189778084ef"},
@@ -672,13 +876,14 @@ files = [
[[package]]
name = "types-paramiko"
-version = "3.5.0.20240928"
+version = "4.0.0.20260518"
description = "Typing stubs for paramiko"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["dev"]
files = [
- {file = "types-paramiko-3.5.0.20240928.tar.gz", hash = "sha256:79dd9b2ee510b76a3b60d8ac1f3f348c45fcecf01347ca79e14db726bbfc442d"},
- {file = "types_paramiko-3.5.0.20240928-py3-none-any.whl", hash = "sha256:cda0aff4905fe8efe4b5448331a80e943d42a796bd4beb77a3eed3485bc96a85"},
+ {file = "types_paramiko-4.0.0.20260518-py3-none-any.whl", hash = "sha256:0ffaf1a6eb796833a49653cba4c7be13af51c8269d75234972d6239763dda270"},
+ {file = "types_paramiko-4.0.0.20260518.tar.gz", hash = "sha256:286f6830945cba63797eedf375ed87138d93198121253afe66c5d6dbcf91318d"},
]
[package.dependencies]
@@ -686,13 +891,14 @@ cryptography = ">=37.0.0"
[[package]]
name = "types-pyyaml"
-version = "6.0.12.20240917"
+version = "6.0.12.20260518"
description = "Typing stubs for PyYAML"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["dev"]
files = [
- {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"},
- {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"},
+ {file = "types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd"},
+ {file = "types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466"},
]
[[package]]
@@ -701,17 +907,33 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
+markers = "python_version < \"3.15\""
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main", "dev"]
+markers = "python_version >= \"3.15\""
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
[[package]]
name = "wrapt"
version = "1.17.2"
description = "Module for decorators, wrappers and monkey patching."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"},
{file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"},
@@ -795,6 +1017,6 @@ files = [
]
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = "^3.10"
-content-hash = "aa6dff54827602c89ee125019291965de50d7a471ca48add889ea23aa5fd9b2f"
+content-hash = "1ca3cdf5bf98c528845b5e22169322db8912fe437988635f4ebea088b2079463"
diff --git a/dts/pyproject.toml b/dts/pyproject.toml
index 8b061c3cee..b639af65b6 100644
--- a/dts/pyproject.toml
+++ b/dts/pyproject.toml
@@ -28,12 +28,12 @@ aenum = "^3.1.15"
pydantic = "^2.9.2"
[tool.poetry.group.dev.dependencies]
-mypy = "^1.13.0"
+mypy = "^2.1.0"
toml = "^0.10.2"
-ruff = "^0.8.1"
-types-paramiko = "^3.5.0.20240928"
+ruff = "^0.15.16"
+types-paramiko = "^4.0.0.20260518"
types-invoke = "^2.0.0.10"
-types-pyyaml = "^6.0.12.20240917"
+types-pyyaml = "^6.0.12.20260518"
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/dts/tests/TestSuite_cryptodev_throughput.py b/dts/tests/TestSuite_cryptodev_throughput.py
index af0a5680ab..2fc0d8779a 100644
--- a/dts/tests/TestSuite_cryptodev_throughput.py
+++ b/dts/tests/TestSuite_cryptodev_throughput.py
@@ -101,12 +101,12 @@ def _print_stats(self, test_vals: list[dict[str, int | float | str]]) -> None:
print(f"{'Throughput Results'.center(border_len)}\n{'=' * border_len}")
for k, v in test_vals[0].items():
print(f"|{k.title():<{element_len}}", end="")
- print(f"|\n{'='*border_len}")
+ print(f"|\n{'=' * border_len}")
for test_val in test_vals:
for k, v in test_val.items():
print(f"|{v:<{element_len}}", end="")
- print(f"|\n{'='*border_len}")
+ print(f"|\n{'=' * border_len}")
def _verify_throughput(
self,
diff --git a/dts/tests/TestSuite_port_control.py b/dts/tests/TestSuite_port_control.py
index 6be47838d0..b51fdc2959 100644
--- a/dts/tests/TestSuite_port_control.py
+++ b/dts/tests/TestSuite_port_control.py
@@ -44,8 +44,7 @@ def _send_packets_and_verify(self) -> None:
recv_pakts = [
p
for p in recv_pakts
- if
- (
+ if (
# Remove padding from the bytes.
hasattr(p, "load") and p.load.decode("utf-8").replace("\x00", "") == payload
)
diff --git a/dts/tests/TestSuite_single_core_forward_perf.py b/dts/tests/TestSuite_single_core_forward_perf.py
index 1e7ab7b036..acdf8ae2f6 100644
--- a/dts/tests/TestSuite_single_core_forward_perf.py
+++ b/dts/tests/TestSuite_single_core_forward_perf.py
@@ -144,7 +144,7 @@ def single_core_forward_perf(self) -> None:
for params in self.test_parameters:
verify(
params["pass"] is True,
- f"""Packets forwarded is less than {(1 -self.delta_tolerance)*100}%
+ f"""Packets forwarded is less than {(1 - self.delta_tolerance) * 100}%
of the expected baseline.
Measured MPPS = {params["measured_mpps"]}
Expected MPPS = {params["expected_mpps"]}""",
--
2.54.0
^ permalink raw reply related
* Re: [PATCH 0/3] net/iavf: vf reset fixes
From: Bruce Richardson @ 2026-06-09 14:53 UTC (permalink / raw)
To: Ciara Loftus; +Cc: dev
In-Reply-To: <20260608145518.1705524-1-ciara.loftus@intel.com>
On Mon, Jun 08, 2026 at 02:55:15PM +0000, Ciara Loftus wrote:
> The patch [1] aimed to address a race condition in the iavf driver
> during a reset and also reduced noisy logging during resets.
> Patch 1 of this series extracts the noisy logging fix into its own
> commit.
> Patch 2 offers an alternative approach to fixing the race condition.
> Patch 3 fixes a pre-existing refcount imbalance in the shared event
> handler thread that became visible while investigating the reset path.
>
> [1] https://patches.dpdk.org/project/dpdk/patch/20260605123646.1328492-1-chaitanyababux.talluri@intel.com/
>
> Ciara Loftus (2):
> net/iavf: wait for PF reset start before reinitializing
> net/iavf: fix event handler refcount leak on HW reset
>
> Talluri Chaitanyababu (1):
> net/iavf: downgrade opcode 0 ARQ log to debug
>
> drivers/net/intel/iavf/iavf.h | 1 +
> drivers/net/intel/iavf/iavf_ethdev.c | 14 +++++++++++++-
> drivers/net/intel/iavf/iavf_vchnl.c | 11 +++++++++--
> 3 files changed, 23 insertions(+), 3 deletions(-)
>
Series-acked-by: Bruce Richardson <bruce.richardson@intel.com>
Series applied to dpdk-next-net-intel.
Thanks,
/Bruce
^ permalink raw reply
* [PATCH v3 0/2] eal: tailq fixes
From: Stephen Hemminger @ 2026-06-09 15:53 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
In-Reply-To: <20260607150418.30885-1-stephen@networkplumber.org>
A couple of small fixes to EAL tailq
Stephen Hemminger (2):
eal: fix off by one in in tailq name init
eal: add destructor to unregister tailq on unload
lib/eal/common/eal_common_tailqs.c | 15 ++++++++++++++-
lib/eal/include/rte_tailq.h | 16 ++++++++++++++++
2 files changed, 30 insertions(+), 1 deletion(-)
--
2.53.0
^ permalink raw reply
* [PATCH v3 1/2] eal: fix off by one in in tailq name init
From: Stephen Hemminger @ 2026-06-09 15:53 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger, stable, Bruce Richardson
In-Reply-To: <20260609155419.263787-1-stephen@networkplumber.org>
The tailq name is defined as 32 bytes, but name would be
silently truncated at 31 bytes. The function strlcpy() size
already accounts for the nul character at the end.
Fixes: f9acaf84e923 ("replace snprintf with strlcpy without adding extra include")
Cc: stable@dpdk.org
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
Exposed by automated review of next patch.
lib/eal/common/eal_common_tailqs.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/eal/common/eal_common_tailqs.c b/lib/eal/common/eal_common_tailqs.c
index c581f43b6f..fe3ced21c7 100644
--- a/lib/eal/common/eal_common_tailqs.c
+++ b/lib/eal/common/eal_common_tailqs.c
@@ -83,7 +83,7 @@ rte_eal_tailq_create(const char *name)
mcfg = rte_eal_get_configuration()->mem_config;
head = &mcfg->tailq_head[rte_tailqs_count];
- strlcpy(head->name, name, sizeof(head->name) - 1);
+ strlcpy(head->name, name, sizeof(head->name));
TAILQ_INIT(&head->tailq_head);
rte_tailqs_count++;
}
--
2.53.0
^ permalink raw reply related
* [PATCH v3 2/2] eal: add destructor to unregister tailq on unload
From: Stephen Hemminger @ 2026-06-09 15:53 UTC (permalink / raw)
To: dev
Cc: Stephen Hemminger, stable, Bruce Richardson, David Marchand,
Neil Horman
In-Reply-To: <20260609155419.263787-1-stephen@networkplumber.org>
EAL_REGISTER_TAILQ registers a static rte_tailq_elem from a
constructor but provides no destructor. If a library using the
macro is loaded with dlopen() and later unloaded with dlclose(),
the process-local list keeps a dangling pointer to the unmapped
elem, and the next dlopen() crashes in rte_eal_tailq_local_register()
while walking the list.
Add a new RTE_FINI destructor that is paired with the constructor
in the macro. rte_eal_tailq_unregister() drops the local entry on
unload. The shared mcfg->tailq_head[] slot is left reserved since
it is keyed by name and shared between processes;
rte_eal_tailq_update() now reattaches to that slot on re-register
instead of failing.
Bugzilla ID: 1081
Fixes: 873a61c7526b ("tailq: introduce dynamic register system")
Cc: stable@dpdk.org
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
Acked-by: Bruce Richardson <bruce.richardson@intel.com>
---
lib/eal/common/eal_common_tailqs.c | 13 +++++++++++++
lib/eal/include/rte_tailq.h | 16 ++++++++++++++++
2 files changed, 29 insertions(+)
diff --git a/lib/eal/common/eal_common_tailqs.c b/lib/eal/common/eal_common_tailqs.c
index fe3ced21c7..34e6883f65 100644
--- a/lib/eal/common/eal_common_tailqs.c
+++ b/lib/eal/common/eal_common_tailqs.c
@@ -113,6 +113,11 @@ rte_eal_tailq_update(struct rte_tailq_elem *t)
if (rte_eal_process_type() == RTE_PROC_PRIMARY) {
/* primary process is the only one that creates */
t->head = rte_eal_tailq_create(t->name);
+
+ if (t->head == NULL) {
+ /* slot reserved by an earlier load -- reuse it */
+ t->head = rte_eal_tailq_lookup(t->name);
+ }
} else {
t->head = rte_eal_tailq_lookup(t->name);
}
@@ -148,6 +153,14 @@ rte_eal_tailq_register(struct rte_tailq_elem *t)
return -1;
}
+RTE_EXPORT_SYMBOL(rte_eal_tailq_unregister)
+void
+rte_eal_tailq_unregister(struct rte_tailq_elem *t)
+{
+ TAILQ_REMOVE(&rte_tailq_elem_head, t, next);
+ t->head = NULL;
+}
+
int
rte_eal_tailqs_init(void)
{
diff --git a/lib/eal/include/rte_tailq.h b/lib/eal/include/rte_tailq.h
index e7caed6812..d4d8bfd6d4 100644
--- a/lib/eal/include/rte_tailq.h
+++ b/lib/eal/include/rte_tailq.h
@@ -117,11 +117,27 @@ struct rte_tailq_head *rte_eal_tailq_lookup(const char *name);
*/
int rte_eal_tailq_register(struct rte_tailq_elem *t);
+/**
+ * Remove a tail queue element from the local list.
+ * This function is mainly used for EAL_REGISTER_TAILQ macro which pairs
+ * an RTE_FINI destructor with the existing RTE_INIT constructor.
+ * The destructor calls this function during dlclose() to prevent
+ * dangling pointers to unmapped library data.
+ *
+ * @param t
+ * The tailq element to remove from the EAL tailq list.
+ */
+void rte_eal_tailq_unregister(struct rte_tailq_elem *t);
+
#define EAL_REGISTER_TAILQ(t) \
RTE_INIT(tailqinitfn_ ##t) \
{ \
if (rte_eal_tailq_register(&t) < 0) \
rte_panic("Cannot initialize tailq: %s\n", t.name); \
+} \
+RTE_FINI(tailqfinifn_ ##t) \
+{ \
+ rte_eal_tailq_unregister(&t); \
}
/* This macro permits both remove and free var within the loop safely.*/
--
2.53.0
^ permalink raw reply related
* [PATCH v3 0/3] extend interactive telemetry script
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
In-Reply-To: <20260521153913.82634-1-bruce.richardson@intel.com>
To simplify interactive telemetry script for general use, i.e. not from
other scripts, we can add two new features to it:
1. Support for FOREACH to allow gathering a set of output values across
a list of ports or devices, e.g. ethdevs or rawdevs.
2. Support having predefined aliases in a file in the user's home
directory to simplify the use of more complicated FOREACH commands.
Putting these together, we can create new commands such as "eth_names".
bruce@host:$ cat ~/.dpdk_telemetry_aliases
eth_names=FOREACH index /ethdev/list /ethdev/info,$index .name
bruce@host:$ echo eth_names | ./usertools/dpdk-telemetry.py | jq
[
{
"index": 0,
"name": "0000:16:00.0"
},
{
"index": 1,
"name": "0000:16:00.1"
}
]
---
v3: updated based on review feedback from Chengwen:
- added arg to override alias file
- printed one-line summary of alias count loaded
- improved doc for "help" command
- added "help alias" to list aliases.
v2: added third patch with "help" command giving more details on
how to use the various commands.
Bruce Richardson (3):
usertools/telemetry: add a FOREACH command
usertools/telemetry: support using aliases for long commands
usertools/telemetry: add help support
doc/guides/howto/telemetry.rst | 106 ++++++++++++-
usertools/dpdk-telemetry.py | 278 ++++++++++++++++++++++++++++++++-
2 files changed, 373 insertions(+), 11 deletions(-)
--
2.53.0
^ permalink raw reply
* [PATCH v3 1/3] usertools/telemetry: add a FOREACH command
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
In-Reply-To: <20260609161400.3661268-1-bruce.richardson@intel.com>
To simplify querying data from multiple devices, e.g. across ethdevs, or
dmadevs, add a FOREACH command to the python script, allowing you to
run, e.g. /ethdev/list, and then run a second command for each item in
the list, gathering the relevant output values, optionally including an
index counter.
Simple examples are given in the documentation:
--> FOREACH /ethdev/list /ethdev/stats .opackets
[0, 0]
--> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
[{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
--> FOREACH i /ethdev/list /ethdev/info,$i .name
[{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
--> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
[{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
Acked-by: Chengwen Feng <fengchengwen@huawei.com>
---
doc/guides/howto/telemetry.rst | 42 +++++++++++
usertools/dpdk-telemetry.py | 128 ++++++++++++++++++++++++++++++++-
2 files changed, 167 insertions(+), 3 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 0464c431fe..4bf48c635e 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -88,6 +88,48 @@ and query information using the telemetry client python script.
{"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
Parameters: int port_id"}}
+ * Run a compound query using ``FOREACH``.
+
+ The ``FOREACH`` command runs a list command, iterates each returned item,
+ runs a second command for each item, and emits combined JSON output.
+
+ Start with the simplest form (no loop variable)::
+
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+
+ To include numbered output, use a loop variable::
+
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Notes:
+
+ - Field selectors are whitespace-separated tokens, each starting with ``.``.
+ - In no-variable mode, the iter command is called as ``/<iter_cmd>,<item>``.
+ - In loop-variable mode, use ``$<var>`` in the iter command where the
+ item value should be substituted.
+
+ Examples::
+
+ --> FOREACH /ethdev/list /ethdev/stats .opackets
+ [0, 0]
+
+ --> FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ [{"ipackets": 0, "opackets": 0}, {"ipackets": 0, "opackets": 0}]
+
+ --> FOREACH i /ethdev/list /ethdev/info,$i .name
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+ [{"i": 0, "ipackets": 0, "opackets": 0}, {"i": 1, "ipackets": 0, "opackets": 0}]
+
+ Output behavior:
+
+ - Without loop variable and one field: returns an array of values.
+ - Without loop variable and multiple fields: returns an array of objects
+ containing named value fields.
+ - With loop variable: returns an array of objects containing the loop
+ variable field and requested value fields.
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 09258a1f7e..2de10cff69 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -23,6 +23,130 @@
CMDS = []
+def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
+ """Send a telemetry command and return the parsed JSON reply"""
+ sock.send(cmd.encode())
+ return read_socket(sock, output_buf_len, echo, pretty)
+
+
+def get_cmd_payload(reply, cmd):
+ """Return the payload for a command response if present"""
+ if isinstance(reply, dict) and len(reply) == 1:
+ return next(iter(reply.values()))
+ return None
+
+
+def get_path_value(payload, path):
+ """Resolve a dotted path (e.g. '.name' or '.a.b') from a JSON payload"""
+ if not path:
+ return payload
+
+ keys = [k for k in path.lstrip(".").split(".") if k]
+ val = payload
+ for key in keys:
+ if not isinstance(val, dict) or key not in val:
+ return None
+ val = val[key]
+ return val
+
+
+def parse_selectors(selector_text):
+ """Parse whitespace-separated dotted selectors"""
+ selectors = selector_text.split()
+ if not selectors:
+ print("Invalid FOREACH syntax: missing selector")
+ return None
+ if any(not selector.startswith(".") for selector in selectors):
+ print("Invalid FOREACH syntax: selector must start with '.'")
+ return None
+ return selectors
+
+
+def parse_foreach(text):
+ """Parse FOREACH [<var>] /<cmd> /<parameterized cmd> .<value> [.<value> ...]"""
+ try:
+ tokens = text.split(None, 3)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if len(tokens) != 4:
+ print("Invalid FOREACH syntax")
+ return None
+
+ _, arg1, arg2, arg3 = tokens
+ if arg1.startswith("/"):
+ var_name = None
+ list_cmd = arg1
+ iter_cmd = arg2
+ selector_text = arg3
+ else:
+ var_name = arg1
+ list_cmd = arg2
+ try:
+ iter_cmd, selector_text = arg3.split(None, 1)
+ except ValueError:
+ print("Invalid FOREACH syntax")
+ return None
+
+ if not list_cmd.startswith("/") or not iter_cmd.startswith("/"):
+ print("Invalid FOREACH syntax: commands must start with '/'")
+ return None
+
+ selectors = parse_selectors(selector_text)
+ if selectors is None:
+ return None
+
+ return var_name, list_cmd, iter_cmd, selectors
+
+
+def build_foreach_result(item, var_name, payload, selectors):
+ """Build one FOREACH result entry based on selector count and index mode"""
+ values = {selector.lstrip("."): get_path_value(payload, selector) for selector in selectors}
+
+ if var_name is None and len(selectors) == 1:
+ return next(iter(values.values()))
+ if var_name is None:
+ return values
+
+ return {var_name: item, **values}
+
+
+def handle_foreach(sock, output_buf_len, text, pretty=False):
+ """Handle FOREACH queries and print telemetry-like JSON array output"""
+ parsed = parse_foreach(text)
+ if parsed is None:
+ return
+ var_name, list_cmd, iter_cmd, selectors = parsed
+
+ list_reply = send_command(sock, list_cmd, output_buf_len)
+ values = get_cmd_payload(list_reply, list_cmd)
+ if not isinstance(values, list):
+ print("FOREACH source command did not return a JSON array")
+ return
+
+ output = []
+ for item in values:
+ if var_name is None:
+ cmd = "{},{}".format(iter_cmd, item)
+ else:
+ cmd = iter_cmd.replace("$" + var_name, str(item))
+ item_reply = send_command(sock, cmd, output_buf_len)
+ item_payload = get_cmd_payload(item_reply, cmd)
+ output.append(build_foreach_result(item, var_name, item_payload, selectors))
+
+ indent = 2 if pretty else None
+ print(json.dumps(output, indent=indent))
+
+
+def handle_command(sock, output_buf_len, text, pretty=False):
+ """Execute a user command if recognized"""
+ if text.startswith("/"):
+ send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text.startswith("FOREACH "):
+ handle_foreach(sock, output_buf_len, text, pretty)
+
+
def read_socket(sock, buf_len, echo=True, pretty=False):
"""Read data from socket and return it in JSON format"""
reply = sock.recv(buf_len).decode()
@@ -140,9 +264,7 @@ def handle_socket(args, path):
try:
text = input(prompt).strip()
while text != "quit":
- if text.startswith("/"):
- sock.send(text.encode())
- read_socket(sock, output_buf_len, pretty=prompt)
+ handle_command(sock, output_buf_len, text, pretty=prompt)
text = input(prompt).strip()
except EOFError:
pass
--
2.53.0
^ permalink raw reply related
* [PATCH v3 2/3] usertools/telemetry: support using aliases for long commands
From: Bruce Richardson @ 2026-06-09 16:13 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
In-Reply-To: <20260609161400.3661268-1-bruce.richardson@intel.com>
Similarly to how shell aliases work, allow specifying of short alias
commands for dpdk-telemetry.py script. The aliases are read from
"$HOME/.dpdk_telemetry_aliases" at startup.
Some examples of use from the docs. Alias file contents:
# Basic shortcuts
ls=/ethdev/list
names=FOREACH i /ethdev/list /ethdev/info,$i .name
q=quit
Alias use:
--> ls
{"/ethdev/list": [0, 1]}
--> names
[{"i": 0, "name": "0000:
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
v2: added support for providing an alias path on cmdline.
added summary printout of how many aliases were loaded.
---
doc/guides/howto/telemetry.rst | 35 ++++++++++++++
usertools/dpdk-telemetry.py | 85 ++++++++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index 4bf48c635e..bdefbdc6a6 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -130,6 +130,41 @@ and query information using the telemetry client python script.
- With loop variable: returns an array of objects containing the loop
variable field and requested value fields.
+ * Use command aliases.
+
+ The telemetry script can load aliases at startup from::
+
+ $HOME/.dpdk_telemetry_aliases
+
+ or from a custom path provided via the ``--alias-file`` script flag.
+ Each alias entry must be in ``alias=command`` format.
+ Empty lines and lines starting with ``#`` are ignored.
+
+ Example alias file::
+
+ # Basic shortcuts
+ ls=/ethdev/list
+ names=FOREACH i /ethdev/list /ethdev/info,$i .name
+ q=quit
+
+ Alias behavior is intentionally similar to shell aliases:
+
+ - The first token of the entered input is checked for an alias match.
+ - If matched, that first token is replaced with its expansion.
+ - Alias expansion is recursive (aliases can expand to other aliases).
+ - Expansion has a safety limit to prevent infinite loops.
+
+ Examples::
+
+ --> ls
+ {"/ethdev/list": [0, 1]}
+
+ --> names
+ [{"i": 0, "name": "0000:16:00.0"}, {"i": 1, "name": "0000:16:00.1"}]
+
+ --> q
+ # exits the client
+
Connecting to Different DPDK Processes
--------------------------------------
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 2de10cff69..7a25b78730 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -21,6 +21,76 @@
SOCKET_NAME = "dpdk_telemetry.{}".format(TELEMETRY_VERSION)
DEFAULT_PREFIX = "rte"
CMDS = []
+ALIASES = {}
+ALIAS_FILE = ".dpdk_telemetry_aliases"
+MAX_ALIAS_EXPANSIONS = 32
+
+
+def load_aliases(alias_path=None):
+ """Load aliases from $HOME/.dpdk_telemetry_aliases or a custom path if provided"""
+ aliases = {}
+ if alias_path and not os.path.isfile(alias_path):
+ print("Warning: alias file {} not found, skipping".format(alias_path), file=sys.stderr)
+ return aliases
+
+ if not alias_path:
+ home = os.environ.get("HOME")
+ if not home:
+ return aliases
+
+ alias_path = os.path.join(home, ALIAS_FILE)
+ if not os.path.isfile(alias_path):
+ return aliases
+
+ try:
+ with open(alias_path) as alias_file:
+ for line_num, line in enumerate(alias_file, start=1):
+ entry = line.strip()
+ if not entry or entry.startswith("#"):
+ continue
+ if "=" not in entry:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ name, command = entry.split("=", 1)
+ name = name.strip()
+ command = command.strip()
+ if not name or not command:
+ print(
+ "Warning: ignoring malformed alias at {}:{}".format(alias_path, line_num),
+ file=sys.stderr,
+ )
+ continue
+ aliases[name] = command
+ except OSError as e:
+ print("Warning: failed to read {}: {}".format(alias_path, e), file=sys.stderr)
+
+ print("Loaded {} aliases from {}".format(len(aliases), alias_path))
+ return aliases
+
+
+def expand_aliases(text, aliases):
+ """Expand aliases similarly to shell aliases on the first token"""
+ expanded = text
+ for _ in range(MAX_ALIAS_EXPANSIONS):
+ stripped = expanded.lstrip()
+ if not stripped:
+ return expanded
+
+ parts = stripped.split(None, 1)
+ first = parts[0]
+ rest = parts[1] if len(parts) > 1 else ""
+
+ if first not in aliases:
+ return expanded
+
+ alias_value = aliases[first]
+ expanded = "{} {}".format(alias_value, rest).strip() if rest else alias_value
+
+ print("Warning: alias expansion limit reached", file=sys.stderr)
+ return expanded
def send_command(sock, cmd, output_buf_len, echo=False, pretty=False):
@@ -262,10 +332,12 @@ def handle_socket(args, path):
# interactive prompt
try:
- text = input(prompt).strip()
- while text != "quit":
- handle_command(sock, output_buf_len, text, pretty=prompt)
+ while True:
text = input(prompt).strip()
+ expanded = expand_aliases(text, ALIASES)
+ if expanded == "quit":
+ break
+ handle_command(sock, output_buf_len, expanded, pretty=prompt)
except EOFError:
pass
finally:
@@ -274,7 +346,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + CMDS
+ all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
@@ -293,6 +365,10 @@ def readline_complete(text, state):
default=DEFAULT_PREFIX,
help="Provide file-prefix for DPDK runtime directory",
)
+parser.add_argument(
+ "--alias-file",
+ help=f"Provide a custom alias file instead of $HOME/{ALIAS_FILE}",
+)
parser.add_argument(
"-i", "--instance", default="0", type=int, help="Provide instance number for DPDK application"
)
@@ -304,6 +380,7 @@ def readline_complete(text, state):
help="List all possible file-prefixes and exit",
)
args = parser.parse_args()
+ALIASES = load_aliases(args.alias_file if args.alias_file else None)
if args.list:
list_fp()
sys.exit(0)
--
2.53.0
^ permalink raw reply related
* [PATCH v3 3/3] usertools/telemetry: add help support
From: Bruce Richardson @ 2026-06-09 16:14 UTC (permalink / raw)
To: dev; +Cc: fengchengwen, Bruce Richardson
In-Reply-To: <20260609161400.3661268-1-bruce.richardson@intel.com>
While the telemetry infrastructure supported using "/help,/<cmd>" to get
help on specific commands, with the addition of script-specific
commands, we needed better help support to explain the use of FOREACH,
and to allow e.g. "help /<cmd>" using space separation, which is more
intuitive. This patch adds that help support.
Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
v2: added "help aliases" to list defined aliases
updated docs for expanded help command
---
doc/guides/howto/telemetry.rst | 31 ++++++++++++---
usertools/dpdk-telemetry.py | 69 +++++++++++++++++++++++++++++++++-
2 files changed, 93 insertions(+), 7 deletions(-)
diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
index bdefbdc6a6..00cfc1a1e1 100644
--- a/doc/guides/howto/telemetry.rst
+++ b/doc/guides/howto/telemetry.rst
@@ -81,12 +81,31 @@ and query information using the telemetry client python script.
...
"tx_priority7_xon_to_xoff_packets": 0}}
- * Get the help text for a command. This will indicate what parameters are
- required. Pass the command as a parameter::
-
- --> /help,/ethdev/xstats
- {"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
- Parameters: int port_id"}}
+ * Get the help text for a command.
+ This will indicate what parameters are required.
+ Use the ``help`` keyword followed by the command or keyword of interest,
+ for example::
+
+ --> help FOREACH
+ FOREACH usage:
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+ Examples:
+ FOREACH /ethdev/list /ethdev/stats .opackets
+ ...
+
+ --> help /ethdev/xstats
+ {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
+
+ .. Note::
+ For commands starting with ``/`` that are telemetry enpoints,
+ the help text can also be obtained by sending the ``/help`` command to the telemetry socket.
+ In this case, the parameter must be separated by a comma, not a space.
+ For example::
+
+ --> /help,/ethdev/xstats
+ {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
* Run a compound query using ``FOREACH``.
diff --git a/usertools/dpdk-telemetry.py b/usertools/dpdk-telemetry.py
index 7a25b78730..20627b596b 100755
--- a/usertools/dpdk-telemetry.py
+++ b/usertools/dpdk-telemetry.py
@@ -25,6 +25,26 @@
ALIAS_FILE = ".dpdk_telemetry_aliases"
MAX_ALIAS_EXPANSIONS = 32
+BASIC_HELP_TEXT = """Basic usage:
+ /<command>[,<params>] Send a telemetry command to the app
+ FOREACH ... Run a compound query over list items
+ help Show this help
+ help /<command> Show app-provided help for a command
+ help FOREACH Show FOREACH usage and examples
+ quit Exit the client
+"""
+
+FOREACH_HELP_TEXT = """FOREACH usage:
+ FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
+ FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
+
+Examples:
+ FOREACH /ethdev/list /ethdev/stats .opackets
+ FOREACH /ethdev/list /ethdev/stats .ipackets .opackets
+ FOREACH i /ethdev/list /ethdev/info,$i .name
+ FOREACH i /ethdev/list /ethdev/stats,$i .ipackets .opackets
+"""
+
def load_aliases(alias_path=None):
"""Load aliases from $HOME/.dpdk_telemetry_aliases or a custom path if provided"""
@@ -209,10 +229,57 @@ def handle_foreach(sock, output_buf_len, text, pretty=False):
print(json.dumps(output, indent=indent))
+def command_exists(cmd):
+ """Check if a telemetry command exists in the command list"""
+ return cmd in CMDS
+
+
+def app_help_command_for(target_cmd):
+ """Build a '/help,<command>' query for app-side command help"""
+ if not target_cmd:
+ return None
+ normalized = target_cmd.strip()
+ if not normalized.startswith("/"):
+ return None
+ if not command_exists(normalized):
+ print("Unknown command for help: {}".format(normalized))
+ return None
+ return "/help,{}".format(normalized)
+
+
+def handle_user_help(sock, output_buf_len, text, pretty=False):
+ """Handle local 'help' command and command-specific help lookup"""
+ parts = text.split(None, 1)
+ if len(parts) == 1:
+ print(BASIC_HELP_TEXT, end="")
+ return
+
+ help_arg = parts[1].strip()
+ if help_arg.upper() == "FOREACH":
+ print(FOREACH_HELP_TEXT, end="")
+ return
+ elif help_arg.lower() == "alias" or help_arg.lower() == "aliases":
+ if not ALIASES:
+ print("No aliases defined")
+ return
+ print("Defined aliases:")
+ for name, command in ALIASES.items():
+ print(f" {name}='{command}'")
+ return
+
+ cmd = app_help_command_for(help_arg)
+ if cmd is None:
+ print("Usage: help [FOREACH|/<command>]")
+ return
+ send_command(sock, cmd, output_buf_len, echo=True, pretty=pretty)
+
+
def handle_command(sock, output_buf_len, text, pretty=False):
"""Execute a user command if recognized"""
if text.startswith("/"):
send_command(sock, text, output_buf_len, echo=True, pretty=pretty)
+ elif text == "help" or text.startswith("help "):
+ handle_user_help(sock, output_buf_len, text, pretty)
elif text.startswith("FOREACH "):
handle_foreach(sock, output_buf_len, text, pretty)
@@ -346,7 +413,7 @@ def handle_socket(args, path):
def readline_complete(text, state):
"""Find any matching commands from the list based on user input"""
- all_cmds = ["quit"] + list(ALIASES.keys()) + CMDS
+ all_cmds = ["quit", "help"] + list(ALIASES.keys()) + CMDS
if text:
matches = [c for c in all_cmds if c.startswith(text)]
else:
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v3 3/3] usertools/telemetry: add help support
From: Bruce Richardson @ 2026-06-09 16:17 UTC (permalink / raw)
To: dev; +Cc: fengchengwen
In-Reply-To: <20260609161400.3661268-4-bruce.richardson@intel.com>
On Tue, Jun 09, 2026 at 05:14:00PM +0100, Bruce Richardson wrote:
> While the telemetry infrastructure supported using "/help,/<cmd>" to get
> help on specific commands, with the addition of script-specific
> commands, we needed better help support to explain the use of FOREACH,
> and to allow e.g. "help /<cmd>" using space separation, which is more
> intuitive. This patch adds that help support.
>
> Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
> ---
> v2: added "help aliases" to list defined aliases
> updated docs for expanded help command
> ---
> doc/guides/howto/telemetry.rst | 31 ++++++++++++---
> usertools/dpdk-telemetry.py | 69 +++++++++++++++++++++++++++++++++-
> 2 files changed, 93 insertions(+), 7 deletions(-)
>
> diff --git a/doc/guides/howto/telemetry.rst b/doc/guides/howto/telemetry.rst
> index bdefbdc6a6..00cfc1a1e1 100644
> --- a/doc/guides/howto/telemetry.rst
> +++ b/doc/guides/howto/telemetry.rst
> @@ -81,12 +81,31 @@ and query information using the telemetry client python script.
> ...
> "tx_priority7_xon_to_xoff_packets": 0}}
>
> - * Get the help text for a command. This will indicate what parameters are
> - required. Pass the command as a parameter::
> -
> - --> /help,/ethdev/xstats
> - {"/help": {"/ethdev/xstats": "Returns the extended stats for a port.
> - Parameters: int port_id"}}
> + * Get the help text for a command.
> + This will indicate what parameters are required.
> + Use the ``help`` keyword followed by the command or keyword of interest,
> + for example::
> +
> + --> help FOREACH
> + FOREACH usage:
> + FOREACH /<list_cmd> /<iter_cmd> .<field> [.<field> ...]
> + FOREACH <var> /<list_cmd> /<iter_cmd_with_$var> .<field> [.<field> ...]
> +
> + Examples:
> + FOREACH /ethdev/list /ethdev/stats .opackets
> + ...
> +
> + --> help /ethdev/xstats
> + {"/help": {"/ethdev/xstats": "Returns the extended stats for a port. Parameters: int port_id"}}
> +
> + .. Note::
> + For commands starting with ``/`` that are telemetry enpoints,
Checkpatch in CI correctly points out the typo for "endpoints" here. Please
fix on apply if no v4 is otherwise necessary.
thanks,
/Bruce
^ permalink raw reply
* DPDK Release Status Meeting 2026-06-08
From: Mcnamara, John @ 2026-06-09 16:19 UTC (permalink / raw)
To: dev@dpdk.org; +Cc: Thomas Monjalon, David Marchand
[-- Attachment #1: Type: text/plain, Size: 3017 bytes --]
Release status meeting minutes 2026-06-08
=========================================
Agenda:
- Release Dates
- Subtrees
- Roadmaps
- LTS
- Defects
- Opens
Participants:
- Broadcom
- Intel
- Marvell
- Nvidia
- Red Hat
- Stephen Hemminger
Release Dates
-------------
The following are the proposed working dates for 27.03:
| Date | Milestone | Description |
|-----------------|------------------|---------------------------------|
| 30 April 2026 | RFC/v1 patches | Proposal deadline |
| 04 June 2026 | 26.07-rc1 | API freeze |
| 25 June 2026 | 26.07-rc2 | PMD features freeze |
| 02 July 2026 | 26.07-rc3 | Builtin apps features freeze |
| 9 July 2026 | 26.07-rc4 | Documentation ready |
| 16 July 2026 | 26.07.0 | Release |
See https://core.dpdk.org/roadmap/
Subtrees
--------
- next-net
- Has been pulled to main.
- 34 patches in review/waiting.
- next-net-intel
- Has been pulled to main.
- 3 others to merge.
- 9 patches in backlog.
- next-net-mlx
- Has been pulled to main.
- next-broadcom
- 20 patches ready to be merge.
- next-net-mvl
- Has been pulled to main.
- next-eventdev
- 1 patch to merge.
- next-baseband
- No update.
- next-virtio
- Has been pulled to main.
- next-crypto
- Applied patches for RC1. Waiting for merge.
- 16 patches in review/waiting.
- next-dts
- No update.
- main
- Bus refactoring series merged.
- Looking at fixes in EAL. Large patchset on atomic deprecations.
- Series on MAC addresses targeting RC2.
- Hash series merged.
- RC1 will probably be released 10 June 2026.
Other
-----
- None.
LTS
---
See also: https://core.dpdk.org/roadmap/#stable
LTS versions ongoing/released:
- 25.11.1 - Released + regression fix on 25.11.2.
- 24.11.5 - Released + regression fix on 24.11.6.
- 23.11.7 - Released.
Older releases:
- 20.11.10 - Will only be updated with CVE and critical fixes.
- 19.11.14 - Will only be updated with CVE and critical fixes.
- Distros
- Debian 13 contains DPDK v24.11
- Ubuntu 25.04 contains DPDK v24.11
- Ubuntu 24.04 LTS contains DPDK v23.11
- RHEL 9 contains DPDK 24.11
Defects
-------
- Bugzilla links, 'Bugs', added for hosted projects
- https://www.dpdk.org/hosted-projects/
DPDK Release Status Meetings
----------------------------
The DPDK Release Status Meeting is intended for DPDK Committers to discuss the
status of the main tree and sub-trees, and for project managers to track
progress or milestone dates.
The meeting occurs on every Tuesday at 14:30 DST over Jitsi on https://meet.jit.si/DPDK
You don't need an invite to join the meeting but if you want a calendar reminder just
send an email to "John McNamara <john.mcnamara@intel.com>" for the invite.
[-- Attachment #2: Type: text/html, Size: 20913 bytes --]
^ permalink raw reply
* [PATCH v2] net/iavf: fix to consolidate link change event handling
From: Anurag Mandal @ 2026-06-09 17:38 UTC (permalink / raw)
To: dev
Cc: bruce.richardson, vladimir.medvedkin, ciara.loftus, Anurag Mandal,
stable
In-Reply-To: <20260609001022.357509-1-anurag.mandal@intel.com>
Handled link-change events through a common static function that
reads the correct advanced & legacy link fields properly and
updates no-poll/watchdog/LSC state consistently.
Fixes: 5e03e316c753 ("net/iavf: handle virtchnl event message without interrupt")
Fixes: 48de41ca11f0 ("net/avf: enable link status update")
Cc: stable@dpdk.org
Signed-off-by: Anurag Mandal <anurag.mandal@intel.com>
---
V2: Addressed Ciara Loftus's review comments
- removed unnecessary NULL checks which were overly defensive checks
drivers/net/intel/iavf/iavf_vchnl.c | 125 ++++++++++++++++------------
1 file changed, 73 insertions(+), 52 deletions(-)
diff --git a/drivers/net/intel/iavf/iavf_vchnl.c b/drivers/net/intel/iavf/iavf_vchnl.c
index 94ccfb5d6e..31ce9e8987 100644
--- a/drivers/net/intel/iavf/iavf_vchnl.c
+++ b/drivers/net/intel/iavf/iavf_vchnl.c
@@ -216,6 +216,67 @@ iavf_convert_link_speed(enum virtchnl_link_speed virt_link_speed)
return speed;
}
+/*
+ * iavf_handle_link_change_event: common handler for VIRTCHNL link change events
+ *
+ * @dev: pointer to rte_eth_dev for this VF
+ * @vpe: pointer to the virtchnl_pf_event payload received from the PF
+ *
+ * Handle PF link-change event: decode adv/legacy link info, update VF
+ * link state, sync no-poll/watchdog behavior & notify app via LSC event.
+ */
+static void
+iavf_handle_link_change_event(struct rte_eth_dev *dev,
+ struct virtchnl_pf_event *vpe)
+{
+ struct iavf_adapter *adapter =
+ IAVF_DEV_PRIVATE_TO_ADAPTER(dev->data->dev_private);
+ struct iavf_info *vf = &adapter->vf;
+ bool adv_link_speed;
+
+ adv_link_speed = (vf->vf_res != NULL) &&
+ ((vf->vf_res->vf_cap_flags & VIRTCHNL_VF_CAP_ADV_LINK_SPEED) != 0);
+
+ if (adv_link_speed) {
+ vf->link_up = vpe->event_data.link_event_adv.link_status;
+ vf->link_speed = vpe->event_data.link_event_adv.link_speed;
+ } else {
+ enum virtchnl_link_speed speed;
+
+ vf->link_up = vpe->event_data.link_event.link_status;
+ speed = vpe->event_data.link_event.link_speed;
+ vf->link_speed = iavf_convert_link_speed(speed);
+ }
+
+ iavf_dev_link_update(dev, 0);
+
+ /*
+ * Update watchdog/no_poll state BEFORE notifying the application via
+ * the LSC event. Otherwise the application's link-up callback could
+ * race with stale (link-down) no_poll/watchdog state and either
+ * continue to drop traffic or trigger a spurious reset detection.
+ *
+ * Keeping the watchdog enabled whenever the link cannot be trusted
+ * (link is down or a VF reset is in progress); the watchdog drives
+ * auto-reset recovery, so it must remain armed in those cases.
+ */
+ if (vf->link_up && !vf->vf_reset)
+ iavf_dev_watchdog_disable(adapter);
+ else
+ iavf_dev_watchdog_enable(adapter);
+
+ if (adapter->devargs.no_poll_on_link_down) {
+ iavf_set_no_poll(adapter, true);
+ PMD_DRV_LOG(DEBUG, "VF no poll turned %s",
+ adapter->no_poll ? "on" : "off");
+ }
+
+ iavf_dev_event_post(dev, RTE_ETH_EVENT_INTR_LSC, NULL, 0);
+
+ PMD_DRV_LOG(INFO, "Link status update:%s",
+ vf->link_up ? "up" : "down");
+}
+
/* Read data in admin queue to get msg from pf driver */
static enum iavf_aq_result
iavf_read_msg_from_pf(struct iavf_adapter *adapter, uint16_t buf_len,
@@ -249,38 +310,15 @@ iavf_read_msg_from_pf(struct iavf_adapter *adapter, uint16_t buf_len,
if (opcode == VIRTCHNL_OP_EVENT) {
struct virtchnl_pf_event *vpe =
(struct virtchnl_pf_event *)event.msg_buf;
+ if (vpe == NULL) {
+ PMD_DRV_LOG(ERR, "Invalid PF event message");
+ return IAVF_MSG_ERR;
+ }
result = IAVF_MSG_SYS;
switch (vpe->event) {
case VIRTCHNL_EVENT_LINK_CHANGE:
- vf->link_up =
- vpe->event_data.link_event.link_status;
- if (vf->vf_res != NULL &&
- vf->vf_res->vf_cap_flags & VIRTCHNL_VF_CAP_ADV_LINK_SPEED) {
- vf->link_speed =
- vpe->event_data.link_event_adv.link_speed;
- } else {
- enum virtchnl_link_speed speed;
- speed = vpe->event_data.link_event.link_speed;
- vf->link_speed = iavf_convert_link_speed(speed);
- }
- iavf_dev_link_update(vf->eth_dev, 0);
- iavf_dev_event_post(vf->eth_dev, RTE_ETH_EVENT_INTR_LSC, NULL, 0);
- if (vf->link_up && !vf->vf_reset) {
- iavf_dev_watchdog_disable(adapter);
- } else {
- if (!vf->link_up)
- iavf_dev_watchdog_enable(adapter);
- }
- if (adapter->devargs.no_poll_on_link_down) {
- iavf_set_no_poll(adapter, true);
- if (adapter->no_poll)
- PMD_DRV_LOG(DEBUG, "VF no poll turned on");
- else
- PMD_DRV_LOG(DEBUG, "VF no poll turned off");
- }
- PMD_DRV_LOG(INFO, "Link status update:%s",
- vf->link_up ? "up" : "down");
+ iavf_handle_link_change_event(vf->eth_dev, vpe);
break;
case VIRTCHNL_EVENT_RESET_IMPENDING:
vf->vf_reset = true;
@@ -505,6 +543,12 @@ iavf_handle_pf_event_msg(struct rte_eth_dev *dev, uint8_t *msg,
PMD_DRV_LOG(DEBUG, "Error event");
return;
}
+
+ if (pf_msg == NULL) {
+ PMD_DRV_LOG(ERR, "Invalid PF event message");
+ return;
+ }
+
switch (pf_msg->event) {
case VIRTCHNL_EVENT_RESET_IMPENDING:
PMD_DRV_LOG(DEBUG, "VIRTCHNL_EVENT_RESET_IMPENDING event");
@@ -518,30 +562,7 @@ iavf_handle_pf_event_msg(struct rte_eth_dev *dev, uint8_t *msg,
break;
case VIRTCHNL_EVENT_LINK_CHANGE:
PMD_DRV_LOG(DEBUG, "VIRTCHNL_EVENT_LINK_CHANGE event");
- vf->link_up = pf_msg->event_data.link_event.link_status;
- if (vf->vf_res->vf_cap_flags & VIRTCHNL_VF_CAP_ADV_LINK_SPEED) {
- vf->link_speed =
- pf_msg->event_data.link_event_adv.link_speed;
- } else {
- enum virtchnl_link_speed speed;
- speed = pf_msg->event_data.link_event.link_speed;
- vf->link_speed = iavf_convert_link_speed(speed);
- }
- iavf_dev_link_update(dev, 0);
- if (vf->link_up && !vf->vf_reset) {
- iavf_dev_watchdog_disable(adapter);
- } else {
- if (!vf->link_up)
- iavf_dev_watchdog_enable(adapter);
- }
- if (adapter->devargs.no_poll_on_link_down) {
- iavf_set_no_poll(adapter, true);
- if (adapter->no_poll)
- PMD_DRV_LOG(DEBUG, "VF no poll turned on");
- else
- PMD_DRV_LOG(DEBUG, "VF no poll turned off");
- }
- iavf_dev_event_post(dev, RTE_ETH_EVENT_INTR_LSC, NULL, 0);
+ iavf_handle_link_change_event(dev, pf_msg);
break;
case VIRTCHNL_EVENT_PF_DRIVER_CLOSE:
PMD_DRV_LOG(DEBUG, "VIRTCHNL_EVENT_PF_DRIVER_CLOSE event");
--
2.34.1
^ permalink raw reply related
* RE: [PATCH] net/iavf: fix to consolidate link change event handling
From: Mandal, Anurag @ 2026-06-09 17:42 UTC (permalink / raw)
To: Mandal, Anurag, Loftus, Ciara, dev@dpdk.org
Cc: Richardson, Bruce, Medvedkin, Vladimir, stable@dpdk.org
In-Reply-To: <CY5PR11MB6116C94D51C347DEAD15108DE41D2@CY5PR11MB6116.namprd11.prod.outlook.com>
> -----Original Message-----
> From: Mandal, Anurag <anurag.mandal@intel.com>
> Sent: 09 June 2026 16:24
> To: Loftus, Ciara <ciara.loftus@intel.com>; dev@dpdk.org
> Cc: Richardson, Bruce <bruce.richardson@intel.com>; Medvedkin, Vladimir
> <vladimir.medvedkin@intel.com>; stable@dpdk.org
> Subject: RE: [PATCH] net/iavf: fix to consolidate link change event handling
>
>
> > -----Original Message-----
> > From: Loftus, Ciara <ciara.loftus@intel.com>
> > Sent: 09 June 2026 15:32
> > To: Mandal, Anurag <anurag.mandal@intel.com>; dev@dpdk.org
> > Cc: Richardson, Bruce <bruce.richardson@intel.com>; Medvedkin,
> > Vladimir <vladimir.medvedkin@intel.com>; stable@dpdk.org
> > Subject: RE: [PATCH] net/iavf: fix to consolidate link change event
> > handling
> >
> > > Subject: [PATCH] net/iavf: fix to consolidate link change event
> > > handling
> > >
> > > Handled link-change events through a common static function that
> > > reads the correct advanced & legacy link fields properly and updates
> > > no-poll/watchdog/LSC state consistently.
> > >
> > > Fixes: 5e03e316c753 ("net/iavf: handle virtchnl event message
> > > without
> > > interrupt")
> > > Fixes: 48de41ca11f0 ("net/avf: enable link status update")
> > > Cc: stable@dpdk.org
> > >
> > > Signed-off-by: Anurag Mandal <anurag.mandal@intel.com>
> > > ---
> > > drivers/net/intel/iavf/iavf_vchnl.c | 133
> > > +++++++++++++++++-----------
> > > 1 file changed, 81 insertions(+), 52 deletions(-)
> > >
> > > diff --git a/drivers/net/intel/iavf/iavf_vchnl.c
> > > b/drivers/net/intel/iavf/iavf_vchnl.c
> > > index 94ccfb5d6e..6454632541 100644
> > > --- a/drivers/net/intel/iavf/iavf_vchnl.c
> > > +++ b/drivers/net/intel/iavf/iavf_vchnl.c
> > > @@ -216,6 +216,75 @@ iavf_convert_link_speed(enum
> > > virtchnl_link_speed
> > > virt_link_speed)
> > > return speed;
> > > }
> > >
> > > +/*
> > > + * iavf_handle_link_change_event: common handler for VIRTCHNL link
> > > change events
> > > + *
> > > + * @dev: pointer to rte_eth_dev for this VF
> > > + * @vpe: pointer to the virtchnl_pf_event payload received from the
> > > +PF
> > > + *
> > > + * Handle PF link-change event: decode adv/legacy link info, update
> > > +VF
> > > + * link state, sync no-poll/watchdog behavior & notify app via LSC event.
> > > + */
> > > +static void
> > > +iavf_handle_link_change_event(struct rte_eth_dev *dev,
> > > + struct virtchnl_pf_event *vpe) {
> > > + struct iavf_adapter *adapter;
> > > + struct iavf_info *vf;
> > > + bool adv_link_speed;
> > > +
> > > + if (dev == NULL || dev->data == NULL ||
> > > + dev->data->dev_private == NULL || vpe == NULL) {
> > > + PMD_DRV_LOG(ERR, "Invalid device pointer in link change
> > > handler");
> > > + return;
> > > + }
> >
> > Thanks for splitting this out into a standalone patch.
> > I'm not sure if all of the above NULL checking is necessary.
> > Especially vpe == NULL considering you've checked that for NULL in
> > both iavf_read_msg_from_pf and iavf_handle_pf_event_msg before
> > entering this function. It's recommended to avoid overly defensive
> > checks that can never trigger so I suggest taking a look at this again. Other
> than that the changes looks good to me.
>
> Hi Ciara,
>
> Thanks for the review. Initially I also did not add. Then I added it as per
> suggestions from ai-code-review run by DPDK. Funny thing is now, it is saying "
> overly defensive checks". So not sure, which suggestion of ai-cdoe-review to
> follow.
> Please guide. Will act accordingly.
>
> Thanks,
> Anuarg
Hi Ciara,
I have removed those overly defensive NULL checks and sent v2 for the review.
Thanks,
Anurag
>
> >
> > > +
> > > + adapter = IAVF_DEV_PRIVATE_TO_ADAPTER(dev->data-
> > > >dev_private);
> > > + vf = &adapter->vf;
> > > +
> > > + adv_link_speed = (vf->vf_res != NULL) &&
> > > + ((vf->vf_res->vf_cap_flags &
> > > VIRTCHNL_VF_CAP_ADV_LINK_SPEED) != 0);
> > > +
^ permalink raw reply
* [RFC] devtools: add tool calling support to review-patch.py
From: Aaron Conole @ 2026-06-09 18:26 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger, David Marchand
Add an iterative tool-use loop to review-patch.py for the Anthropic
and OpenAI providers. The reviewer can now look up additional context
from the DPDK source tree when the patch alone is insufficient,
rather than having to guess at surrounding code, API contracts, or
function signatures.
Tool calling is enabled by default with a limit of 10 rounds. Pass
'--tool-rounds 0' to disable it and restore the previous single-shot
behavior. The round limit prevents runaway cost on large patches
that when reached will force the model to deliver a final judgement.
Initial tool set:
- grep Searches for regex across the file system with
optional path restrictions and case-insensitive
matches.
- file_read Line range read of a specific path.
Both tools are limited to the repository root to prevent path
traversal. Path outputs are relative to the repo root.
The system prompt is extended when tool calling is active to
encourage the model to use tools only when genuinely needed,
keeping unnecessary round trips and token costs under control
and to a minimum.
Internally, _common.py gains send_request_raw() (returning the
raw response dict) so the tool-calling loops can inspect
stop_reason / finish_reason before extracting text.
Signed-off-by: Aaron Conole <aconole@redhat.com>
---
devtools/ai/_common.py | 66 ++++-
devtools/ai/review-patch.py | 552 +++++++++++++++++++++++++++++++++++-
2 files changed, 602 insertions(+), 16 deletions(-)
diff --git a/devtools/ai/_common.py b/devtools/ai/_common.py
index 69982cbda5..e9fb25557b 100644
--- a/devtools/ai/_common.py
+++ b/devtools/ai/_common.py
@@ -121,7 +121,8 @@ def add_token_args(parser: argparse.ArgumentParser) -> None:
def print_token_summary(
usage: TokenUsage, provider: str, model: str, show: bool
) -> None:
- """Print token usage summary to stderr if requested and any calls were made."""
+ """Print token usage summary to stderr if requested and any calls were
+ made."""
if not show or usage.api_calls == 0:
return
print("", file=sys.stderr)
@@ -173,13 +174,15 @@ def _extract_usage(provider: str, result: dict[str, Any]) -> TokenUsage:
def _extract_text(provider: str, result: dict[str, Any]) -> str:
- """Extract response text from a provider response. Calls error() on failure."""
+ """Extract response text from a provider response. Calls error() on
+ failure."""
if "error" in result:
error(f"API error: {result['error'].get('message', result)}")
if provider == "anthropic":
content = result.get("content", [])
return "".join(
- block.get("text", "") for block in content if block.get("type") == "text"
+ block.get("text", "") for block in content
+ if block.get("type") == "text"
)
if provider == "google":
candidates = result.get("candidates", [])
@@ -200,13 +203,14 @@ def _print_verbose_usage(usage: TokenUsage) -> None:
print(f"Input tokens: {usage.input_tokens:,}", file=sys.stderr)
print(f"Output tokens: {usage.output_tokens:,}", file=sys.stderr)
if usage.cache_creation_tokens:
- print(f"Cache creation: {usage.cache_creation_tokens:,}", file=sys.stderr)
+ print(f"Cache creation: {usage.cache_creation_tokens:,}",
+ file=sys.stderr)
if usage.cache_read_tokens:
print(f"Cache read: {usage.cache_read_tokens:,}", file=sys.stderr)
print("===================", file=sys.stderr)
-def send_request(
+def _send_http_raw(
provider: str,
api_key: str,
model: str,
@@ -214,13 +218,9 @@ def send_request(
*,
timeout: int = 120,
verbose: bool = False,
-) -> tuple[str, TokenUsage]:
- """Send a prebuilt request to a provider and return (response_text, usage).
-
- The caller assembles the provider-specific request body via its own
- build_*_request helpers (the prompts differ per script). This function
- handles transport, error reporting, and token-usage extraction.
- """
+) -> tuple[dict[str, Any], TokenUsage]:
+ """Shared HTTP transport layer. Returns (response_dict, usage). Calls
+ error() on failure."""
url, headers = _build_request_meta(provider, api_key, model)
body = json.dumps(request_data).encode("utf-8")
req = Request(url, data=body, headers=headers)
@@ -243,4 +243,46 @@ def send_request(
usage = _extract_usage(provider, result)
if verbose:
_print_verbose_usage(usage)
+ return result, usage
+
+
+def send_request(
+ provider: str,
+ api_key: str,
+ model: str,
+ request_data: dict[str, Any],
+ *,
+ timeout: int = 120,
+ verbose: bool = False,
+) -> tuple[str, TokenUsage]:
+ """Send a prebuilt request to a provider and return (response_text, usage).
+
+ The caller assembles the provider-specific request body via its own
+ build_*_request helpers (the prompts differ per script). This function
+ handles transport, error reporting, and token-usage extraction.
+ """
+ result, usage = _send_http_raw(
+ provider, api_key, model, request_data, timeout=timeout,
+ verbose=verbose
+ )
return _extract_text(provider, result), usage
+
+
+def send_request_raw(
+ provider: str,
+ api_key: str,
+ model: str,
+ request_data: dict[str, Any],
+ *,
+ timeout: int = 120,
+ verbose: bool = False,
+) -> tuple[dict[str, Any], TokenUsage]:
+ """Send a prebuilt request and return the raw response dict plus usage.
+
+ Used by tool-calling loops that need to inspect stop_reason / finish_reason
+ before extracting text.
+ """
+ return _send_http_raw(
+ provider, api_key, model, request_data, timeout=timeout,
+ verbose=verbose
+ )
diff --git a/devtools/ai/review-patch.py b/devtools/ai/review-patch.py
index 52601ac156..18ed445afe 100755
--- a/devtools/ai/review-patch.py
+++ b/devtools/ai/review-patch.py
@@ -29,6 +29,7 @@
list_providers,
print_token_summary,
send_request,
+ send_request_raw,
)
# Output formats
@@ -114,6 +115,152 @@
--- PATCH CONTENT ---
"""
+TOOL_PROMPT_EXTENSION = """\
+Use tools to gather context that improves the review. Specifically:
+
+- New files or scripts: use grep to find similar existing files and compare \
+structure, naming conventions, and patterns (e.g. for a new CI script, check \
+other scripts under .ci/).
+- Modified or called functions: use grep to find their declaration and \
+file_read to inspect the header or implementation.
+- New symbols, macros, or config keys: use grep to check whether similar names \
+already exist and whether naming conventions are consistent.
+- MAINTAINERS or documentation changes: use file_read to verify the surrounding \
+context is consistent.
+
+Each tool call costs tokens, so skip lookups that clearly add no value. But \
+when in doubt about an existing pattern or convention, look it up."""
+
+TOOLS_ANTHROPIC: list[dict] = [
+ {
+ "name": "grep",
+ "description": (
+ "Search the DPDK source tree for a pattern. Returns matching lines "
+ "with file paths and line numbers. Use to find API definitions, "
+ "usage examples, or code referenced by the patch."
+ ),
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Regular expression or literal string to "
+ "search for",
+ },
+ "path": {
+ "type": "string",
+ "description": (
+ "Directory or file path to search, relative to the "
+ "repo root. Defaults to '.' (entire tree)."
+ ),
+ },
+ "case_insensitive": {
+ "type": "boolean",
+ "description": "Ignore case when matching (default: false)",
+ },
+ },
+ "required": ["pattern"],
+ },
+ },
+ {
+ "name": "file_read",
+ "description": (
+ "Read lines from a file in the DPDK source tree. "
+ "Use to inspect headers, existing implementations, or files "
+ "referenced in the patch."
+ ),
+ "input_schema": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "File path relative to the repository root",
+ },
+ "offset": {
+ "type": "integer",
+ "description": "First line to return, 1-indexed "
+ "(default: 1)",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum lines to return (default: 100, "
+ "max: 500)",
+ },
+ },
+ "required": ["path"],
+ },
+ },
+]
+
+TOOLS_OPENAI: list[dict] = [
+ {
+ "type": "function",
+ "function": {
+ "name": "grep",
+ "description": (
+ "Search the DPDK source tree for a pattern. Returns matching "
+ "lines with file paths and line numbers. Use to find API "
+ "definitions, usage examples, or code referenced by the patch."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Regular expression or literal string "
+ "to search for",
+ },
+ "path": {
+ "type": "string",
+ "description": (
+ "Directory or file path to search, relative to the "
+ "repo root. Defaults to '.' (entire tree)."
+ ),
+ },
+ "case_insensitive": {
+ "type": "boolean",
+ "description": "Ignore case when matching (default: "
+ "false)",
+ },
+ },
+ "required": ["pattern"],
+ },
+ },
+ },
+ {
+ "type": "function",
+ "function": {
+ "name": "file_read",
+ "description": (
+ "Read lines from a file in the DPDK source tree. "
+ "Use to inspect headers, existing implementations, or files "
+ "referenced in the patch."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "File path relative to the repository "
+ "root",
+ },
+ "offset": {
+ "type": "integer",
+ "description": "First line to return, 1-indexed "
+ "(default: 1)",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Maximum lines to return (default: 100, "
+ "max: 500)",
+ },
+ },
+ "required": ["path"],
+ },
+ },
+ },
+]
+
# Exit codes for review results
EXIT_CLEAN = 0
EXIT_WARNINGS = 2
@@ -158,9 +305,8 @@ def classify_review(review_text: str, output_format: str) -> int:
r"^<h[1-3]>\s*error", stripped
):
has_errors = True
- elif re.match(r"^(#{1,3}\s+)?(\*{0,2})warning", stripped) or re.match(
- r"^<h[1-3]>\s*warning", stripped
- ):
+ elif re.match(r"^(#{1,3}\s+)?(\*{0,2})warning", stripped) or \
+ re.match(r"^<h[1-3]>\s*warning", stripped):
has_warnings = True
if has_errors:
@@ -551,6 +697,340 @@ def build_google_request(
}
+def get_repo_root() -> str:
+ """Return the git repository root, falling back to cwd."""
+ try:
+ result = subprocess.run(
+ ["git", "rev-parse", "--show-toplevel"],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ return result.stdout.strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return os.getcwd()
+
+
+def _tool_grep(tool_input: dict[str, Any], repo_root: str) -> str:
+ """Execute a grep tool call against the repository."""
+ pattern = tool_input.get("pattern", "")
+ rel_path = tool_input.get("path", ".")
+ case_insensitive = tool_input.get("case_insensitive", False)
+
+ repo_resolved = Path(repo_root).resolve()
+ search_path = (repo_resolved / rel_path).resolve()
+ if not str(search_path).startswith(str(repo_resolved)):
+ return "Error: path is outside the repository"
+ if not search_path.exists():
+ return f"Error: path not found: {rel_path}"
+
+ cmd = ["grep", "-nH"]
+ if case_insensitive:
+ cmd.append("-i")
+ if search_path.is_dir():
+ cmd.extend(
+ [
+ "-r",
+ "--include=*.[ch]",
+ "--include=*.py",
+ "--include=*.rst",
+ "--include=*.ini",
+ ]
+ )
+ cmd.extend(["--", pattern, str(search_path)])
+
+ try:
+ proc = subprocess.run(
+ cmd, capture_output=True, text=True, timeout=30, errors="replace"
+ )
+ output = proc.stdout
+ if not output:
+ return "No matches found."
+ # Make paths relative to repo root for readability
+ prefix = str(repo_resolved) + "/"
+ output = output.replace(prefix, "")
+ lines = output.splitlines()
+ if len(lines) > 100:
+ truncated = "\n".join(lines[:100])
+ return f"{truncated}\n... ({len(lines) - 100} more lines truncated)"
+ return output.rstrip()
+ except subprocess.TimeoutExpired:
+ return "Error: grep timed out after 30 seconds"
+ except Exception as e:
+ return f"Error: grep failed: {e}"
+
+
+def _tool_file_read(tool_input: dict[str, Any], repo_root: str) -> str:
+ """Execute a file_read tool call against the repository."""
+ rel_path = tool_input.get("path", "")
+ offset = max(1, int(tool_input.get("offset", 1)))
+ limit = min(500, max(1, int(tool_input.get("limit", 100))))
+
+ repo_resolved = Path(repo_root).resolve()
+ file_path = (repo_resolved / rel_path).resolve()
+ if not str(file_path).startswith(str(repo_resolved)):
+ return "Error: path is outside the repository"
+ if not file_path.exists():
+ return f"Error: file not found: {rel_path}"
+ if not file_path.is_file():
+ return f"Error: not a file: {rel_path}"
+
+ try:
+ content = file_path.read_text(encoding="utf-8", errors="replace")
+ lines = content.splitlines()
+ total = len(lines)
+ start = offset - 1 # convert to 0-indexed
+ end = start + limit
+ selected = lines[start:end]
+ numbered = "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
+ if end < total:
+ numbered += f"\n... ({total - end} more lines; use offset={end + 1} to continue)"
+ return numbered
+ except Exception as e:
+ return f"Error reading file: {e}"
+
+
+def execute_tool(name: str, tool_input: dict[str, Any], repo_root: str) -> str:
+ """Dispatch a tool call by name and return the result string."""
+ if name == "grep":
+ return _tool_grep(tool_input, repo_root)
+ if name == "file_read":
+ return _tool_file_read(tool_input, repo_root)
+ return f"Error: unknown tool '{name}'"
+
+
+def call_api_with_tools_anthropic(
+ api_key: str,
+ model: str,
+ max_tokens: int,
+ system_prompt: str,
+ agents_content: str,
+ patch_content: str,
+ patch_name: str,
+ output_format: str,
+ verbose: bool,
+ timeout: int,
+ max_tool_rounds: int,
+ repo_root: str,
+) -> tuple[str, TokenUsage]:
+ """Anthropic API call with an iterative tool-use loop."""
+ format_instruction = FORMAT_INSTRUCTIONS.get(output_format, "")
+ user_prompt = USER_PROMPT.format(
+ patch_name=patch_name,
+ format_instruction=format_instruction + "\n\n" + TOOL_PROMPT_EXTENSION,
+ )
+
+ system: list[dict[str, Any]] = [
+ {"type": "text", "text": system_prompt},
+ {
+ "type": "text",
+ "text": agents_content,
+ "cache_control": {"type": "ephemeral"},
+ },
+ ]
+ messages: list[dict[str, Any]] = [
+ {"role": "user", "content": user_prompt + patch_content}
+ ]
+ total_usage = TokenUsage()
+
+ for _ in range(max_tool_rounds):
+ request_data: dict[str, Any] = {
+ "model": model,
+ "max_tokens": max_tokens,
+ "system": system,
+ "messages": messages,
+ "tools": TOOLS_ANTHROPIC,
+ }
+ api_result, usage = send_request_raw(
+ "anthropic", api_key, model, request_data, timeout=timeout, verbose=verbose
+ )
+ total_usage.add(usage)
+
+ stop_reason = api_result.get("stop_reason", "end_turn")
+ content_blocks = api_result.get("content", [])
+
+ if stop_reason != "tool_use":
+ text = "".join(
+ b.get("text", "") for b in content_blocks if b.get("type") == "text"
+ )
+ return text, total_usage
+
+ tool_use_blocks = [b for b in content_blocks if b.get("type") == "tool_use"]
+ if verbose:
+ for b in tool_use_blocks:
+ args_str = json.dumps(b.get("input", {}), separators=(",", ":"))
+ print(f"Tool call: {b['name']}({args_str})", file=sys.stderr)
+
+ messages.append({"role": "assistant", "content": content_blocks})
+ tool_results = [
+ {
+ "type": "tool_result",
+ "tool_use_id": b["id"],
+ "content": execute_tool(b["name"], b.get("input", {}), repo_root),
+ }
+ for b in tool_use_blocks
+ ]
+ messages.append({"role": "user", "content": tool_results})
+
+ # Exhausted rounds — append a text instruction to the last user message so
+ # the model understands it must switch from tool-calling to text-generation,
+ # then send with tool_choice:none to prevent further tool use.
+ if verbose:
+ print(
+ f"Tool round limit ({max_tool_rounds}) reached, forcing final judgment",
+ file=sys.stderr,
+ )
+ judgment_text = (
+ "You have reached the maximum number of tool call rounds. "
+ "Do not call any more tools. Based on all information gathered, "
+ "provide your complete final review now."
+ )
+ if messages and messages[-1].get("role") == "user":
+ last_content = messages[-1].get("content", [])
+ if isinstance(last_content, list):
+ messages[-1] = {
+ "role": "user",
+ "content": last_content + [{"type": "text", "text": judgment_text}],
+ }
+ request_data = {
+ "model": model,
+ "max_tokens": max_tokens,
+ "system": system,
+ "messages": messages,
+ "tools": TOOLS_ANTHROPIC,
+ "tool_choice": {"type": "none"},
+ }
+ api_result, usage = send_request_raw(
+ "anthropic", api_key, model, request_data, timeout=timeout, verbose=verbose
+ )
+ total_usage.add(usage)
+ content_blocks = api_result.get("content", [])
+ text = "".join(
+ b.get("text", "") for b in content_blocks if b.get("type") == "text"
+ )
+ if not text:
+ text = "(Review incomplete: tool call limit reached without a final response.)"
+ return text, total_usage
+
+
+def call_api_with_tools_openai(
+ api_key: str,
+ model: str,
+ max_tokens: int,
+ system_prompt: str,
+ agents_content: str,
+ patch_content: str,
+ patch_name: str,
+ output_format: str,
+ verbose: bool,
+ timeout: int,
+ max_tool_rounds: int,
+ repo_root: str,
+) -> tuple[str, TokenUsage]:
+ """OpenAI API call with an iterative tool-use loop."""
+ format_instruction = FORMAT_INSTRUCTIONS.get(output_format, "")
+ user_prompt = USER_PROMPT.format(
+ patch_name=patch_name,
+ format_instruction=format_instruction + "\n\n" + TOOL_PROMPT_EXTENSION,
+ )
+
+ messages: list[dict[str, Any]] = [
+ {"role": "system", "content": system_prompt},
+ {"role": "system", "content": agents_content},
+ {"role": "user", "content": user_prompt + patch_content},
+ ]
+ total_usage = TokenUsage()
+
+ for _ in range(max_tool_rounds):
+ request_data: dict[str, Any] = {
+ "model": model,
+ "max_tokens": max_tokens,
+ "messages": messages,
+ "tools": TOOLS_OPENAI,
+ }
+ api_result, usage = send_request_raw(
+ "openai", api_key, model, request_data, timeout=timeout, verbose=verbose
+ )
+ total_usage.add(usage)
+
+ choices = api_result.get("choices", [])
+ if not choices:
+ return "", total_usage
+
+ choice = choices[0]
+ finish_reason = choice.get("finish_reason", "stop")
+ message = choice.get("message", {})
+
+ if finish_reason != "tool_calls":
+ return message.get("content") or "", total_usage
+
+ tool_calls = message.get("tool_calls", [])
+ if verbose:
+ for tc in tool_calls:
+ fn = tc.get("function", {})
+ print(f"Tool call: {fn.get('name')}({fn.get('arguments', '')})", file=sys.stderr)
+
+ messages.append(
+ {
+ "role": "assistant",
+ "content": message.get("content"),
+ "tool_calls": tool_calls,
+ }
+ )
+
+ for tc in tool_calls:
+ fn = tc.get("function", {})
+ tool_name = fn.get("name", "")
+ try:
+ tool_input = json.loads(fn.get("arguments", "{}"))
+ except json.JSONDecodeError:
+ tool_input = {}
+ result_text = execute_tool(tool_name, tool_input, repo_root)
+ messages.append(
+ {
+ "role": "tool",
+ "tool_call_id": tc["id"],
+ "content": result_text,
+ }
+ )
+
+ # Exhausted rounds — add a user message directing the model to stop calling
+ # tools and deliver its final review, then send with tool_choice:none.
+ if verbose:
+ print(
+ f"Tool round limit ({max_tool_rounds}) reached, forcing final judgment",
+ file=sys.stderr,
+ )
+ messages.append(
+ {
+ "role": "user",
+ "content": (
+ "You have reached the maximum number of tool call rounds. "
+ "Do not call any more tools. Based on all information gathered, "
+ "provide your complete final review now."
+ ),
+ }
+ )
+ request_data = {
+ "model": model,
+ "max_tokens": max_tokens,
+ "messages": messages,
+ "tools": TOOLS_OPENAI,
+ "tool_choice": "none",
+ }
+ api_result, usage = send_request_raw(
+ "openai", api_key, model, request_data, timeout=timeout, verbose=verbose
+ )
+ total_usage.add(usage)
+ choices = api_result.get("choices", [])
+ if not choices:
+ return "(Review incomplete: tool call limit reached without a final response.)", total_usage
+ text = choices[0].get("message", {}).get("content") or ""
+ if not text:
+ text = "(Review incomplete: tool call limit reached without a final response.)"
+ return text, total_usage
+
+
def call_api(
provider: str,
api_key: str,
@@ -563,8 +1043,45 @@ def call_api(
output_format: str = "text",
verbose: bool = False,
timeout: int = 300,
+ max_tool_rounds: int = 0,
+ repo_root: str = "",
) -> tuple[str, TokenUsage]:
- """Build the per-provider request body and dispatch via _common."""
+ """Build the per-provider request body and dispatch via _common.
+
+ When max_tool_rounds > 0 and the provider is anthropic or openai, runs an
+ iterative tool-use loop before returning the final review text.
+ """
+ if max_tool_rounds > 0 and provider == "anthropic":
+ return call_api_with_tools_anthropic(
+ api_key,
+ model,
+ max_tokens,
+ system_prompt,
+ agents_content,
+ patch_content,
+ patch_name,
+ output_format,
+ verbose,
+ timeout,
+ max_tool_rounds,
+ repo_root,
+ )
+ if max_tool_rounds > 0 and provider == "openai":
+ return call_api_with_tools_openai(
+ api_key,
+ model,
+ max_tokens,
+ system_prompt,
+ agents_content,
+ patch_content,
+ patch_name,
+ output_format,
+ verbose,
+ timeout,
+ max_tool_rounds,
+ repo_root,
+ )
+
if provider == "anthropic":
request_data = build_anthropic_request(
model,
@@ -768,6 +1285,11 @@ def main() -> None:
stricter review rules: bug fixes only, no new features or APIs.
Any DPDK release with minor version .11 is an LTS release.
+Tool Calling (Anthropic and OpenAI only):
+ By default, the reviewer can call grep and file_read tools to look up
+ additional context from the source tree (up to 10 rounds). Use
+ --tool-rounds to change the limit or pass 0 to disable tool use.
+
Token Usage:
Use --show-tokens (or -v/--verbose) to print a token usage summary
on stderr after the run. Off by default.
@@ -840,6 +1362,14 @@ def main() -> None:
metavar="SECONDS",
help="API request timeout in seconds (default: 300)",
)
+ parser.add_argument(
+ "--tool-rounds",
+ type=int,
+ default=10,
+ metavar="N",
+ help="Max tool call rounds for Anthropic/OpenAI providers "
+ "(default: 10, 0 to disable tool calling)",
+ )
# Date and release options
parser.add_argument(
@@ -971,6 +1501,9 @@ def main() -> None:
patch_content = patch_path.read_text(encoding="utf-8", errors="replace")
patch_name = patch_path.name
+ # Repo root is used by tool calls (grep, file_read) to locate source files
+ repo_root = get_repo_root()
+
# Determine max tokens for this provider
max_input_tokens = args.max_tokens or PROVIDER_INPUT_LIMITS.get(
args.provider, 100000
@@ -1051,6 +1584,8 @@ def main() -> None:
args.output_format,
args.verbose,
args.timeout,
+ args.tool_rounds,
+ repo_root,
)
total_usage.add(call_usage)
all_reviews.append((patch_label, review_text))
@@ -1121,6 +1656,8 @@ def main() -> None:
args.output_format,
args.verbose,
args.timeout,
+ args.tool_rounds,
+ repo_root,
)
total_usage.add(call_usage)
all_reviews.append((chunk_label, review_text))
@@ -1150,6 +1687,11 @@ def main() -> None:
print(f"Large file mode: {args.large_file}", file=sys.stderr)
if args.split_patches:
print("Split patches: yes", file=sys.stderr)
+ if args.provider in ("anthropic", "openai"):
+ if args.tool_rounds > 0:
+ print(f"Tool calling: enabled (max {args.tool_rounds} rounds)", file=sys.stderr)
+ else:
+ print("Tool calling: disabled", file=sys.stderr)
if args.output:
print(f"Output file: {args.output}", file=sys.stderr)
if args.send_email:
@@ -1174,6 +1716,8 @@ def main() -> None:
args.output_format,
args.verbose,
args.timeout,
+ args.tool_rounds,
+ repo_root,
)
total_usage.add(call_usage)
--
2.51.0
^ permalink raw reply related
* [RFC 0/4] alternative capture mechanism
From: Stephen Hemminger @ 2026-06-09 21:02 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger
This is an RFC for an alternative way to capture packets from a DPDK
application. I did brief demo of similar mechanism at DPDK summit but
this is more complete. Capture runs in the primary process and is driven
entirely over telemetry; no secondary process is involved.
A client asks the application to start capturing and passes it a file
descriptor to write to. The application writes pcapng to that descriptor.
A Wireshark extcap script is the intended front end, but the control path
is just telemetry and the output is just a pipe, so other front ends are
possible.
1/4 telemetry: let a command receive file descriptors from the client
2/4 capture: the library
3/4 test: functional test
4/4 app: the Wireshark extcap script and its documentation
Setup and usage are in doc/guides/tools/wireshark_extcap.rst.
Primary process only for now; secondary-process capture is possible as
follow-on. Posting as RFC to get feedback on the approach.
The extcap script is dual licensed (BSD-3-Clause OR GPL-2.0-or-later) as
it may be more useful in the Wireshark tree.
Stephen Hemminger (4):
telemetry: allow commands to receive file descriptors
capture: infrastructure wireshark packet capture
test: add test for capture hooks
usertools/dpdk-wireshark-extcap.py: script for external capture
MAINTAINERS | 4 +
app/test/meson.build | 1 +
app/test/test_capture.c | 365 +++++++++++
doc/guides/rel_notes/release_26_07.rst | 12 +
doc/guides/tools/index.rst | 1 +
doc/guides/tools/wireshark_extcap.rst | 155 +++++
lib/capture/capture.c | 821 +++++++++++++++++++++++++
lib/capture/capture_impl.h | 56 ++
lib/capture/filter.c | 108 ++++
lib/capture/meson.build | 19 +
lib/meson.build | 1 +
lib/telemetry/rte_telemetry.h | 66 ++
lib/telemetry/telemetry.c | 115 +++-
usertools/dpdk-wireshark-extcap.py | 274 +++++++++
14 files changed, 1986 insertions(+), 12 deletions(-)
create mode 100644 app/test/test_capture.c
create mode 100644 doc/guides/tools/wireshark_extcap.rst
create mode 100644 lib/capture/capture.c
create mode 100644 lib/capture/capture_impl.h
create mode 100644 lib/capture/filter.c
create mode 100644 lib/capture/meson.build
create mode 100755 usertools/dpdk-wireshark-extcap.py
--
2.53.0
^ permalink raw reply
* [RFC 1/4] telemetry: allow commands to receive file descriptors
From: Stephen Hemminger @ 2026-06-09 21:02 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger, Bruce Richardson
In-Reply-To: <20260609210540.768074-1-stephen@networkplumber.org>
Add rte_telemetry_register_cmd_fd_arg() to register a command whose
callback also receives file descriptors passed by the client as
SCM_RIGHTS ancillary data. The callback owns the descriptors and must
close them.
This lets a client open a file itself and hand the descriptor to the
primary process, so DPDK never opens the path. That avoids path and
permission problems and works across container filesystem namespaces.
Existing commands and clients are unaffected. If unsolicited file
descriptor is passed, it is closed.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
doc/guides/rel_notes/release_26_07.rst | 5 ++
lib/telemetry/rte_telemetry.h | 66 ++++++++++++++
lib/telemetry/telemetry.c | 115 ++++++++++++++++++++++---
3 files changed, 174 insertions(+), 12 deletions(-)
diff --git a/doc/guides/rel_notes/release_26_07.rst b/doc/guides/rel_notes/release_26_07.rst
index b5285af5fe..d7a2df88c1 100644
--- a/doc/guides/rel_notes/release_26_07.rst
+++ b/doc/guides/rel_notes/release_26_07.rst
@@ -141,6 +141,11 @@ New Features
Added AGENTS.md file for AI review
and supporting scripts to review patches and documentation.
+* **Added telemetry support for passing file descriptors.**
+
+ Add experimental telemetry callback ``rte_telemetry_register_cmd_fd_arg()``
+ to allow command to receive file descriptors passed by client.
+
Removed Items
-------------
diff --git a/lib/telemetry/rte_telemetry.h b/lib/telemetry/rte_telemetry.h
index 0a58e518f7..3e32d2902b 100644
--- a/lib/telemetry/rte_telemetry.h
+++ b/lib/telemetry/rte_telemetry.h
@@ -325,6 +325,37 @@ typedef int (*telemetry_cb)(const char *cmd, const char *params,
typedef int (*telemetry_arg_cb)(const char *cmd, const char *params, void *arg,
struct rte_tel_data *info);
+/**
+ * This telemetry callback is used when registering a telemetry command with
+ * rte_telemetry_register_cmd_fd_arg().
+ *
+ * It behaves like telemetry_arg_cb, but additionally receives any file
+ * descriptors the client passed alongside the command as SCM_RIGHTS ancillary
+ * data. The callback takes ownership of these descriptors and is responsible
+ * for closing them.
+ *
+ * @param cmd
+ * The cmd that was requested by the client.
+ * @param params
+ * Contains data required by the callback function.
+ * @param arg
+ * The opaque value that was passed to rte_telemetry_register_cmd_fd_arg().
+ * @param fds
+ * Array of file descriptors received from the client. May be NULL when
+ * n_fds is zero.
+ * @param n_fds
+ * Number of file descriptors in the fds array.
+ * @param info
+ * The information to be returned to the caller.
+ *
+ * @return
+ * Length of buffer used on success.
+ * @return
+ * Negative integer on error.
+ */
+typedef int (*telemetry_fd_cb)(const char *cmd, const char *params, void *arg,
+ const int *fds, unsigned int n_fds, struct rte_tel_data *info);
+
/**
* Used when registering a command and callback function with telemetry.
*
@@ -368,6 +399,41 @@ __rte_experimental
int
rte_telemetry_register_cmd_arg(const char *cmd, telemetry_arg_cb fn, void *arg, const char *help);
+/**
+ * Register a command and a file-descriptor-aware callback with telemetry.
+ *
+ * The callback is invoked like rte_telemetry_register_cmd_arg(), but also
+ * receives any file descriptors the client passed alongside the command as
+ * SCM_RIGHTS ancillary data. This lets a client open a file (for example a
+ * capture output file) itself and hand the descriptor to the DPDK process,
+ * which never opens the path - avoiding path and permission concerns and
+ * working across container filesystem namespaces.
+ *
+ * Descriptors sent to a command registered with rte_telemetry_register_cmd()
+ * or rte_telemetry_register_cmd_arg() are rejected and the connection is
+ * closed.
+ *
+ * @param cmd
+ * The command to register with telemetry.
+ * @param fn
+ * Callback function to be called when the command is requested.
+ * @param arg
+ * An opaque value that will be passed to the callback function.
+ * @param help
+ * Help text for the command.
+ *
+ * @return
+ * 0 on success.
+ * @return
+ * -EINVAL for invalid parameters failure.
+ * @return
+ * -ENOMEM for mem allocation failure.
+ */
+__rte_experimental
+int
+rte_telemetry_register_cmd_fd_arg(const char *cmd, telemetry_fd_cb fn, void *arg,
+ const char *help);
+
/**
* @internal
* Free a container that has memory allocated.
diff --git a/lib/telemetry/telemetry.c b/lib/telemetry/telemetry.c
index b109d076d4..30d3ae3a13 100644
--- a/lib/telemetry/telemetry.c
+++ b/lib/telemetry/telemetry.c
@@ -29,6 +29,8 @@
#define MAX_CMD_LEN 56
#define MAX_OUTPUT_LEN (1024 * 16)
#define MAX_CONNECTIONS 10
+/* Maximum number of file descriptors a client may pass with one command. */
+#define MAX_FDS 8
#ifndef RTE_EXEC_ENV_WINDOWS
static void *
@@ -39,6 +41,7 @@ struct cmd_callback {
char cmd[MAX_CMD_LEN];
telemetry_cb fn;
telemetry_arg_cb fn_arg;
+ telemetry_fd_cb fn_fd;
void *arg;
char help[RTE_TEL_MAX_STRING_LEN];
};
@@ -72,15 +75,15 @@ static RTE_ATOMIC(uint16_t) v2_clients;
#endif /* !RTE_EXEC_ENV_WINDOWS */
static int
-register_cmd(const char *cmd, const char *help,
- telemetry_cb fn, telemetry_arg_cb fn_arg, void *arg)
+register_cmd(const char *cmd, const char *help, telemetry_cb fn,
+ telemetry_arg_cb fn_arg, telemetry_fd_cb fn_fd, void *arg)
{
struct cmd_callback *new_callbacks;
const char *cmdp = cmd;
int i = 0;
- if (strlen(cmd) >= MAX_CMD_LEN || (fn == NULL && fn_arg == NULL) || cmd[0] != '/'
- || strlen(help) >= RTE_TEL_MAX_STRING_LEN)
+ if (strlen(cmd) >= MAX_CMD_LEN || (fn == NULL && fn_arg == NULL && fn_fd == NULL)
+ || cmd[0] != '/' || strlen(help) >= RTE_TEL_MAX_STRING_LEN)
return -EINVAL;
while (*cmdp != '\0') {
@@ -107,6 +110,7 @@ register_cmd(const char *cmd, const char *help,
strlcpy(callbacks[i].cmd, cmd, MAX_CMD_LEN);
callbacks[i].fn = fn;
callbacks[i].fn_arg = fn_arg;
+ callbacks[i].fn_fd = fn_fd;
callbacks[i].arg = arg;
strlcpy(callbacks[i].help, help, RTE_TEL_MAX_STRING_LEN);
num_callbacks++;
@@ -119,14 +123,22 @@ RTE_EXPORT_SYMBOL(rte_telemetry_register_cmd)
int
rte_telemetry_register_cmd(const char *cmd, telemetry_cb fn, const char *help)
{
- return register_cmd(cmd, help, fn, NULL, NULL);
+ return register_cmd(cmd, help, fn, NULL, NULL, NULL);
}
RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_telemetry_register_cmd_arg, 24.11)
int
rte_telemetry_register_cmd_arg(const char *cmd, telemetry_arg_cb fn, void *arg, const char *help)
{
- return register_cmd(cmd, help, NULL, fn, arg);
+ return register_cmd(cmd, help, NULL, fn, NULL, arg);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_telemetry_register_cmd_fd_arg, 26.07)
+int
+rte_telemetry_register_cmd_fd_arg(const char *cmd, telemetry_fd_cb fn, void *arg,
+ const char *help)
+{
+ return register_cmd(cmd, help, NULL, NULL, fn, arg);
}
#ifndef RTE_EXEC_ENV_WINDOWS
@@ -368,13 +380,70 @@ output_json(const char *cmd, const struct rte_tel_data *d, int s)
TMTY_LOG_LINE(ERR, "Error writing to socket: %s", strerror(errno));
}
+/*
+ * Receive a command and any file descriptors the client passed alongside it
+ * as SCM_RIGHTS ancillary data. The payload length is returned (0 if the
+ * client sent an empty message or closed the connection, negative on error).
+ * Descriptors that arrive are returned in fds[]/n_fds and are owned by the
+ * caller. MSG_CTRUNC means more descriptors were sent than the control buffer
+ * could hold; *ctrunc is set so the caller can reject the command, but the
+ * descriptors that did fit are still returned so they can be closed rather
+ * than leaked.
+ */
+static int
+recv_with_fds(int s, char *buf, size_t buf_len, int *fds, unsigned int *n_fds,
+ bool *ctrunc)
+{
+ char cmsgbuf[CMSG_SPACE(sizeof(int) * MAX_FDS)];
+ struct iovec iov = { .iov_base = buf, .iov_len = buf_len };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cmsgbuf,
+ .msg_controllen = sizeof(cmsgbuf),
+ };
+ struct cmsghdr *cmsg;
+ int bytes;
+
+ *n_fds = 0;
+ *ctrunc = false;
+
+ bytes = recvmsg(s, &msg, 0);
+ if (bytes < 0)
+ return bytes;
+
+ if (msg.msg_flags & MSG_CTRUNC)
+ *ctrunc = true;
+
+ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+ if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
+ continue;
+ *n_fds = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
+ memcpy(fds, CMSG_DATA(cmsg), *n_fds * sizeof(int));
+ break;
+ }
+ return bytes;
+}
+
static void
-perform_command(const struct cmd_callback *cb, const char *cmd, const char *param, int s)
+close_fds(const int *fds, unsigned int n_fds)
+{
+ unsigned int i;
+
+ for (i = 0; i < n_fds; i++)
+ close(fds[i]);
+}
+
+static void
+perform_command(const struct cmd_callback *cb, const char *cmd, const char *param,
+ const int *fds, unsigned int n_fds, int s)
{
struct rte_tel_data data = {0};
int ret;
- if (cb->fn_arg != NULL)
+ if (cb->fn_fd != NULL)
+ ret = cb->fn_fd(cmd, param, cb->arg, fds, n_fds, &data);
+ else if (cb->fn_arg != NULL)
ret = cb->fn_arg(cmd, param, cb->arg, &data);
else
ret = cb->fn(cmd, param, &data);
@@ -412,8 +481,11 @@ client_handler(void *sock_id)
}
/* receive data is not null terminated */
- int bytes = read(s, buffer, sizeof(buffer) - 1);
- while (bytes > 0) {
+ int fds[MAX_FDS];
+ unsigned int n_fds = 0;
+ bool ctrunc = false;
+ int bytes = recv_with_fds(s, buffer, sizeof(buffer) - 1, fds, &n_fds, &ctrunc);
+ while (bytes > 0 || (bytes == 0 && n_fds > 0)) {
buffer[bytes] = 0;
const char *cmd = strtok(buffer, ",");
const char *param = strtok(NULL, "\0");
@@ -429,9 +501,28 @@ client_handler(void *sock_id)
}
rte_spinlock_unlock(&callback_sl);
}
- perform_command(&cb, cmd, param, s);
- bytes = read(s, buffer, sizeof(buffer) - 1);
+ /*
+ * File descriptors go only to a command that registered to
+ * receive them. A command that did not, or a truncated control
+ * message, is a client error: close the descriptors and drop the
+ * connection rather than silently discarding them.
+ */
+ if (n_fds > 0 && (cb.fn_fd == NULL || ctrunc)) {
+ TMTY_LOG_LINE(ERR,
+ "Closing connection: %u file descriptor(s) passed to '%s'%s",
+ n_fds, cmd ? cmd : "(none)",
+ ctrunc ? " (truncated)" : " which does not accept them");
+ close_fds(fds, n_fds);
+ break;
+ }
+
+ /* an fd-aware callback takes ownership of the descriptors */
+ perform_command(&cb, cmd, param, fds, n_fds, s);
+
+ n_fds = 0;
+ ctrunc = false;
+ bytes = recv_with_fds(s, buffer, sizeof(buffer) - 1, fds, &n_fds, &ctrunc);
}
exit:
close(s);
--
2.53.0
^ permalink raw reply related
* [RFC 2/4] capture: infrastructure wireshark packet capture
From: Stephen Hemminger @ 2026-06-09 21:02 UTC (permalink / raw)
To: dev; +Cc: Stephen Hemminger, Thomas Monjalon, Reshma Pattan,
Anatoly Burakov
In-Reply-To: <20260609210540.768074-1-stephen@networkplumber.org>
This provides a telemetry extension to provide packet capture.
It is intended to be used with a front end script to provide
external packet capture for wireshark.
Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
MAINTAINERS | 1 +
doc/guides/rel_notes/release_26_07.rst | 7 +
lib/capture/capture.c | 821 +++++++++++++++++++++++++
lib/capture/capture_impl.h | 56 ++
lib/capture/filter.c | 108 ++++
lib/capture/meson.build | 19 +
lib/meson.build | 1 +
7 files changed, 1013 insertions(+)
create mode 100644 lib/capture/capture.c
create mode 100644 lib/capture/capture_impl.h
create mode 100644 lib/capture/filter.c
create mode 100644 lib/capture/meson.build
diff --git a/MAINTAINERS b/MAINTAINERS
index 4a68a19b32..dd359d956e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1723,6 +1723,7 @@ F: doc/guides/sample_app_ug/qos_scheduler.rst
Packet capture
M: Reshma Pattan <reshma.pattan@intel.com>
M: Stephen Hemminger <stephen@networkplumber.org>
+F: lib/capture/
F: lib/pdump/
F: doc/guides/prog_guide/pdump_lib.rst
F: app/test/test_pdump.*
diff --git a/doc/guides/rel_notes/release_26_07.rst b/doc/guides/rel_notes/release_26_07.rst
index d7a2df88c1..309a6078bd 100644
--- a/doc/guides/rel_notes/release_26_07.rst
+++ b/doc/guides/rel_notes/release_26_07.rst
@@ -146,6 +146,13 @@ New Features
Add experimental telemetry callback ``rte_telemetry_register_cmd_fd_arg()``
to allow command to receive file descriptors passed by client.
+* **Added packet capture library.**
+
+ Added a new ``capture`` library which provides a mechanism via telemetry
+ interface for capturing packets to a file descriptor. This mechanism
+ is used by the new ``dpdk-wireshark-extcap.py`` script which provides
+ seamless integration with Wireshark.
+
Removed Items
-------------
diff --git a/lib/capture/capture.c b/lib/capture/capture.c
new file mode 100644
index 0000000000..a837c377fc
--- /dev/null
+++ b/lib/capture/capture.c
@@ -0,0 +1,821 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <pthread.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <sys/queue.h>
+#include <sys/utsname.h>
+#include <net/if.h>
+#include <unistd.h>
+
+#include <rte_branch_prediction.h>
+#include <rte_common.h>
+#include <rte_debug.h>
+#include <rte_ethdev.h>
+#include <rte_log.h>
+#include <rte_malloc.h>
+#include <rte_memory.h>
+#include <rte_mempool.h>
+#include <rte_mbuf.h>
+#include <rte_pcapng.h>
+#include <rte_pause.h>
+#include <rte_ring.h>
+#include <rte_spinlock.h>
+#include <rte_stdatomic.h>
+#include <rte_string_fns.h>
+#include <rte_telemetry.h>
+#include <rte_version.h>
+
+#include "capture_impl.h"
+
+#ifndef DLT_EN10MB
+#define DLT_EN10MB 1
+#endif
+
+RTE_LOG_REGISTER_DEFAULT(rte_capture_logtype, NOTICE);
+
+/*
+ * List of active captures.
+ *
+ * This is a control-plane only structure: it is created, walked and torn down
+ * from the telemetry handler thread and from the per-capture drain threads,
+ * never from the dataplane. A plain spinlock is therefore enough; the EAL
+ * shared tailq (rte_tailq) is not used because captures are not visible to
+ * secondary processes in this design.
+ */
+TAILQ_HEAD(capture_list, capture);
+static struct capture_list capture_list = TAILQ_HEAD_INITIALIZER(capture_list);
+static rte_spinlock_t capture_lock = RTE_SPINLOCK_INITIALIZER;
+
+#define DEFAULT_SNAPLEN 262144u /* from tcpdump et.al. */
+#define CAPTURE_BURST_SIZE 32u
+#define MBUF_POOL_CACHE_SIZE 32
+#define CAPTURE_RING_SIZE 256
+#define CAPTURE_POOL_SIZE 1024
+#define SLEEP_THRESHOLD 100
+#define SLEEP_US 100
+
+/* Parameter values: only used on stack inside parsing */
+struct capture_config {
+ uint16_t port_id;
+ uint32_t snaplen;
+ const char *filter_str;
+};
+
+/*
+ * Data used by callback
+ * This per-queue to avoid cache thrashing
+ */
+struct __rte_cache_aligned capture_rxtx_cb {
+ RTE_ATOMIC(uint32_t) use_count;
+ const struct rte_eth_rxtx_callback *cb;
+
+ struct capture_stats {
+ RTE_ATOMIC(uint64_t) accepted; /**< Number of packets accepted by filter. */
+ RTE_ATOMIC(uint64_t) filtered; /**< Number of packets rejected by filter. */
+ RTE_ATOMIC(uint64_t) nombuf; /**< Number of mbuf allocation failures. */
+ RTE_ATOMIC(uint64_t) ringfull; /**< Number of missed packets due to ring full. */
+ } stats;
+};
+
+/*
+ * Per-capture instance state.
+ */
+struct capture {
+ TAILQ_ENTRY(capture) next; /* links into capture_list */
+ unsigned int idx;
+ RTE_ATOMIC(bool) running;
+ int fd; /* file descriptor of FIFO */
+ struct rte_capture_filter *filter;
+ struct rte_ring *ring; /* ring from dataplane to capture thread */
+ struct rte_mempool *mp; /* mempool for capture mbufs */
+
+ uint32_t snaplen; /* amount of data to copy */
+ uint16_t port_id;
+ uint16_t tx_queues;
+ uint16_t rx_queues;
+
+ /* per-queue data sized to max(tx_queue, rx_queues) */
+ struct capture_cbs {
+ struct capture_rxtx_cb tx_cb;
+ struct capture_rxtx_cb rx_cb;
+ } cbs[];
+};
+
+/* Wait for callbacks to be idle before free */
+static void
+capture_cb_wait(struct capture_rxtx_cb *cbs)
+{
+ /* wait until use_count is even (not in use) */
+ RTE_WAIT_UNTIL_MASKED(&cbs->use_count, 1, ==, 0, rte_memory_order_acquire);
+}
+
+/* Hold a reference to callback while active */
+static inline __rte_hot void
+capture_cb_hold(struct capture_rxtx_cb *cbs)
+{
+ rte_atomic_fetch_add_explicit(&cbs->use_count, 1, rte_memory_order_acquire);
+}
+
+/* Drop reference to callback when done */
+static inline __rte_hot void
+capture_cb_release(struct capture_rxtx_cb *cbs)
+{
+ rte_atomic_fetch_sub_explicit(&cbs->use_count, 1, rte_memory_order_release);
+}
+
+/* Cleanup call backs */
+static void __rte_cold
+capture_cb_cleanup(struct capture *cap)
+{
+
+ for (unsigned int q = 0; q < cap->tx_queues; q++) {
+ struct capture_rxtx_cb *tx_cb = &cap->cbs[q].tx_cb;
+ if (tx_cb->cb) {
+ rte_eth_remove_tx_callback(cap->port_id, q, tx_cb->cb);
+ capture_cb_wait(tx_cb);
+ tx_cb->cb = NULL;
+ }
+ }
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++) {
+ struct capture_rxtx_cb *rx_cb = &cap->cbs[q].rx_cb;
+ if (rx_cb->cb) {
+ rte_eth_remove_rx_callback(cap->port_id, q, rx_cb->cb);
+ capture_cb_wait(rx_cb);
+ rx_cb->cb = NULL;
+ }
+ }
+}
+
+/* Create a clone of mbuf to be placed into ring. */
+static inline __rte_hot void
+capture_copy_burst(uint16_t port_id, uint16_t queue_id,
+ enum rte_pcapng_direction direction,
+ struct rte_mbuf **pkts, unsigned int nb_pkts,
+ const struct capture *cap,
+ struct capture_stats *stats)
+{
+ unsigned int i, ring_enq, d_pkts = 0;
+ struct rte_mbuf *dup_bufs[CAPTURE_BURST_SIZE]; /* duplicated packets */
+ struct rte_ring *ring = cap->ring;
+ struct rte_mempool *mp = cap->mp;
+ uint32_t snaplen = cap->snaplen;
+ struct rte_mbuf *p;
+
+ RTE_ASSERT(nb_pkts <= CAPTURE_BURST_SIZE);
+
+ for (i = 0; i < nb_pkts; i++) {
+ /*
+ * This uses same BPF return value convention as socket filter and pcap_offline_filter.
+ * if program returns zero then packet doesn't match the filter (will be ignored).
+ */
+ if (cap->filter) {
+ uint64_t rc = __rte_capture_filter(cap->filter, pkts[i]);
+ if (rc == 0) {
+ rte_atomic_fetch_add_explicit(&stats->filtered, 1,
+ rte_memory_order_relaxed);
+ continue;
+ }
+ }
+
+ p = rte_pcapng_copy(port_id, queue_id, pkts[i], mp,
+ snaplen, direction, NULL);
+
+ if (unlikely(p == NULL))
+ rte_atomic_fetch_add_explicit(&stats->nombuf, 1,
+ rte_memory_order_relaxed);
+ else
+ dup_bufs[d_pkts++] = p;
+ }
+
+ if (d_pkts == 0)
+ return;
+
+ rte_atomic_fetch_add_explicit(&stats->accepted, d_pkts, rte_memory_order_relaxed);
+
+ ring_enq = rte_ring_enqueue_burst(ring, (void *)&dup_bufs[0], d_pkts, NULL);
+ if (unlikely(ring_enq < d_pkts)) {
+ unsigned int drops = d_pkts - ring_enq;
+
+ rte_atomic_fetch_add_explicit(&stats->ringfull, drops, rte_memory_order_relaxed);
+ rte_pktmbuf_free_bulk(&dup_bufs[ring_enq], drops);
+ }
+}
+
+/* Create a clone of mbuf to be placed into ring. */
+static __rte_hot inline void
+capture_copy(uint16_t port_id, uint16_t queue_id,
+ enum rte_pcapng_direction direction,
+ struct rte_mbuf **pkts, uint16_t nb_pkts,
+ const struct capture *cap,
+ struct capture_stats *stats)
+{
+ unsigned int offs = 0;
+
+ do {
+ unsigned int n = RTE_MIN(nb_pkts - offs, CAPTURE_BURST_SIZE);
+
+ capture_copy_burst(port_id, queue_id, direction, &pkts[offs], n, cap, stats);
+ offs += n;
+ } while (offs < nb_pkts);
+}
+
+static __rte_hot uint16_t
+capture_rx(uint16_t port, uint16_t queue,
+ struct rte_mbuf **pkts, uint16_t nb_pkts,
+ uint16_t max_pkts __rte_unused, void *user_params)
+{
+ struct capture *cap = user_params;
+ struct capture_rxtx_cb *cbs = &cap->cbs[queue].rx_cb;
+
+ capture_cb_hold(cbs);
+ capture_copy(port, queue, RTE_PCAPNG_DIRECTION_IN, pkts, nb_pkts, cap, &cbs->stats);
+ capture_cb_release(cbs);
+
+ return nb_pkts;
+}
+
+static __rte_hot uint16_t
+capture_tx(uint16_t port, uint16_t queue,
+ struct rte_mbuf **pkts, uint16_t nb_pkts, void *user_params)
+{
+ struct capture *capture = user_params;
+ struct capture_rxtx_cb *cbs = &capture->cbs[queue].tx_cb;
+
+ capture_cb_hold(cbs);
+ capture_copy(port, queue, RTE_PCAPNG_DIRECTION_OUT, pkts, nb_pkts, capture, &cbs->stats);
+ capture_cb_release(cbs);
+
+ return nb_pkts;
+}
+
+/*
+ * Break the comma separated parameter string into tokens
+ * and fill in the capture config structure.
+ *
+ * Does not use rte_kvargs because that would mangle [] etc in filter expression.
+ */
+static __rte_cold int
+parse_params(char *str, struct capture_config *cfg)
+{
+ uint32_t snaplen = DEFAULT_SNAPLEN;
+
+ char *args[4];
+ int nargs = rte_strsplit(str, strlen(str), args, RTE_DIM(args), ',');
+ /* Need at least the port id */
+ if (nargs < 1) {
+ CAPTURE_LOG(ERR, "missing parameters '%s'", str);
+ return -1;
+ }
+
+ /* Parse port id (required) */
+ char *endp;
+ errno = 0;
+ unsigned long port_id = strtoul(args[0], &endp, 10);
+ if (errno != 0 || port_id >= RTE_MAX_ETHPORTS) {
+ CAPTURE_LOG(ERR, "invalid port_id=%s", args[0]);
+ return -1;
+ }
+ if (*endp != '\0') {
+ CAPTURE_LOG(ERR, "garbage after port_id value");
+ return -1;
+ }
+
+ /* parse remainder as name=value parameters */
+ for (int i = 1; i < nargs; i++) {
+ char *key = args[i];
+
+ /* split at the = */
+ char *eq = strchr(args[i], '=');
+
+ /* all current options require argument after = */
+ if (eq == NULL || eq[1] == '\0') {
+ CAPTURE_LOG(ERR, "missing value for '%s'", key);
+ return -1;
+ }
+ *eq = '\0';
+ char *value = eq + 1;
+
+ if (strcmp(key, "filter") == 0) {
+ cfg->filter_str = value;
+ } else if (strcmp(key, "snaplen") == 0) {
+ errno = 0;
+ unsigned long len = strtoul(value, &endp, 10);
+ if (errno != 0 || *endp != '\0' || len >= UINT32_MAX) {
+ CAPTURE_LOG(ERR, "invalid snaplen '%lu'", len);
+ return -1;
+ }
+ snaplen = len;
+ } else {
+ CAPTURE_LOG(ERR, "unknown parameter '%s'", key);
+ return -1;
+ }
+ }
+
+ cfg->port_id = port_id;
+
+ /*
+ * Default is 256K from tcpdump legacy
+ * using snaplen=0 means everything.
+ */
+ cfg->snaplen = snaplen > 0 ? snaplen : UINT32_MAX;
+
+ return 0;
+}
+
+/*
+ * Open pcapng handle.
+ * Look up OS name and add DPDK version.
+ */
+static __rte_cold rte_pcapng_t *
+capture_pcapng_open(int fd, uint16_t port_id, const char *filter)
+{
+ rte_pcapng_t *pcapng = NULL;
+ char port_name[RTE_ETH_NAME_MAX_LEN];
+ char ifname[IFNAMSIZ];
+ char *ifdescr = NULL;
+ struct utsname uts;
+ char *osname = NULL;
+
+ /* OS name is optional, just keep going if not found */
+ if (uname(&uts) == 0 &&
+ asprintf(&osname, "%s %s", uts.sysname, uts.release) < 0)
+ osname = NULL;
+
+ /* add DPDK internal name */
+ if (rte_eth_dev_get_name_by_port(port_id, port_name) != 0) {
+ CAPTURE_LOG(NOTICE, "Could not find port name for %u", port_id);
+ goto close_fd;
+ }
+
+ /* match name convention used by dpdk-wireshark-extcap.py */
+ snprintf(ifname, sizeof(ifname), "dpdk:%u", port_id);
+ if (asprintf(&ifdescr, "DPDK %s (port %u)", port_name, port_id) < 0)
+ ifdescr = NULL;
+
+ pcapng = rte_pcapng_fdopen(fd, osname, NULL, rte_version(), NULL);
+ if (pcapng == NULL) {
+ CAPTURE_LOG(ERR, "Add section block failed");
+ goto close_fd;
+ }
+
+ if (rte_pcapng_add_interface(pcapng, port_id, DLT_EN10MB, ifname, ifdescr, filter) < 0) {
+ CAPTURE_LOG(ERR, "Add interface for port %u:%s failed", port_id, ifname);
+ rte_pcapng_close(pcapng); /* closes fd */
+ pcapng = NULL;
+ }
+ goto cleanup;
+
+close_fd:
+ close(fd);
+cleanup:
+ free(osname);
+ free(ifdescr);
+ return pcapng;
+}
+
+static __rte_cold void
+capture_link(struct capture *cap)
+{
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_INSERT_TAIL(&capture_list, cap, next);
+ rte_spinlock_unlock(&capture_lock);
+}
+
+static __rte_cold void
+capture_unlink(struct capture *cap)
+{
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_REMOVE(&capture_list, cap, next);
+ rte_spinlock_unlock(&capture_lock);
+}
+
+static __rte_cold void
+capture_free(struct capture *cap)
+{
+ if (cap == NULL)
+ return;
+
+ __rte_capture_filter_free(cap->filter);
+ rte_ring_free(cap->ring);
+ rte_mempool_free(cap->mp);
+ rte_free(cap);
+}
+
+/* Generate unique id for naming and telemetry */
+static unsigned int
+get_unique_id(void)
+{
+ static RTE_ATOMIC(unsigned int) capture_instance;
+
+ return rte_atomic_fetch_add_explicit(&capture_instance, 1, rte_memory_order_relaxed);
+}
+
+/*
+ * Convert configuration into running state
+ */
+static struct capture *
+capture_alloc(const struct capture_config *cfg, int fd,
+ const struct rte_eth_dev_info *dev_info,
+ int socket_id)
+{
+ struct capture *cap;
+ char ring_name[RTE_RING_NAMESIZE];
+ uint16_t mbuf_size;
+ uint16_t num_queues = RTE_MAX(dev_info->nb_tx_queues, dev_info->nb_rx_queues);
+ size_t cb_size = sizeof(*cap) + num_queues * sizeof(cap->cbs[0]);
+
+ cap = rte_zmalloc_socket("capture", cb_size, RTE_CACHE_LINE_SIZE, socket_id);
+ if (cap == NULL) {
+ CAPTURE_LOG(ERR, "Could not allocate capture struct");
+ goto err_close_fd;
+ }
+
+ cap->idx = get_unique_id();
+
+ snprintf(ring_name, sizeof(ring_name), "capture-%u", cap->idx);
+ cap->ring = rte_ring_create(ring_name, CAPTURE_RING_SIZE, socket_id, 0);
+ if (cap->ring == NULL) {
+ CAPTURE_LOG(ERR, "Could not create ring");
+ goto err_close_fd;
+ }
+
+ /*
+ * If snapshot length is smaller than one mbuf segment then pool
+ * element size can be reduced; otherwise can just use the default
+ * and rte_pktmbuf_copy handle multiple segments.
+ */
+ if (cfg->snaplen < RTE_MBUF_DEFAULT_BUF_SIZE)
+ mbuf_size = rte_pcapng_mbuf_size(cfg->snaplen);
+ else
+ mbuf_size = RTE_MBUF_DEFAULT_BUF_SIZE;
+
+ cap->mp = rte_pktmbuf_pool_create_by_ops(ring_name, CAPTURE_POOL_SIZE,
+ MBUF_POOL_CACHE_SIZE, 0, mbuf_size,
+ socket_id, "ring_mp_mc");
+ if (cap->mp == NULL) {
+ CAPTURE_LOG(ERR, "Could not create mempool");
+ goto err_close_fd;
+ }
+
+ if (cfg->filter_str) {
+ cap->filter = __rte_capture_filter_create(cfg->filter_str);
+ if (cap->filter == NULL) {
+ CAPTURE_LOG(ERR, "Could not compile filter: %s", cfg->filter_str);
+ goto err_close_fd;
+ }
+ }
+
+ cap->fd = fd;
+ cap->port_id = cfg->port_id;
+ rte_atomic_store_explicit(&cap->running, true, rte_memory_order_relaxed);
+ cap->snaplen = cfg->snaplen;
+ cap->tx_queues = dev_info->nb_tx_queues;
+ cap->rx_queues = dev_info->nb_rx_queues;
+
+ for (unsigned int q = 0; q < cap->tx_queues; q++) {
+ struct capture_rxtx_cb *tx_cb = &cap->cbs[q].tx_cb;
+ tx_cb->cb = rte_eth_add_tx_callback(cfg->port_id, q, capture_tx, cap);
+ if (tx_cb->cb == NULL)
+ CAPTURE_LOG(ERR, "Register tx callback for %u:%u failed",
+ cfg->port_id, q);
+ }
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++) {
+ struct capture_rxtx_cb *rx_cb = &cap->cbs[q].rx_cb;
+ rx_cb->cb = rte_eth_add_rx_callback(cfg->port_id, q, capture_rx, cap);
+ if (rx_cb->cb == NULL)
+ CAPTURE_LOG(ERR, "Register rx callback for %u:%u failed",
+ cfg->port_id, q);
+ }
+
+ return cap;
+
+err_close_fd:
+ close(fd);
+ capture_free(cap);
+ return NULL;
+}
+
+/*
+ * The capture thread that moves packets from ring into the FIFO
+ */
+static void *
+capture_thread(void *arg)
+{
+ struct capture *cap = arg;
+ unsigned int empty_count = 0;
+
+ CAPTURE_LOG(INFO, "capture thread starting");
+
+ /* This thread wants to detect when FIFO gets closed */
+ sigset_t set;
+ sigemptyset(&set);
+ sigaddset(&set, SIGPIPE);
+ pthread_sigmask(SIG_BLOCK, &set, NULL);
+
+ rte_pcapng_t *pcapng = capture_pcapng_open(cap->fd, cap->port_id,
+ __rte_capture_filter_string(cap->filter));
+ if (pcapng == NULL)
+ goto error;
+
+ while (rte_atomic_load_explicit(&cap->running, rte_memory_order_relaxed)) {
+ unsigned int avail, n;
+ struct rte_mbuf *pkts[CAPTURE_BURST_SIZE];
+
+ n = rte_ring_sc_dequeue_burst(cap->ring, (void **) pkts, CAPTURE_BURST_SIZE, &avail);
+
+ /*
+ * If the ring is empty, apply simple heuristic to keep this
+ * thread from fully consuming the CPU.
+ */
+ if (n == 0) {
+ /* repeat a few times before waiting */
+ if (empty_count < SLEEP_THRESHOLD) {
+ ++empty_count;
+ } else {
+ struct pollfd pfd = { .fd = cap->fd };
+ struct timespec ts = { .tv_nsec = SLEEP_US * 1000 };
+
+ if (ppoll(&pfd, 1, &ts, NULL) > 0 &&
+ (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) {
+ CAPTURE_LOG(NOTICE, "fifo reader closed");
+ break; /* reader is gone */
+ }
+ }
+ continue;
+ }
+
+ /* If this drained the ring count it as first emptying */
+ empty_count = (avail == 0);
+
+ if (unlikely(rte_pcapng_write_packets(pcapng, pkts, n) < 0)) {
+ CAPTURE_LOG(NOTICE, "write to fifo failed: %s", strerror(errno));
+ break;
+ }
+ }
+
+ rte_atomic_store_explicit(&cap->running, false, rte_memory_order_relaxed);
+
+ /* Capture exiting */
+ CAPTURE_LOG(INFO, "capture thread stopping");
+ rte_pcapng_close(pcapng);
+
+error:
+
+ capture_cb_cleanup(cap);
+ capture_unlink(cap);
+ capture_free(cap);
+
+ return NULL;
+}
+
+/*
+ * Callback handler for telemetry library to start capture.
+ *
+ * Need to handle: <iface>,snaplen=<n>,filter=<str>
+ */
+static int
+capture_start_req(const char *cmd, const char *params, void *arg __rte_unused,
+ const int *fds, unsigned int n_fds, struct rte_tel_data *d)
+{
+ struct capture *cap = NULL;
+ struct capture_config cfg = { };
+ struct rte_eth_dev_info dev_info;
+
+ CAPTURE_LOG(DEBUG, "telemetry: %s %s", cmd, params);
+
+ if (rte_eal_process_type() != RTE_PROC_PRIMARY) {
+ CAPTURE_LOG(ERR, "capture can only be started from primary");
+ goto error;
+ }
+
+ if (params == NULL || !isdigit((unsigned char)*params))
+ goto error;
+
+ /* Note: params is const so need non-const copy for parsing */
+ if (parse_params(strdupa(params), &cfg) < 0)
+ goto error;
+
+ /* Need one fd for output */
+ if (n_fds != 1) {
+ if (n_fds == 0)
+ CAPTURE_LOG(ERR, "missing output fd");
+ else
+ CAPTURE_LOG(ERR, "too many fds");
+ goto error;
+ }
+
+ /* Lookup number of queues etc, also validates port_id */
+ if (rte_eth_dev_info_get(cfg.port_id, &dev_info) < 0) {
+ CAPTURE_LOG(ERR, "can not get info for port %u", cfg.port_id);
+ goto error;
+ }
+
+ int socket_id = rte_eth_dev_socket_id(cfg.port_id);
+ if (socket_id < 0) {
+ CAPTURE_LOG(NOTICE, "could not determine socket for port %u", cfg.port_id);
+ socket_id = SOCKET_ID_ANY;
+ }
+
+ cap = capture_alloc(&cfg, fds[0], &dev_info, socket_id);
+ if (cap == NULL)
+ return -1; /* fd already closed by capture_alloc */
+
+ /*
+ * Publish into the active list before starting the drain thread so the
+ * thread is guaranteed to find itself there when it removes itself on
+ * exit (it may exit immediately, e.g. if the FIFO reader is already
+ * gone). On thread-create failure we undo the insertion here.
+ */
+ unsigned int idx = cap->idx;
+ capture_link(cap);
+
+ /*
+ * Make a new thread to do the capture work
+ * Thread will inherit affinity from the telemetry handler that calls us
+ */
+ pthread_t thread_id;
+ if (pthread_create(&thread_id, NULL, capture_thread, cap) != 0) {
+ CAPTURE_LOG(ERR, "Capture thread start failed: %s", strerror(errno));
+
+ close(cap->fd);
+ capture_unlink(cap);
+ capture_cb_cleanup(cap);
+ capture_free(cap);
+ return -1;
+ }
+
+ /* Nothing will be waiting for this thread. */
+ pthread_detach(thread_id);
+
+ /* Return id back for later use. */
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_uint(d, "id", idx);
+ rte_tel_data_add_dict_string(d, "status", "running");
+ return 0;
+
+error:
+ for (unsigned int i = 0; i < n_fds; i++)
+ close(fds[i]);
+ return -1;
+}
+
+
+
+/* Telemetry: stop active capture. */
+static int
+capture_stop_req(const char *cmd, const char *params, struct rte_tel_data *d)
+{
+
+ CAPTURE_LOG(DEBUG, "telemetry %s %s", cmd, params);
+
+ if (params == NULL || *params == '\0')
+ return -EINVAL;
+
+ errno = 0;
+ char *endp;
+ unsigned long idx = strtoul(params, &endp, 10);
+ if (errno != 0 || *endp != '\0')
+ return -EINVAL;
+
+ rte_spinlock_lock(&capture_lock);
+ struct capture *cap;
+ TAILQ_FOREACH(cap, &capture_list, next) {
+ if (cap->idx == idx)
+ break;
+ }
+ if (cap == NULL) {
+ CAPTURE_LOG(ERR, "Capture index %lu not found", idx);
+ rte_spinlock_unlock(&capture_lock);
+ return -ENOENT;
+ }
+ rte_atomic_store_explicit(&cap->running, false, rte_memory_order_relaxed);
+ rte_spinlock_unlock(&capture_lock);
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_string(d, "status", "stopped");
+ return 0;
+}
+
+/* Telemetry: list the ids of all active captures. */
+static int
+capture_list_req(const char *cmd __rte_unused, const char *params __rte_unused,
+ struct rte_tel_data *d)
+{
+ struct capture *cap;
+
+ CAPTURE_LOG(DEBUG, "telemetry %s %s", cmd, params);
+ rte_tel_data_start_array(d, RTE_TEL_UINT_VAL);
+
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_FOREACH(cap, &capture_list, next)
+ rte_tel_data_add_array_uint(d, cap->idx);
+ rte_spinlock_unlock(&capture_lock);
+
+ return 0;
+}
+
+/* Aggregate per-queue counters of a capture instance. */
+struct capture_total {
+ uint64_t accepted;
+ uint64_t filtered;
+ uint64_t nombuf;
+ uint64_t ringfull;
+};
+
+static void
+capture_sum_one(struct capture_total *t, const struct capture_stats *s)
+{
+ t->accepted += rte_atomic_load_explicit(&s->accepted, rte_memory_order_relaxed);
+ t->filtered += rte_atomic_load_explicit(&s->filtered, rte_memory_order_relaxed);
+ t->nombuf += rte_atomic_load_explicit(&s->nombuf, rte_memory_order_relaxed);
+ t->ringfull += rte_atomic_load_explicit(&s->ringfull, rte_memory_order_relaxed);
+}
+
+/* Sum the rx and tx counters across all queues. Caller holds capture_lock. */
+static void
+capture_sum_stats(const struct capture *cap, struct capture_total *t)
+{
+ *t = (struct capture_total){ };
+
+ for (unsigned int q = 0; q < cap->rx_queues; q++)
+ capture_sum_one(t, &cap->cbs[q].rx_cb.stats);
+ for (unsigned int q = 0; q < cap->tx_queues; q++)
+ capture_sum_one(t, &cap->cbs[q].tx_cb.stats);
+}
+
+/* Telemetry: report configuration and counters for one capture. */
+static int
+capture_stats_req(const char *cmd, const char *params,
+ struct rte_tel_data *d)
+{
+ struct capture *cap;
+ struct capture_total t;
+ char *endp;
+
+ CAPTURE_LOG(DEBUG, "telemetry %s %s", cmd, params);
+ if (params == NULL || *params == '\0')
+ return -EINVAL;
+
+ errno = 0;
+ unsigned long idx = strtoul(params, &endp, 10);
+ if (errno != 0 || *endp != '\0')
+ return -EINVAL;
+
+ /* Find the instance and snapshot what we need while holding the lock. */
+ rte_spinlock_lock(&capture_lock);
+ TAILQ_FOREACH(cap, &capture_list, next) {
+ if (cap->idx == idx)
+ break;
+ }
+ if (cap == NULL) {
+ CAPTURE_LOG(ERR, "Capture index %lu not found", idx);
+ rte_spinlock_unlock(&capture_lock);
+ return -ENOENT;
+ }
+
+ rte_tel_data_start_dict(d);
+ rte_tel_data_add_dict_uint(d, "port_id", cap->port_id);
+ if (cap->filter)
+ rte_tel_data_add_dict_string(d, "filter",
+ __rte_capture_filter_string(cap->filter));
+ rte_tel_data_add_dict_int(d, "running",
+ rte_atomic_load_explicit(&cap->running,
+ rte_memory_order_relaxed));
+ rte_tel_data_add_dict_uint(d, "snaplen", cap->snaplen);
+ rte_tel_data_add_dict_uint(d, "rx_queues", cap->rx_queues);
+ rte_tel_data_add_dict_uint(d, "tx_queues", cap->tx_queues);
+ capture_sum_stats(cap, &t);
+ rte_spinlock_unlock(&capture_lock);
+
+ rte_tel_data_add_dict_uint(d, "accepted", t.accepted);
+ rte_tel_data_add_dict_uint(d, "filtered", t.filtered);
+ rte_tel_data_add_dict_uint(d, "nombuf", t.nombuf);
+ rte_tel_data_add_dict_uint(d, "ringfull", t.ringfull);
+
+ return 0;
+}
+
+RTE_INIT(capture_telemetry)
+{
+ rte_telemetry_register_cmd("/ethdev/capture/list", capture_list_req,
+ "List ids of active captures. Takes no parameters.");
+ rte_telemetry_register_cmd("/ethdev/capture/stats", capture_stats_req,
+ "Report configuration and counters for a capture. Parameters: id");
+ rte_telemetry_register_cmd_fd_arg("/ethdev/capture/start", capture_start_req, NULL,
+ "Start capture."
+ "Parameters: port_id,snaplen=N(optional),filter=string(optional)");
+ rte_telemetry_register_cmd("/ethdev/capture/stop", capture_stop_req,
+ "Stop an active capture. Parameters: id");
+}
diff --git a/lib/capture/capture_impl.h b/lib/capture/capture_impl.h
new file mode 100644
index 0000000000..adee734b6c
--- /dev/null
+++ b/lib/capture/capture_impl.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+#ifndef CAPTURE_IMPL_H
+#define CAPTURE_IMPL_H
+
+#define RTE_LOGTYPE_CAPTURE rte_capture_logtype
+extern int rte_capture_logtype;
+#define CAPTURE_LOG(level, ...) \
+ RTE_LOG_LINE_PREFIX(level, CAPTURE, "%s(): ", __func__, __VA_ARGS__)
+
+struct rte_capture_filter;
+
+#ifdef RTE_HAS_LIBPCAP
+struct rte_capture_filter *__rte_capture_filter_create(const char *str);
+const char *__rte_capture_filter_string(struct rte_capture_filter *filter);
+void __rte_capture_filter_free(struct rte_capture_filter *filter);
+uint64_t __rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb);
+
+#else /* !RTE_HAS_LIBPCAP */
+
+/* Stub version if pcap is not available */
+static inline struct rte_capture_filter *
+__rte_capture_filter_create(const char *str)
+{
+ RTE_SET_USED(str);
+ return NULL; /* not supported */
+}
+
+static inline const char *
+__rte_capture_filter_string(struct rte_capture_filter *filter)
+{
+ RTE_SET_USED(filter);
+ return NULL;
+}
+
+static inline void
+__rte_capture_filter_free(struct rte_capture_filter *filter)
+{
+ RTE_SET_USED(filter);
+}
+
+/*
+ * This will be zero if the packet doesn't match the filter and non-zero if
+ * the packet matches the filter.
+ */
+static inline uint64_t
+__rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb)
+{
+ RTE_SET_USED(filter);
+ RTE_SET_USED(mb);
+ return 1;
+}
+
+#endif /* !RTE_HAS_LIBPCAP */
+#endif /* CAPTURE_IMPL_H */
diff --git a/lib/capture/filter.c b/lib/capture/filter.c
new file mode 100644
index 0000000000..ecb5e8a765
--- /dev/null
+++ b/lib/capture/filter.c
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <pcap/pcap.h>
+
+#include <rte_bpf.h>
+#include <rte_errno.h>
+#include <rte_log.h>
+#include <rte_malloc.h>
+#include <rte_mbuf.h>
+
+#include "capture_impl.h"
+
+struct rte_capture_filter {
+ struct rte_bpf *bpf;
+ struct rte_bpf_jit jit;
+ char expr[]; /* original filter text */
+};
+
+/*
+ * Convert text string into an eBPF program
+ */
+struct rte_capture_filter *
+__rte_capture_filter_create(const char *filter)
+{
+ struct rte_capture_filter *flt = NULL;
+ struct rte_bpf_prm *prm = NULL;
+
+ /* libpcap needs a handle */
+ pcap_t *pcap = pcap_open_dead(DLT_EN10MB, UINT16_MAX);
+ if (!pcap) {
+ CAPTURE_LOG(ERR, "pcap: can not open handle");
+ return NULL;
+ }
+
+ flt = rte_zmalloc("capture_filter", sizeof(*flt) + strlen(filter) + 1, 0);
+ if (flt == NULL) {
+ CAPTURE_LOG(ERR, "capture filter alloc failed");
+ goto error;
+ }
+
+ /* convert string to cBPF program */
+ struct bpf_program bf;
+ if (pcap_compile(pcap, &bf, filter, 1, PCAP_NETMASK_UNKNOWN) != 0) {
+ CAPTURE_LOG(ERR, "pcap: can not compile filter: %s",
+ pcap_geterr(pcap));
+ goto error;
+ }
+ strcpy(flt->expr, filter);
+
+ /* convert cBPF to eBPF */
+ prm = rte_bpf_convert(&bf);
+ pcap_freecode(&bf); /* drop the cBPF program */
+
+ if (prm == NULL) {
+ CAPTURE_LOG(ERR, "BPF convert interface %s(%d)",
+ rte_strerror(rte_errno), rte_errno);
+ goto error;
+ }
+
+ flt->bpf = rte_bpf_load(prm);
+ if (flt->bpf == NULL) {
+ CAPTURE_LOG(ERR, "BPF load failed: %s(%d)",
+ rte_strerror(rte_errno), rte_errno);
+ goto error;
+ }
+
+ rte_bpf_get_jit(flt->bpf, &flt->jit);
+ if (flt->jit.func == NULL)
+ CAPTURE_LOG(NOTICE, "No JIT available for filter");
+
+ pcap_close(pcap);
+ rte_free(prm);
+ return flt;
+
+error:
+ pcap_close(pcap);
+ rte_free(prm);
+ rte_free(flt);
+ return NULL;
+}
+
+const char *__rte_capture_filter_string(struct rte_capture_filter *filter)
+{
+ return filter ? filter->expr : NULL;
+}
+
+void __rte_capture_filter_free(struct rte_capture_filter *filter)
+{
+ if (filter == NULL)
+ return;
+
+ rte_bpf_destroy(filter->bpf);
+ rte_free(filter);
+}
+
+uint64_t __rte_capture_filter(const struct rte_capture_filter *filter, struct rte_mbuf *mb)
+{
+ if (filter->jit.func)
+ return filter->jit.func(mb);
+ else
+ return rte_bpf_exec(filter->bpf, mb);
+}
diff --git a/lib/capture/meson.build b/lib/capture/meson.build
new file mode 100644
index 0000000000..4dbe0d1a78
--- /dev/null
+++ b/lib/capture/meson.build
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Stephen Hemminger
+
+if is_windows
+ build = false
+ reason = 'not supported on Windows'
+ subdir_done()
+endif
+
+sources = files('capture.c')
+
+deps += ['ethdev', 'pcapng', 'bpf']
+
+if dpdk_conf.has('RTE_HAS_LIBPCAP')
+ sources += files('filter.c')
+ ext_deps += pcap_dep
+else
+ warning('libpcap is missing, capture filtering will be disabled')
+endif
diff --git a/lib/meson.build b/lib/meson.build
index af5c160cb8..6d9992f61f 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -49,6 +49,7 @@ libraries = [
'lpm',
'member',
'pcapng',
+ 'capture', # depends on pcapng and bpf
'power',
'rawdev',
'regexdev',
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox