When --format json is passed to ovs-appctl, dpctl/show returns a JSON
object keyed by datapath name. Each entry contains "flows", "lookups"
(hits/lost/missed), and "ports" (keyed by port name, each with
"port-number" and "type"). The optional sections "masks" and "cache"
are only reported when the datapath supports them, and so are the port
"config" and "statistics" sub-objects. "cache" contains "statistics"
(hits and hit rate) and "config", an object keyed by cache level name
with the size of every level. The per packet mask hits and the cache
hit rate are omitted when no packets were looked up.
The output_format field is added to struct dpctl_params so the handler
can select the appropriate callback (show_dpif or show_dpif_json).
The JSON accumulator is stored in dpctl_params.json, and the reply
is sent by dpctl_unixctl_handler based on whether json is set. On
error, the JSON object is discarded and a text error reply is sent.
Since the JSON error path reuses the text error string, a failure of
dps_for_each() to enumerate the datapaths is now reported instead of
replying with an empty message.
Example output:
{"ovs-system": {"flows": 0,
"lookups": {"hits": 0, "lost": 0, "missed": 0},
"ports": {"br0": {"port-number": 0,
"type": "internal"}}}}
Signed-off-by: Timothy Redaelli <[email protected]>
---
NEWS | 2 +-
lib/dpctl.c | 199 ++++++++++++++++++++++++++++++++++++++--
lib/dpctl.h | 11 +++
tests/dpctl.at | 93 +++++++++++++++++++
tests/system-traffic.at | 31 +++++++
5 files changed, 329 insertions(+), 7 deletions(-)
diff --git a/NEWS b/NEWS
index b6f95303e..9dc9c7f5a 100644
--- a/NEWS
+++ b/NEWS
@@ -3,7 +3,7 @@ Post-v4.0.0
- ovs-appctl:
* Added JSON output support (--format json) for 'fdb/stats-show',
'upcall/show', 'dpif-netdev/pmd-sleep-show',
- 'dpif-netdev/pmd-perf-show'.
+ 'dpif-netdev/pmd-perf-show', 'dpctl/show'.
v4.0.0 - 17 Aug 2026
diff --git a/lib/dpctl.c b/lib/dpctl.c
index d6ff348e5..40ad6cba3 100644
--- a/lib/dpctl.c
+++ b/lib/dpctl.c
@@ -37,6 +37,7 @@
#include "dpif-provider.h"
#include "openvswitch/dynamic-string.h"
#include "flow.h"
+#include "openvswitch/json.h"
#include "openvswitch/match.h"
#include "netdev.h"
#include "netlink.h"
@@ -635,6 +636,173 @@ show_dpif_cache(struct dpif *dpif, struct dpctl_params
*dpctl_p)
show_dpif_cache__(dpif, dpctl_p);
}
+/* Adds 'value' to 'json' under 'name', unless the statistic is not
+ * supported by the netdev, which is indicated by UINT64_MAX. */
+static void
+json_put_stat(struct json *json, const char *name, uint64_t value)
+{
+ if (value != UINT64_MAX) {
+ json_object_put(json, name, json_integer_create(value));
+ }
+}
+
+static struct json *
+netdev_stats_to_json(const struct netdev_stats *s)
+{
+ struct json *json = json_object_create();
+
+ json_put_stat(json, "collisions", s->collisions);
+ json_put_stat(json, "rx-bytes", s->rx_bytes);
+ json_put_stat(json, "rx-dropped", s->rx_dropped);
+ json_put_stat(json, "rx-errors", s->rx_errors);
+ json_put_stat(json, "rx-frame-errors", s->rx_frame_errors);
+ json_put_stat(json, "rx-over-errors", s->rx_over_errors);
+ json_put_stat(json, "rx-packets", s->rx_packets);
+ json_put_stat(json, "tx-aborted-errors", s->tx_aborted_errors);
+ json_put_stat(json, "tx-bytes", s->tx_bytes);
+ json_put_stat(json, "tx-carrier-errors", s->tx_carrier_errors);
+ json_put_stat(json, "tx-dropped", s->tx_dropped);
+ json_put_stat(json, "tx-errors", s->tx_errors);
+ json_put_stat(json, "tx-packets", s->tx_packets);
+ json_put_stat(json, "upcall-errors", s->upcall_errors);
+ json_put_stat(json, "upcall-packets", s->upcall_packets);
+
+ return json;
+}
+
+static void
+show_dpif_json(struct dpif *dpif, struct dpctl_params *dpctl_p)
+{
+ struct json *json_ports = json_object_create();
+ struct json *json_dp = json_object_create();
+ struct json *json_dps = dpctl_p->json;
+ struct json *json_cache = NULL;
+ struct dpif_port_dump dump;
+ struct dpif_dp_stats stats;
+ struct dpif_port dpif_port;
+ uint32_t nr_caches;
+
+ if (!dpif_get_dp_stats(dpif, &stats)) {
+ struct json *json_lookups = json_object_create();
+ uint64_t n_pkts = stats.n_hit + stats.n_missed;
+
+ json_object_put(json_lookups, "hits",
+ json_integer_create(stats.n_hit));
+ json_object_put(json_lookups, "lost",
+ json_integer_create(stats.n_lost));
+ json_object_put(json_lookups, "missed",
+ json_integer_create(stats.n_missed));
+ json_object_put(json_dp, "flows", json_integer_create(stats.n_flows));
+ json_object_put(json_dp, "lookups", json_lookups);
+
+ if (stats.n_masks != UINT32_MAX) {
+ struct json *json_masks = json_object_create();
+
+ json_object_put(json_masks, "hits",
+ json_integer_create(stats.n_mask_hit));
+ if (n_pkts) {
+ double avg = (double) stats.n_mask_hit / n_pkts;
+
+ json_object_put(json_masks, "hits-per-packet",
+ json_real_create(avg));
+ }
+
+ json_object_put(json_masks, "total",
+ json_integer_create(stats.n_masks));
+ json_object_put(json_dp, "masks", json_masks);
+ }
+
+ if (stats.n_cache_hit != UINT64_MAX) {
+ struct json *json_stats = json_object_create();
+
+ json_object_put(json_stats, "hits",
+ json_integer_create(stats.n_cache_hit));
+ if (n_pkts) {
+ double avg_hits = (double) stats.n_cache_hit / n_pkts * 100;
+
+ json_object_put(json_stats, "hit-rate",
+ json_real_create(avg_hits));
+ }
+
+ json_cache = json_object_create();
+ json_object_put(json_cache, "statistics", json_stats);
+ }
+ }
+
+ if (!dpif_cache_get_supported_levels(dpif, &nr_caches) && nr_caches > 0) {
+ struct json *json_config = json_object_create();
+
+ for (uint32_t i = 0; i < nr_caches; i++) {
+ struct json *json_c;
+ const char *name;
+ uint32_t size;
+
+ if (dpif_cache_get_name(dpif, i, &name) ||
+ dpif_cache_get_size(dpif, i, &size)) {
+ continue;
+ }
+
+ json_c = json_object_create();
+ json_object_put(json_c, "size", json_integer_create(size));
+ json_object_put(json_config, name, json_c);
+ }
+
+ if (!json_cache) {
+ json_cache = json_object_create();
+ }
+ json_object_put(json_cache, "config", json_config);
+ }
+
+ if (json_cache) {
+ json_object_put(json_dp, "cache", json_cache);
+ }
+
+ DPIF_PORT_FOR_EACH (&dpif_port, &dump, dpif) {
+ struct json *json_port = json_object_create();
+ struct netdev *netdev;
+
+ json_object_put(json_port, "port-number",
+ json_integer_create(odp_to_u32(dpif_port.port_no)));
+ json_object_put_string(json_port, "type", dpif_port.type);
+
+ if (strcmp(dpif_port.type, "system")) {
+ int error = netdev_open(dpif_port.name, dpif_port.type, &netdev);
+
+ if (!error) {
+ struct smap config = SMAP_INITIALIZER(&config);
+
+ error = netdev_get_config(netdev, &config);
+ if (!error && smap_count(&config) > 0) {
+ json_object_put(json_port, "config",
+ smap_to_json(&config));
+ }
+ smap_destroy(&config);
+ netdev_close(netdev);
+ }
+ }
+
+ if (dpctl_p->print_statistics) {
+ int error = netdev_open(dpif_port.name, dpif_port.type, &netdev);
+ struct netdev_stats s;
+
+ if (!error) {
+ error = netdev_get_stats(netdev, &s);
+
+ netdev_close(netdev);
+ if (!error) {
+ json_object_put(json_port, "statistics",
+ netdev_stats_to_json(&s));
+ }
+ }
+ }
+
+ json_object_put(json_ports, dpif_port.name, json_port);
+ }
+
+ json_object_put(json_dp, "ports", json_ports);
+ json_object_put(json_dps, dpif_name(dpif), json_dp);
+}
+
static void
show_dpif(struct dpif *dpif, struct dpctl_params *dpctl_p)
{
@@ -822,17 +990,28 @@ dps_for_each(struct dpctl_params *dpctl_p,
dps_for_each_cb cb)
* because they're not interesting for the user. This happens, for
* example, if OVS is using a userspace datapath and the kernel module
* is not loaded. */
- if (openerror) {
+ if (openerror || at_least_one) {
return openerror;
- } else {
- return at_least_one ? 0 : enumerror;
}
+ if (enumerror) {
+ dpctl_error(dpctl_p, enumerror, "enumerating datapaths failed");
+ }
+ return enumerror;
}
static int
dpctl_show(int argc, const char *argv[], struct dpctl_params *dpctl_p)
{
int error, lasterror = 0;
+ dps_for_each_cb cb;
+
+ if (dpctl_p->output_format == UNIXCTL_OUTPUT_FMT_JSON) {
+ dpctl_p->json = json_object_create();
+ cb = show_dpif_json;
+ } else {
+ cb = show_dpif;
+ }
+
if (argc > 1) {
int i;
for (i = 1; i < argc; i++) {
@@ -841,7 +1020,7 @@ dpctl_show(int argc, const char *argv[], struct
dpctl_params *dpctl_p)
error = parsed_dpif_open(name, false, &dpif);
if (!error) {
- show_dpif(dpif, dpctl_p);
+ cb(dpif, dpctl_p);
dpif_close(dpif);
} else {
dpctl_error(dpctl_p, error, "opening datapath %s failed",
@@ -850,7 +1029,7 @@ dpctl_show(int argc, const char *argv[], struct
dpctl_params *dpctl_p)
}
}
} else {
- lasterror = dps_for_each(dpctl_p, show_dpif);
+ lasterror = dps_for_each(dpctl_p, cb);
}
return lasterror;
@@ -3159,6 +3338,7 @@ dpctl_unixctl_handler(struct unixctl_conn *conn, int
argc, const char *argv[],
.is_appctl = true,
.output = dpctl_unixctl_print,
.aux = &ds,
+ .output_format = unixctl_command_get_output_format(conn),
};
/* Parse options (like getopt). Unfortunately it does
@@ -3226,7 +3406,14 @@ dpctl_unixctl_handler(struct unixctl_conn *conn, int
argc, const char *argv[],
error = handler(argc, argv, &dpctl_p) != 0;
}
- if (error) {
+ if (dpctl_p.json) {
+ if (error) {
+ json_destroy(dpctl_p.json);
+ unixctl_command_reply_error(conn, ds_cstr(&ds));
+ } else {
+ unixctl_command_reply_json(conn, dpctl_p.json);
+ }
+ } else if (error) {
unixctl_command_reply_error(conn, ds_cstr(&ds));
} else {
unixctl_command_reply(conn, ds_cstr(&ds));
diff --git a/lib/dpctl.h b/lib/dpctl.h
index 9d0052152..fe475471e 100644
--- a/lib/dpctl.h
+++ b/lib/dpctl.h
@@ -19,6 +19,9 @@
#include <stdbool.h>
#include "compiler.h"
+#include "unixctl.h"
+
+struct json;
struct dpctl_params {
/* True if it is called by ovs-appctl command. */
@@ -51,6 +54,14 @@ struct dpctl_params {
/* 'usage' (if != NULL) gets called for the "help" command. */
void (*usage)(void *aux);
+
+ /* Output format requested by the caller. */
+ enum unixctl_output_fmt output_format;
+
+ /* JSON object for accumulating structured output. Command handlers
+ * set this to a non-NULL value when producing JSON output; the caller
+ * uses it to decide whether to send a JSON or text reply. */
+ struct json *json;
};
int dpctl_run_command(int argc, const char *argv[],
diff --git a/tests/dpctl.at b/tests/dpctl.at
index a87f67f98..88fccc8fe 100644
--- a/tests/dpctl.at
+++ b/tests/dpctl.at
@@ -86,6 +86,99 @@ OVS_VSWITCHD_STOP(["/dummy@br0: port_del failed/d
/Dropped 1 log messages in last/d"])
AT_CLEANUP
+AT_SETUP([dpctl - show JSON output])
+OVS_VSWITCHD_START([], [], [=override])
+AT_CHECK([ovs-appctl dpctl/add-dp dummy@br0])
+AT_CHECK([ovs-appctl --format json --pretty dpctl/show dummy@br0], [0], [dnl
+{
+ "dummy@br0": {
+ "flows": 0,
+ "lookups": {
+ "hits": 0,
+ "lost": 0,
+ "missed": 0},
+ "ports": {
+ "br0": {
+ "port-number": 0,
+ "type": "dummy-internal"}}}}
+])
+
+dnl Check that netdev configuration shows up in the JSON output.
+AT_CHECK([ovs-appctl dpctl/add-if dummy@br0 vif1.0,type=dummy,port_no=5])
+AT_CHECK([ovs-appctl dpctl/add-if dummy@br0 dnl
+ vif2.0,type=dummy,ifindex=42,port_no=6])
+AT_CHECK([ovs-appctl --format json --pretty dpctl/show dummy@br0], [0], [dnl
+{
+ "dummy@br0": {
+ "flows": 0,
+ "lookups": {
+ "hits": 0,
+ "lost": 0,
+ "missed": 0},
+ "ports": {
+ "br0": {
+ "port-number": 0,
+ "type": "dummy-internal"},
+ "vif1.0": {
+ "port-number": 5,
+ "type": "dummy"},
+ "vif2.0": {
+ "config": {
+ "ifindex": "42"},
+ "port-number": 6,
+ "type": "dummy"}}}}
+])
+
+dnl Check port statistics in the JSON output. Dummy netdevs only collect
+dnl rx/tx packet and byte counters; unavailable counters must be omitted.
+AT_CHECK([ovs-appctl --format json --pretty dpctl/show -s dummy@br0], [0], [dnl
+{
+ "dummy@br0": {
+ "flows": 0,
+ "lookups": {
+ "hits": 0,
+ "lost": 0,
+ "missed": 0},
+ "ports": {
+ "br0": {
+ "port-number": 0,
+ "statistics": {
+ "rx-bytes": 0,
+ "rx-packets": 0,
+ "tx-bytes": 0,
+ "tx-packets": 0},
+ "type": "dummy-internal"},
+ "vif1.0": {
+ "port-number": 5,
+ "statistics": {
+ "rx-bytes": 0,
+ "rx-packets": 0,
+ "tx-bytes": 0,
+ "tx-packets": 0},
+ "type": "dummy"},
+ "vif2.0": {
+ "config": {
+ "ifindex": "42"},
+ "port-number": 6,
+ "statistics": {
+ "rx-bytes": 0,
+ "rx-packets": 0,
+ "tx-bytes": 0,
+ "tx-packets": 0},
+ "type": "dummy"}}}}
+])
+
+dnl Check that errors are reported as text even if JSON was requested.
+AT_CHECK([ovs-appctl --format json dpctl/show dummy@nodp], [2], [], [stderr])
+AT_CHECK([sed 's/(.*)/(...)/' stderr], [0], [dnl
+ovs-vswitchd: opening datapath dummy@nodp failed (...)
+ovs-appctl: ovs-vswitchd: server returned an error
+])
+
+AT_CHECK([ovs-appctl dpctl/del-dp dummy@br0])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
+
AT_SETUP([dpctl - add/mod/del-flows])
OVS_VSWITCHD_START
AT_CHECK([ovs-appctl dpctl/add-dp dummy@br0])
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index a1794ceca..1218b5df9 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -2521,6 +2521,37 @@ AT_CHECK([ovs-dpctl cache-get-size | grep masks-cache |
tr -d [[:blank:]]], [0],
masks-cache:size:256
])
+dnl Check that the mask and cache details are present in the JSON output.
+dnl The values depend on the traffic, so only the structure is checked. The
+dnl cache statistics are not included, as the datapath reports no cache hits
+dnl at this point. The mask hits per packet are only reported if a packet
+dnl was looked up, so they are filtered out.
+AT_CHECK([ovs-appctl --format json --pretty dpctl/show | dnl
+ sed -e '/"hits-per-packet": /d' dnl
+ -e 's/: [[0-9]][[0-9.e+-]]*/: <cleared>/g'], [0], [dnl
+{
+ "system@ovs-system": {
+ "cache": {
+ "config": {
+ "masks-cache": {
+ "size": <cleared>}}},
+ "flows": <cleared>,
+ "lookups": {
+ "hits": <cleared>,
+ "lost": <cleared>,
+ "missed": <cleared>},
+ "masks": {
+ "hits": <cleared>,
+ "total": <cleared>},
+ "ports": {
+ "br0": {
+ "port-number": <cleared>,
+ "type": "internal"},
+ "ovs-system": {
+ "port-number": <cleared>,
+ "type": "internal"}}}}
+])
+
OVS_TRAFFIC_VSWITCHD_STOP
AT_CLEANUP
--
2.56.0
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev