diff mbox series

[ovs-dev,RFC,v2,1/1] Add command args to output JSON from ovs-appctl commands.

Message ID 20231020092205.710399-2-jmeng@redhat.com
State RFC
Headers show
Series Add command args to output JSON from ovs-appctl commands. | expand

Checks

Context Check Description
ovsrobot/apply-robot warning apply and check: warning
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/intel-ovs-compilation fail test: fail

Commit Message

Jakob Meng Oct. 20, 2023, 9:22 a.m. UTC
From: Jakob Meng <code@jakobmeng.de>

For monitoring systems such as Prometheus it would be beneficial if OVS
and OVS-DPDK would expose statistics in a machine-readable format.
Several approaches like UNIX socket, OVSDB queries and JSON output from
ovs-xxx tools have been proposed [0],[1]. This proof of concept
describes one way how ovs-xxx tools could output JSON in addition to
plain-text for humans.

With this patch, the 'ovs-appctl dpif/show' command has an optional
[(-o|--output=)text|json] option. By default, the output format is
plain-text as before. The JSON option provides the same amount of
information but as JSON. The tests/pmd.at file has an example of how
the output will look like.

The new output option has been implemented for the dpif/show command
specifically and not as a global option. This approach allows to add
this option to commands incrementally. With a global option, each
command would have to either announce which output format it supports
and/or implement code to fail if the user-supplied format is
unsupported. Also, passing global options to commands is not
implemented atm. Both would require deeper changes than adding new
options to command.

One drawback of a command option is that we cannot (?) enforce the
option name and values across all commands in code. We would have to
ensure in reviews that the option is always called '-o/--output' and
not '-f,--format' etc.

Another issue is that most commands bring own code to parse argc/argv.
This patch delegates the parsing code for the output option to a new
function ovs_cmdl_parse_output_fmt() in lib/command-line.c. However, a
more generic approach might be favorable to reduce duplicate argc/argv
parsing code across commands.
The existing getopt()/getopt_long() cannot be used in its current state
because both have global state and both are already used to parse
global options. We could modify both functions to use local state only
or use alternatives [2].

Another question is whether JSON output should be pretty printed and
sorted or not. In most cases it would be unnecessary because machines
consume the output or humans could use jq for pretty printing. However,
it would make tests more readable (for humans) without having to use jq
(which would require us to introduce a dependency on jq).

Wdyt?

[0] https://bugzilla.redhat.com/show_bug.cgi?id=1824861#c7
[1] https://patchwork.ozlabs.org/project/openvswitch/patch/20230831131122.284913-1-rjarry@redhat.com/
[2] https://nullprogram.com/blog/2014/10/12/

Reported-at: https://bugzilla.redhat.com/1824861
Signed-off-by: Jakob Meng <code@jakobmeng.de>
---
 lib/command-line.c     |  40 +++++++++++
 lib/command-line.h     |   8 +++
 lib/unixctl.c          |  75 ++++++++++++++------
 lib/unixctl.h          |   3 +
 ofproto/ofproto-dpif.c | 153 +++++++++++++++++++++++++++++++++++++----
 tests/pmd.at           |  29 ++++++++
 6 files changed, 273 insertions(+), 35 deletions(-)

--
2.39.2

Comments

0-day Robot Oct. 20, 2023, 9:39 a.m. UTC | #1
Bleep bloop.  Greetings Jakob Meng, I am a robot and I have tried out your patch.
Thanks for your contribution.

I encountered some error that I wasn't expecting.  See the details below.


checkpatch:
ERROR: Inappropriate spacing around cast
#475 FILE: ofproto/ofproto-dpif.c:6725:
    unixctl_command_register("dpif/show", "[(-o|--output=)text|json]", 0, 2,

Lines checked: 523, Warnings: 0, Errors: 1


Please check this out.  If you feel there has been an error, please email aconole@redhat.com

Thanks,
0-day Robot
diff mbox series

Patch

diff --git a/lib/command-line.c b/lib/command-line.c
index 967f4f5d5..7bd3ce11d 100644
--- a/lib/command-line.c
+++ b/lib/command-line.c
@@ -53,6 +53,46 @@  ovs_cmdl_long_options_to_short_options(const struct option options[])
     return xstrdup(short_options);
 }

+static char * OVS_WARN_UNUSED_RESULT
+ovs_cmdl_parse_output_fmt__(const char * arg, enum ovs_output_fmt *fmt)
+{
+    if (!strcmp(arg, "text")) {
+        *fmt = OVS_OUTPUT_FMT_TEXT;
+    } else if (!strcmp(arg, "json")) {
+        *fmt = OVS_OUTPUT_FMT_JSON;
+    } else {
+        return xasprintf("unsupported output format '%s'", arg);
+    }
+    return NULL;
+}
+
+char * OVS_WARN_UNUSED_RESULT
+ovs_cmdl_parse_output_fmt(int argi, int argc, const char *argv[],
+                          enum ovs_output_fmt *fmt, int *parsed)
+{
+    ovs_assert(argi < argc);
+    const char *arg = argv[argi];
+    *parsed = 0;
+
+    if (!strncmp(arg, "--output=", 9)) {
+        *parsed = 2;
+        if (strlen(arg) <= 9) {
+            return xasprintf("option '%s' requires an argument", arg);
+        }
+        return ovs_cmdl_parse_output_fmt__(arg + 9, fmt);
+    } else if (!strcmp(arg, "-o") || !strcmp(arg, "--output")) {
+        *parsed = 2;
+        if (argi+1 >= argc) {
+            return xasprintf("option '%s' requires an argument", arg);
+        }
+        return ovs_cmdl_parse_output_fmt__(argv[argi + 1], fmt);
+    } else {
+        return NULL;
+    }
+
+    OVS_NOT_REACHED();
+}
+
 static char * OVS_WARN_UNUSED_RESULT
 build_short_options(const struct option *long_options)
 {
diff --git a/lib/command-line.h b/lib/command-line.h
index fc2a2690f..d04377098 100644
--- a/lib/command-line.h
+++ b/lib/command-line.h
@@ -46,6 +46,14 @@  struct ovs_cmdl_command {

 char *ovs_cmdl_long_options_to_short_options(const struct option *options);

+enum ovs_output_fmt {
+    OVS_OUTPUT_FMT_TEXT,
+    OVS_OUTPUT_FMT_JSON
+};
+
+char * ovs_cmdl_parse_output_fmt(int argi, int argc, const char *argv[],
+                                 enum ovs_output_fmt *fmt, int *parsed);
+
 struct ovs_cmdl_parsed_option {
     const struct option *o;
     char *arg;
diff --git a/lib/unixctl.c b/lib/unixctl.c
index 103357ee9..9ccb1255a 100644
--- a/lib/unixctl.c
+++ b/lib/unixctl.c
@@ -21,7 +21,6 @@ 
 #include "coverage.h"
 #include "dirs.h"
 #include "openvswitch/dynamic-string.h"
-#include "openvswitch/json.h"
 #include "jsonrpc.h"
 #include "openvswitch/list.h"
 #include "openvswitch/poll-loop.h"
@@ -128,36 +127,42 @@  unixctl_command_register(const char *name, const char *usage,
     shash_add(&commands, name, command);
 }

-static void
-unixctl_command_reply__(struct unixctl_conn *conn,
-                        bool success, const char *body)
+static struct json *
+json_string_create__(const char *body)
 {
-    struct json *body_json;
-    struct jsonrpc_msg *reply;
-
-    COVERAGE_INC(unixctl_replied);
-    ovs_assert(conn->request_id);
-
     if (!body) {
         body = "";
     }

     if (body[0] && body[strlen(body) - 1] != '\n') {
-        body_json = json_string_create_nocopy(xasprintf("%s\n", body));
+        return json_string_create_nocopy(xasprintf("%s\n", body));
     } else {
-        body_json = json_string_create(body);
+        return json_string_create(body);
     }
+}
+
+/* Takes ownership of 'body'. */
+static void
+unixctl_command_reply__(struct unixctl_conn *conn,
+                        bool success, struct json *body)
+{
+    struct jsonrpc_msg *reply;
+
+    COVERAGE_INC(unixctl_replied);
+    ovs_assert(conn->request_id);

     if (success) {
-        reply = jsonrpc_create_reply(body_json, conn->request_id);
+        reply = jsonrpc_create_reply(body, conn->request_id);
     } else {
-        reply = jsonrpc_create_error(body_json, conn->request_id);
+        reply = jsonrpc_create_error(body, conn->request_id);
     }

     if (VLOG_IS_DBG_ENABLED()) {
         char *id = json_to_string(conn->request_id, 0);
+        char *msg = json_to_string(body, 0);
         VLOG_DBG("replying with %s, id=%s: \"%s\"",
-                 success ? "success" : "error", id, body);
+                 success ? "success" : "error", id, msg);
+        free(msg);
         free(id);
     }

@@ -170,22 +175,34 @@  unixctl_command_reply__(struct unixctl_conn *conn,

 /* Replies to the active unixctl connection 'conn'.  'result' is sent to the
  * client indicating the command was processed successfully.  Only one call to
- * unixctl_command_reply() or unixctl_command_reply_error() may be made per
- * request. */
+ * unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request. */
 void
 unixctl_command_reply(struct unixctl_conn *conn, const char *result)
 {
-    unixctl_command_reply__(conn, true, result);
+    unixctl_command_reply__(conn, true, json_string_create__(result));
 }

 /* Replies to the active unixctl connection 'conn'. 'error' is sent to the
- * client indicating an error occurred processing the command.  Only one call to
- * unixctl_command_reply() or unixctl_command_reply_error() may be made per
- * request. */
+ * client indicating an error occurred processing the command.  Only one call
+ * to unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request. */
 void
 unixctl_command_reply_error(struct unixctl_conn *conn, const char *error)
 {
-    unixctl_command_reply__(conn, false, error);
+    unixctl_command_reply__(conn, false, json_string_create__(error));
+}
+
+/* Replies to the active unixctl connection 'conn'.  'result' is sent to the
+ * client indicating the command was processed successfully.  Only one call to
+ * unixctl_command_reply(), unixctl_command_reply_error() or
+ * unixctl_command_reply_json() may be made per request.
+ *
+ * Takes ownership of 'body'. */
+void
+unixctl_command_reply_json(struct unixctl_conn *conn, struct json *body)
+{
+    unixctl_command_reply__(conn, true, body);
 }

 /* Creates a unixctl server listening on 'path', which for POSIX may be:
@@ -518,6 +535,20 @@  unixctl_client_transact(struct jsonrpc *client, const char *command, int argc,
     } else if (reply->result) {
         if (reply->result->type == JSON_STRING) {
             *result = xstrdup(json_string(reply->result));
+        } else if (reply->result->type == JSON_OBJECT ||
+                   reply->result->type == JSON_ARRAY) {
+            /* TODO: How about other result types? */
+
+            /* TODO: Do we really want to prettyfy and sort the output?
+             * The benefit for users is probably minimal because they could
+             * simply use jq to format the output if needed. Since JSON output
+             * is meant to be consumed by machines, this pretty-printing is
+             * probably unnecessary in most cases.
+             * However, it might have its use in our unit tests because it
+             * allows us to make readable checks without having to introduce a
+             * dependency on jq.
+             */
+            *result = json_to_string(reply->result, JSSF_PRETTY | JSSF_SORT);
         } else {
             VLOG_WARN("%s: unexpected result type in JSON rpc reply: %s",
                       jsonrpc_get_name(client),
diff --git a/lib/unixctl.h b/lib/unixctl.h
index 4562dbc49..87c45fa14 100644
--- a/lib/unixctl.h
+++ b/lib/unixctl.h
@@ -17,6 +17,8 @@ 
 #ifndef UNIXCTL_H
 #define UNIXCTL_H 1

+#include "openvswitch/json.h"
+
 #ifdef  __cplusplus
 extern "C" {
 #endif
@@ -47,6 +49,7 @@  void unixctl_command_register(const char *name, const char *usage,
                               unixctl_cb_func *cb, void *aux);
 void unixctl_command_reply_error(struct unixctl_conn *, const char *error);
 void unixctl_command_reply(struct unixctl_conn *, const char *body);
+void unixctl_command_reply_json(struct unixctl_conn *, struct json *body);

 #ifdef  __cplusplus
 }
diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
index ba5706f6a..640f32829 100644
--- a/ofproto/ofproto-dpif.c
+++ b/ofproto/ofproto-dpif.c
@@ -20,6 +20,7 @@ 
 #include "bond.h"
 #include "bundle.h"
 #include "byte-order.h"
+#include "command-line.h"
 #include "connectivity.h"
 #include "connmgr.h"
 #include "coverage.h"
@@ -6344,8 +6345,103 @@  done:
     return changed;
 }

+static struct json *
+dpif_show_backer_json(const struct dpif_backer *backer)
+{
+    struct json *json_backer = json_object_create();
+
+    /* name */
+    json_object_put_string(json_backer, "name", dpif_name(backer->dpif));
+
+    /* stats */
+    struct dpif_dp_stats dp_stats;
+    dpif_get_dp_stats(backer->dpif, &dp_stats);
+    struct json *json_dp_stats = json_object_create();
+    json_object_put_format(json_dp_stats, "n_hit", "%"PRIu64, dp_stats.n_hit);
+    json_object_put_format(json_dp_stats, "n_missed", "%"PRIu64,
+                           dp_stats.n_missed);
+    json_object_put(json_backer, "stats", json_dp_stats);
+
+    /* ofprotos */
+    struct json *json_ofprotos = json_array_create_empty();
+    struct shash ofproto_shash;
+    shash_init(&ofproto_shash);
+    const struct shash_node **ofprotos = get_ofprotos(&ofproto_shash);
+    for (size_t i = 0; i < shash_count(&ofproto_shash); i++) {
+        struct ofproto_dpif *ofproto = ofprotos[i]->data;
+
+        if (ofproto->backer != backer) {
+            continue;
+        }
+
+        struct json *json_ofproto = json_object_create();
+
+        /* ofproto name */
+        json_object_put_string(json_ofproto, "name", ofproto->up.name);
+
+        /* ofproto ports */
+        struct json *json_ofproto_ports = json_array_create_empty();
+        const struct shash_node **ports;
+        ports = shash_sort(&ofproto->up.port_by_name);
+        for (size_t j = 0; j < shash_count(&ofproto->up.port_by_name); j++) {
+            const struct shash_node *port = ports[j];
+            struct ofport *ofport = port->data;
+
+            struct json * json_ofproto_port = json_object_create();
+            /* ofproto port netdev name */
+            json_object_put_string(json_ofproto_port, "netdev_name",
+                                   netdev_get_name(ofport->netdev));
+            /* ofproto port ofp port */
+            json_object_put_format(json_ofproto_port, "ofp_port", "%u",
+                                   ofport->ofp_port);
+
+            /* ofproto port odp port */
+            odp_port_t odp_port = ofp_port_to_odp_port(ofproto,
+                                                       ofport->ofp_port);
+            if (odp_port != ODPP_NONE) {
+                json_object_put_format(json_ofproto_port, "odp_port",
+                                       "%"PRIu32, odp_port);
+            } else {
+                json_object_put_string(json_ofproto_port, "odp_port", "none");
+            }
+
+            /* ofproto port netdev type */
+            json_object_put_string(json_ofproto_port, "netdev_type",
+                                   netdev_get_type(ofport->netdev));
+
+            /* ofproto port config */
+            struct json *json_port_config = json_object_create();
+            struct smap config;
+            smap_init(&config);
+            if (!netdev_get_config(ofport->netdev, &config)) {
+                struct smap_node *node;
+
+                SMAP_FOR_EACH (node, &config) {
+                    json_object_put_string(json_port_config, node->key,
+                                           node->value);
+                }
+            }
+            smap_destroy(&config);
+            json_object_put(json_ofproto_port, "netdev_config",
+                            json_port_config);
+
+            json_array_add(json_ofproto_ports, json_ofproto_port);
+        } /* ofproto port */
+
+        free(ports);
+        json_object_put(json_ofproto, "ports", json_ofproto_ports);
+
+        json_array_add(json_ofprotos, json_ofproto);
+    } /* ofproto */
+    shash_destroy(&ofproto_shash);
+    free(ofprotos);
+
+    json_object_put(json_backer, "ofprotos", json_ofprotos);
+    return json_backer;
+}
+
 static void
-dpif_show_backer(const struct dpif_backer *backer, struct ds *ds)
+dpif_show_backer_text(const struct dpif_backer *backer, struct ds *ds)
 {
     const struct shash_node **ofprotos;
     struct dpif_dp_stats dp_stats;
@@ -6409,21 +6505,52 @@  dpif_show_backer(const struct dpif_backer *backer, struct ds *ds)
 }

 static void
-ofproto_unixctl_dpif_show(struct unixctl_conn *conn, int argc OVS_UNUSED,
-                          const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED)
+ofproto_unixctl_dpif_show(struct unixctl_conn *conn, int argc,
+                          const char *argv[], void *aux OVS_UNUSED)
 {
     struct ds ds = DS_EMPTY_INITIALIZER;
-    const struct shash_node **backers;
-    int i;
+    enum ovs_output_fmt fmt = OVS_OUTPUT_FMT_TEXT;
+
+    /* Parse options. Both getopt() and getopt_long() cannot be used here
+     * because they use global variables and are already used by the caller.
+     */
+    /* TODO: Modify getopt()/getopt_long() to use local storage or use
+     *       alternatives like presented in
+     *       https://nullprogram.com/blog/2014/10/12/ ? */
+    for (int i = 1, parsed = 0; i < argc; i += parsed) {
+        char *error = ovs_cmdl_parse_output_fmt(i, argc, argv, &fmt, &parsed);

-    backers = shash_sort(&all_dpif_backers);
-    for (i = 0; i < shash_count(&all_dpif_backers); i++) {
-        dpif_show_backer(backers[i]->data, &ds);
+        if (error) {
+            unixctl_command_reply_error(conn, error);
+            free(error);
+            return;
+        }
+
+        if (!parsed) {
+            ds_put_format(&ds, "Unrecognized option %s", argv[i]);
+            unixctl_command_reply_error(conn, ds_cstr(&ds));
+            ds_destroy(&ds);
+            return;
+        }
     }
-    free(backers);

-    unixctl_command_reply(conn, ds_cstr(&ds));
-    ds_destroy(&ds);
+    if (fmt == OVS_OUTPUT_FMT_JSON) {
+        struct json *backers = json_array_create_empty();
+        const struct shash_node *backer;
+        SHASH_FOR_EACH (backer, &all_dpif_backers) {
+            json_array_add(backers, dpif_show_backer_json(backer->data));
+        }
+        unixctl_command_reply_json(conn, backers);
+    } else {
+        const struct shash_node **backers = shash_sort(&all_dpif_backers);
+        for (int i = 0; i < shash_count(&all_dpif_backers); i++) {
+            dpif_show_backer_text(backers[i]->data, &ds);
+        }
+        free(backers);
+
+        unixctl_command_reply(conn, ds_cstr(&ds));
+        ds_destroy(&ds);
+    }
 }

 static void
@@ -6595,8 +6722,8 @@  ofproto_unixctl_init(void)
                              ofproto_unixctl_mcast_snooping_show, NULL);
     unixctl_command_register("dpif/dump-dps", "", 0, 0,
                              ofproto_unixctl_dpif_dump_dps, NULL);
-    unixctl_command_register("dpif/show", "", 0, 0, ofproto_unixctl_dpif_show,
-                             NULL);
+    unixctl_command_register("dpif/show", "[(-o|--output=)text|json]", 0, 2,
+                             ofproto_unixctl_dpif_show, NULL);
     unixctl_command_register("dpif/show-dp-features", "bridge", 1, 1,
                              ofproto_unixctl_dpif_show_dp_features, NULL);
     unixctl_command_register("dpif/dump-flows",
diff --git a/tests/pmd.at b/tests/pmd.at
index 7bdaca9e7..154fd48a3 100644
--- a/tests/pmd.at
+++ b/tests/pmd.at
@@ -100,6 +100,35 @@  dummy@ovs-dummy: hit:0 missed:0
     p0 1/1: (dummy-pmd: configured_rx_queues=1, configured_tx_queues=<cleared>, requested_rx_queues=1, requested_tx_queues=<cleared>)
 ])

+AT_CHECK([ovs-appctl dpif/show --output json | sed 's/\(tx_queues": "\)[[0-9]]*"/\1<cleared>"/g'], [0], [dnl
+[[
+  {
+    "name": "dummy@ovs-dummy",
+    "ofprotos": [
+      {
+        "name": "br0",
+        "ports": [
+          {
+            "netdev_config": {
+              },
+            "netdev_name": "br0",
+            "netdev_type": "dummy-internal",
+            "odp_port": "100",
+            "ofp_port": "65534"},
+          {
+            "netdev_config": {
+              "configured_rx_queues": "1",
+              "configured_tx_queues": "<cleared>",
+              "requested_rx_queues": "1",
+              "requested_tx_queues": "<cleared>"},
+            "netdev_name": "p0",
+            "netdev_type": "dummy-pmd",
+            "odp_port": "1",
+            "ofp_port": "1"}]}],
+    "stats": {
+      "n_hit": "0",
+      "n_missed": "0"}}]]])
+
 OVS_VSWITCHD_STOP
 AT_CLEANUP