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 <[email protected]>
---
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