diff mbox series

[ovs-dev,v11,1/6] Add global option for JSON output to ovs-appctl.

Message ID 20240516101939.54900-2-jmeng@redhat.com
State Superseded, archived
Headers show
Series Add global option to output JSON from ovs-appctl cmds. | expand

Checks

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

Commit Message

Jakob Meng May 16, 2024, 10:19 a.m. UTC
From: Jakob Meng <code@jakobmeng.de>

For monitoring systems such as Prometheus it would be beneficial if
OVS would expose statistics in a machine-readable format.

This patch introduces support for different output formats to
ovs-appctl. It gains a global option '-f,--format' which changes it to
print a JSON document instead of plain-text for humans. For example, a
later patch implements support for
'ovs-appctl --format json dpif/show'. By default, the output format
is plain-text as before.

A new 'set-options' command has been added to lib/unixctl.c which
allows to change the output format of the commands executed afterwards
on the same socket connection. It is supposed to be run by ovs-appctl
transparently for the user when a specific output format has been
requested.
For example, when a user calls 'ovs-appctl --format json dpif/show',
then ovs-appctl will call 'set-options' to set the output format as
requested by the user and afterwards it will call the actual command
'dpif/show'.
This ovs-appctl behaviour has been implemented in a backward compatible
way. One can use an updated client (ovs-appctl) with an old server
(ovs-vswitchd) and vice versa. Of course, JSON output only works when
both sides have been updated.

Two access functions unixctl_command_{get,set}_output_format() and a
unixctl_command_reply_json function have been added to lib/unixctl.h:
unixctl_command_get_output_format() is supposed to be used in commands
like 'dpif/show' to query the requested output format. When JSON output
has been selected, the unixctl_command_reply_json() function can be
used to return JSON objects to the client (ovs-appctl) instead of
plain-text with the unixctl_command_reply{,_error}() functions.

When JSON has been requested but a command has not implemented JSON
output the plain-text output will be wrapped in a provisional JSON
document with the following structure:

  {"reply":"$PLAIN_TEXT_HERE","reply-format":"plain"}

Thus commands which have been executed successfully will not fail when
they try to render the output at a later stage.

A test for the 'version' command has been implemented which shows how
the provisional JSON document looks like in practice. For a cleaner
JSON document, the trailing newline has been moved from the program
version string to function ovs_print_version(). This way, the
plain-text output of the 'version' command has not changed.

Output formatting has been moved from unixctl_client_transact() in
lib/unixctl.c to utilities/ovs-appctl.c. The former merely returns the
JSON objects returned from the server and the latter is now responsible
for printing it properly.

In popular tools like kubectl the option for output control is usually
called '-o|--output' instead of '-f,--format'. But ovs-appctl already
has an short option '-o' which prints the available ovs-appctl options
('--option'). The now choosen name also better alignes with
ovsdb-client where '-f,--format' controls output formatting.

Reported-at: https://bugzilla.redhat.com/1824861
Signed-off-by: Jakob Meng <code@jakobmeng.de>
---
 Documentation/ref/ovs-appctl.8.rst |  12 ++
 NEWS                               |   3 +
 lib/unixctl.c                      | 195 ++++++++++++++++++++++-------
 lib/unixctl.h                      |  20 ++-
 lib/util.c                         |   6 +-
 python/ovs/unixctl/server.py       |   3 -
 tests/appctl.py                    |   5 +
 tests/ovs-vswitchd.at              |  11 ++
 utilities/ovs-appctl.c             | 130 ++++++++++++++++---
 9 files changed, 314 insertions(+), 71 deletions(-)

Comments

0-day Robot May 16, 2024, 10:40 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:
WARNING: Line lacks whitespace around operator
WARNING: Line lacks whitespace around operator
#676 FILE: utilities/ovs-appctl.c:150:
  -f, --format=FMT   Output format. One of: 'json', or 'text'\n\

Lines checked: 791, Warnings: 2, Errors: 0


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/Documentation/ref/ovs-appctl.8.rst b/Documentation/ref/ovs-appctl.8.rst
index 3ce02e984..9619c1226 100644
--- a/Documentation/ref/ovs-appctl.8.rst
+++ b/Documentation/ref/ovs-appctl.8.rst
@@ -8,6 +8,7 @@  Synopsis
 ``ovs-appctl``
 [``--target=``<target> | ``-t`` <target>]
 [``--timeout=``<secs> | ``-T`` <secs>]
+[``--format=``<format> | ``-f`` <format>]
 <command> [<arg>...]
 
 ``ovs-appctl --help``
@@ -67,6 +68,17 @@  In normal use only a single option is accepted:
   runtime to approximately <secs> seconds.  If the timeout expires,
   ``ovs-appctl`` exits with a ``SIGALRM`` signal.
 
+* ``-f <format>`` or ``--format=<format>``
+
+  Tells ``ovs-appctl`` which output format to use. By default, or with a
+  <format> of ``text``, ``ovs-appctl`` will print plain-text for humans.
+  When <format> is ``json``, ``ovs-appctl`` will return a JSON document.
+  When ``json`` is requested, but a command has not implemented JSON
+  output, the plain-text output will be wrapped in a provisional JSON
+  document with the following structure:
+
+  ``{"reply-format":"plain","reply":"$PLAIN_TEXT_HERE"}``
+
 Common Commands
 ===============
 
diff --git a/NEWS b/NEWS
index b92cec532..3c52e5ec1 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,8 @@ 
 Post-v3.3.0
 --------------------
+   - ovs-appctl:
+     * Added new option [-f|--format] to choose the output format, e.g. 'json'
+       or 'text' (by default).
    - Userspace datapath:
      * Conntrack now supports 'random' flag for selecting ports in a range
        while natting and 'persistent' flag for selection of the IP address
diff --git a/lib/unixctl.c b/lib/unixctl.c
index 103357ee9..c430eac0b 100644
--- a/lib/unixctl.c
+++ b/lib/unixctl.c
@@ -17,11 +17,13 @@ 
 #include <config.h>
 #include "unixctl.h"
 #include <errno.h>
+#include <getopt.h>
 #include <unistd.h>
+#include "command-line.h"
 #include "coverage.h"
 #include "dirs.h"
 #include "openvswitch/dynamic-string.h"
-#include "openvswitch/json.h"
+#include "json.h"
 #include "jsonrpc.h"
 #include "openvswitch/list.h"
 #include "openvswitch/poll-loop.h"
@@ -50,6 +52,8 @@  struct unixctl_conn {
     /* Only one request can be in progress at a time.  While the request is
      * being processed, 'request_id' is populated, otherwise it is null. */
     struct json *request_id;   /* ID of the currently active request. */
+
+    enum unixctl_output_fmt fmt; /* Output format of current connection. */
 };
 
 /* Server for control connection. */
@@ -63,6 +67,42 @@  static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
 
 static struct shash commands = SHASH_INITIALIZER(&commands);
 
+const char *
+unixctl_output_fmt_to_string(enum unixctl_output_fmt fmt)
+{
+    switch (fmt) {
+    case UNIXCTL_OUTPUT_FMT_TEXT:
+        return "text";
+
+    case UNIXCTL_OUTPUT_FMT_JSON:
+        return "json";
+
+    default:
+        return NULL;
+    }
+}
+
+struct json *
+unixctl_output_fmt_to_json(enum unixctl_output_fmt fmt)
+{
+    const char *string = unixctl_output_fmt_to_string(fmt);
+    return string ? json_string_create(string) : NULL;
+}
+
+bool
+unixctl_output_fmt_from_string(const char *string,
+                               enum unixctl_output_fmt *fmt)
+{
+    if (!strcmp(string, "text")) {
+        *fmt = UNIXCTL_OUTPUT_FMT_TEXT;
+    } else if (!strcmp(string, "json")) {
+        *fmt = UNIXCTL_OUTPUT_FMT_JSON;
+    } else {
+        return false;
+    }
+    return true;
+}
+
 static void
 unixctl_list_commands(struct unixctl_conn *conn, int argc OVS_UNUSED,
                       const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED)
@@ -94,6 +134,48 @@  unixctl_version(struct unixctl_conn *conn, int argc OVS_UNUSED,
     unixctl_command_reply(conn, ovs_get_program_version());
 }
 
+static void
+unixctl_set_options(struct unixctl_conn *conn, int argc, const char *argv[],
+                    void *aux OVS_UNUSED)
+{
+    struct ovs_cmdl_parsed_option *parsed_options = NULL;
+    size_t n_parsed_options;
+    char *error = NULL;
+
+    static const struct option options[] = {
+        {"format", required_argument, NULL, 'f'},
+        {NULL, 0, NULL, 0},
+    };
+
+    error = ovs_cmdl_parse_all(argc--, (char **) (argv++), options,
+                               &parsed_options, &n_parsed_options);
+    if (error) {
+        goto error;
+    }
+
+    for (size_t i = 0; i < n_parsed_options; i++) {
+        struct ovs_cmdl_parsed_option *parsed_option = parsed_options + i;
+        if (parsed_option->o == (options + 0)) {
+            /* format */
+            if (!unixctl_output_fmt_from_string(parsed_option->arg,
+                                                &conn->fmt)) {
+                error = xasprintf("option format has invalid value %s",
+                                  parsed_option->arg);
+                goto error;
+            }
+        } else  {
+            OVS_NOT_REACHED();
+        }
+    }
+    unixctl_command_reply(conn, NULL);
+    free(parsed_options);
+    return;
+error:
+    unixctl_command_reply_error(conn, error);
+    free(error);
+    free(parsed_options);
+}
+
 /* Registers a unixctl command with the given 'name'.  'usage' describes the
  * arguments to the command; it is used only for presentation to the user in
  * "list-commands" output.  (If 'usage' is NULL, then the command is hidden.)
@@ -128,36 +210,41 @@  unixctl_command_register(const char *name, const char *usage,
     shash_add(&commands, name, command);
 }
 
+enum unixctl_output_fmt
+unixctl_command_get_output_format(struct unixctl_conn *conn)
+{
+    return conn->fmt;
+}
+
+void
+unixctl_command_set_output_format(struct unixctl_conn *conn,
+                                  enum unixctl_output_fmt fmt)
+{
+    conn->fmt = fmt;
+}
+
+/* Takes ownership of the 'body'. */
 static void
 unixctl_command_reply__(struct unixctl_conn *conn,
-                        bool success, const char *body)
+                        bool success, struct json *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));
-    } else {
-        body_json = json_string_create(body);
-    }
-
     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, JSSF_SORT);
         VLOG_DBG("replying with %s, id=%s: \"%s\"",
-                 success ? "success" : "error", id, body);
+                 success ? "success" : "error", id, msg);
+        free(msg);
         free(id);
     }
 
@@ -169,23 +256,52 @@  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. */
+ * client indicating the command was processed successfully.  'result' should
+ * be plain-text; use unixctl_command_reply_json() to return a JSON document
+ * when JSON output has been requested.  Only one call to
+ * unixctl_command_reply*() functions may be made per request. */
 void
 unixctl_command_reply(struct unixctl_conn *conn, const char *result)
 {
-    unixctl_command_reply__(conn, true, result);
+    struct json * json_result = json_string_create(result ? result : "");
+
+    if (conn->fmt == UNIXCTL_OUTPUT_FMT_JSON) {
+        /* Wrap plain-text reply in provisional JSON document when JSON output
+         * has been requested. */
+        struct json *json_reply = json_object_create();
+
+        json_object_put_string(json_reply, "reply-format", "plain");
+        json_object_put(json_reply, "reply", json_result);
+
+        json_result = json_reply;
+    }
+
+    unixctl_command_reply__(conn, true, json_result);
+}
+
+/* Replies to the active unixctl connection 'conn'.  'body' is sent to the
+ * client indicating the command was processed successfully.  Use this function
+ * when JSON output has been requested; otherwise use unixctl_command_reply()
+ * for plain-text output.  Only one call to unixctl_command_reply*() functions
+ * may be made per request.
+ *
+ * Takes ownership of the 'body'. */
+void
+unixctl_command_reply_json(struct unixctl_conn *conn, struct json *body)
+{
+    ovs_assert(conn->fmt == UNIXCTL_OUTPUT_FMT_JSON);
+    unixctl_command_reply__(conn, true, body);
 }
 
 /* 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.  'error' should
+ * be plain-text.  Only one call to unixctl_command_reply*() functions 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 ? error : ""));
 }
 
 /* Creates a unixctl server listening on 'path', which for POSIX may be:
@@ -250,6 +366,8 @@  unixctl_server_create(const char *path, struct unixctl_server **serverp)
     unixctl_command_register("list-commands", "", 0, 0, unixctl_list_commands,
                              NULL);
     unixctl_command_register("version", "", 0, 0, unixctl_version, NULL);
+    unixctl_command_register("set-options", "[--format text|json]", 0, 2,
+                             unixctl_set_options, NULL);
 
     struct unixctl_server *server = xmalloc(sizeof *server);
     server->listener = listener;
@@ -381,6 +499,7 @@  unixctl_server_run(struct unixctl_server *server)
             struct unixctl_conn *conn = xzalloc(sizeof *conn);
             ovs_list_push_back(&server->conns, &conn->node);
             conn->rpc = jsonrpc_open(stream);
+            conn->fmt = UNIXCTL_OUTPUT_FMT_TEXT;
         } else if (error == EAGAIN) {
             break;
         } else {
@@ -483,10 +602,10 @@  unixctl_client_create(const char *path, struct jsonrpc **client)
  * '*err' if not NULL. */
 int
 unixctl_client_transact(struct jsonrpc *client, const char *command, int argc,
-                        char *argv[], char **result, char **err)
+                        char *argv[], struct json **result, struct json **err)
 {
-    struct jsonrpc_msg *request, *reply;
     struct json **json_args, *params;
+    struct jsonrpc_msg *request, *reply;
     int error, i;
 
     *result = NULL;
@@ -506,25 +625,11 @@  unixctl_client_transact(struct jsonrpc *client, const char *command, int argc,
         return error;
     }
 
-    if (reply->error) {
-        if (reply->error->type == JSON_STRING) {
-            *err = xstrdup(json_string(reply->error));
-        } else {
-            VLOG_WARN("%s: unexpected error type in JSON RPC reply: %s",
-                      jsonrpc_get_name(client),
-                      json_type_to_string(reply->error->type));
-            error = EINVAL;
-        }
-    } else if (reply->result) {
-        if (reply->result->type == JSON_STRING) {
-            *result = xstrdup(json_string(reply->result));
-        } else {
-            VLOG_WARN("%s: unexpected result type in JSON rpc reply: %s",
-                      jsonrpc_get_name(client),
-                      json_type_to_string(reply->result->type));
-            error = EINVAL;
-        }
-    }
+    ovs_assert(!reply->result || !reply->error);
+    *result = reply->result;
+    reply->result = NULL;
+    *err = reply->error;
+    reply->error = NULL;
 
     jsonrpc_msg_destroy(reply);
     return error;
diff --git a/lib/unixctl.h b/lib/unixctl.h
index 4562dbc49..61b31cb3a 100644
--- a/lib/unixctl.h
+++ b/lib/unixctl.h
@@ -17,10 +17,22 @@ 
 #ifndef UNIXCTL_H
 #define UNIXCTL_H 1
 
+#include <stdbool.h>
+
 #ifdef  __cplusplus
 extern "C" {
 #endif
 
+struct json;
+enum unixctl_output_fmt {
+    UNIXCTL_OUTPUT_FMT_TEXT = 1 << 0,
+    UNIXCTL_OUTPUT_FMT_JSON = 1 << 1
+};
+
+const char *unixctl_output_fmt_to_string(enum unixctl_output_fmt);
+struct json *unixctl_output_fmt_to_json(enum unixctl_output_fmt);
+bool unixctl_output_fmt_from_string(const char *, enum unixctl_output_fmt *);
+
 /* Server for Unix domain socket control connection. */
 struct unixctl_server;
 int unixctl_server_create(const char *path, struct unixctl_server **);
@@ -36,7 +48,7 @@  int unixctl_client_create(const char *path, struct jsonrpc **client);
 int unixctl_client_transact(struct jsonrpc *client,
                             const char *command,
                             int argc, char *argv[],
-                            char **result, char **error);
+                            struct json **result, struct json **error);
 
 /* Command registration. */
 struct unixctl_conn;
@@ -45,8 +57,14 @@  typedef void unixctl_cb_func(struct unixctl_conn *,
 void unixctl_command_register(const char *name, const char *usage,
                               int min_args, int max_args,
                               unixctl_cb_func *cb, void *aux);
+enum unixctl_output_fmt
+unixctl_command_get_output_format(struct unixctl_conn *);
+void unixctl_command_set_output_format(struct unixctl_conn *,
+                                       enum unixctl_output_fmt);
 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/lib/util.c b/lib/util.c
index 5c31d983a..87d5a4a90 100644
--- a/lib/util.c
+++ b/lib/util.c
@@ -619,11 +619,11 @@  ovs_set_program_name(const char *argv0, const char *version)
 
     free(program_version);
     if (!strcmp(version, VERSION)) {
-        program_version = xasprintf("%s (Open vSwitch) "VERSION"\n",
+        program_version = xasprintf("%s (Open vSwitch) "VERSION,
                                     program_name);
     } else {
         program_version = xasprintf("%s %s\n"
-                                    "Open vSwitch Library "VERSION"\n",
+                                    "Open vSwitch Library "VERSION,
                                     program_name, version);
     }
 }
@@ -760,7 +760,7 @@  ovs_get_program_name(void)
 void
 ovs_print_version(uint8_t min_ofp, uint8_t max_ofp)
 {
-    printf("%s", program_version);
+    printf("%s\n", program_version);
     if (min_ofp || max_ofp) {
         printf("OpenFlow versions %#x:%#x\n", min_ofp, max_ofp);
     }
diff --git a/python/ovs/unixctl/server.py b/python/ovs/unixctl/server.py
index b9cb52fad..d24a7092c 100644
--- a/python/ovs/unixctl/server.py
+++ b/python/ovs/unixctl/server.py
@@ -87,9 +87,6 @@  class UnixctlConnection(object):
         if body is None:
             body = ""
 
-        if body and not body.endswith("\n"):
-            body += "\n"
-
         if success:
             reply = Message.create_reply(body, self._request_id)
         else:
diff --git a/tests/appctl.py b/tests/appctl.py
index b85b364fa..e5cc28138 100644
--- a/tests/appctl.py
+++ b/tests/appctl.py
@@ -63,11 +63,16 @@  def main():
         ovs.util.ovs_fatal(err_no, "%s: transaction error" % target)
     elif error is not None:
         sys.stderr.write(error)
+        if error and not error.endswith("\n"):
+            sys.stderr.write("\n")
+
         ovs.util.ovs_error(0, "%s: server returned an error" % target)
         sys.exit(2)
     else:
         assert result is not None
         sys.stdout.write(result)
+        if result and not result.endswith("\n"):
+            sys.stdout.write("\n")
 
 
 if __name__ == '__main__':
diff --git a/tests/ovs-vswitchd.at b/tests/ovs-vswitchd.at
index 977b2eba1..1ae7fcc32 100644
--- a/tests/ovs-vswitchd.at
+++ b/tests/ovs-vswitchd.at
@@ -265,3 +265,14 @@  OFPT_FEATURES_REPLY: dpid:$orig_dpid
 
 OVS_VSWITCHD_STOP
 AT_CLEANUP
+
+AT_SETUP([ovs-vswitchd version])
+OVS_VSWITCHD_START
+
+AT_CHECK([ovs-appctl version], [0], [ignore])
+ovs_version=$(ovs-appctl version)
+
+AT_CHECK_UNQUOTED([ovs-appctl --format json version], [0], [dnl
+{"reply":"$ovs_version","reply-format":"plain"}])
+
+AT_CLEANUP
diff --git a/utilities/ovs-appctl.c b/utilities/ovs-appctl.c
index ba0c172e6..a359a14f1 100644
--- a/utilities/ovs-appctl.c
+++ b/utilities/ovs-appctl.c
@@ -26,57 +26,103 @@ 
 #include "daemon.h"
 #include "dirs.h"
 #include "openvswitch/dynamic-string.h"
+#include "json.h"
 #include "jsonrpc.h"
 #include "process.h"
 #include "timeval.h"
+#include "svec.h"
 #include "unixctl.h"
 #include "util.h"
 #include "openvswitch/vlog.h"
 
 static void usage(void);
-static const char *parse_command_line(int argc, char *argv[]);
+
+/* Parsed command line args. */
+struct cmdl_args {
+    enum unixctl_output_fmt format;
+    char *target;
+};
+
+static struct cmdl_args *cmdl_args_create(void);
+static struct cmdl_args *parse_command_line(int argc, char *argv[]);
 static struct jsonrpc *connect_to_target(const char *target);
+static char * reply_to_string(struct json *reply, enum unixctl_output_fmt fmt);
 
 int
 main(int argc, char *argv[])
 {
-    char *cmd_result, *cmd_error;
+    struct svec opt_argv = SVEC_EMPTY_INITIALIZER;
+    struct json *cmd_result, *cmd_error;
     struct jsonrpc *client;
+    struct cmdl_args *args;
     char *cmd, **cmd_argv;
-    const char *target;
+    char *msg = NULL;
     int cmd_argc;
     int error;
 
     set_program_name(argv[0]);
 
     /* Parse command line and connect to target. */
-    target = parse_command_line(argc, argv);
-    client = connect_to_target(target);
+    args = parse_command_line(argc, argv);
+    client = connect_to_target(args->target);
 
-    /* Transact request and process reply. */
+    /* Transact options request (if required) and process reply. */
+    if (args->format != UNIXCTL_OUTPUT_FMT_TEXT) {
+        svec_add(&opt_argv, "--format");
+        svec_add(&opt_argv, unixctl_output_fmt_to_string(args->format));
+    }
+    svec_terminate(&opt_argv);
+
+    if (!svec_is_empty(&opt_argv)) {
+        error = unixctl_client_transact(client, "set-options",
+                                        opt_argv.n, opt_argv.names,
+                                        &cmd_result, &cmd_error);
+
+        if (error) {
+            ovs_fatal(error, "%s: transaction error", args->target);
+        }
+
+        if (cmd_error) {
+            jsonrpc_close(client);
+            msg = reply_to_string(cmd_error, UNIXCTL_OUTPUT_FMT_TEXT);
+            fputs(msg, stderr);
+            ovs_error(0, "%s: server returned an error", args->target);
+            exit(2);
+        }
+
+        json_destroy(cmd_result);
+        json_destroy(cmd_error);
+    }
+    svec_destroy(&opt_argv);
+
+    /* Transact command request and process reply. */
     cmd = argv[optind++];
     cmd_argc = argc - optind;
     cmd_argv = cmd_argc ? argv + optind : NULL;
     error = unixctl_client_transact(client, cmd, cmd_argc, cmd_argv,
                                     &cmd_result, &cmd_error);
     if (error) {
-        ovs_fatal(error, "%s: transaction error", target);
+        ovs_fatal(error, "%s: transaction error", args->target);
     }
 
     if (cmd_error) {
         jsonrpc_close(client);
-        fputs(cmd_error, stderr);
-        ovs_error(0, "%s: server returned an error", target);
+        msg = reply_to_string(cmd_error, UNIXCTL_OUTPUT_FMT_TEXT);
+        fputs(msg, stderr);
+        ovs_error(0, "%s: server returned an error", args->target);
         exit(2);
     } else if (cmd_result) {
-        fputs(cmd_result, stdout);
+        msg = reply_to_string(cmd_result, args->format);
+        fputs(msg, stdout);
     } else {
         OVS_NOT_REACHED();
     }
 
     jsonrpc_close(client);
-    free(cmd_result);
-    free(cmd_error);
+    json_destroy(cmd_result);
+    json_destroy(cmd_error);
+    free(args);
+    free(msg);
     return 0;
 }
 
@@ -101,13 +147,26 @@  Common commands:\n\
   vlog/reopen        Make the program reopen its log file\n\
 Other options:\n\
   --timeout=SECS     wait at most SECS seconds for a response\n\
+  -f, --format=FMT   Output format. One of: 'json', or 'text'\n\
+                     (default: text)\n\
   -h, --help         Print this helpful information\n\
   -V, --version      Display ovs-appctl version information\n",
            program_name, program_name);
     exit(EXIT_SUCCESS);
 }
 
-static const char *
+static struct cmdl_args *
+cmdl_args_create(void)
+{
+    struct cmdl_args *args = xmalloc(sizeof *args);
+
+    args->format = UNIXCTL_OUTPUT_FMT_TEXT;
+    args->target = NULL;
+
+    return args;
+}
+
+static struct cmdl_args *
 parse_command_line(int argc, char *argv[])
 {
     enum {
@@ -117,6 +176,7 @@  parse_command_line(int argc, char *argv[])
     static const struct option long_options[] = {
         {"target", required_argument, NULL, 't'},
         {"execute", no_argument, NULL, 'e'},
+        {"format", required_argument, NULL, 'f'},
         {"help", no_argument, NULL, 'h'},
         {"option", no_argument, NULL, 'o'},
         {"version", no_argument, NULL, 'V'},
@@ -126,11 +186,10 @@  parse_command_line(int argc, char *argv[])
     };
     char *short_options_ = ovs_cmdl_long_options_to_short_options(long_options);
     char *short_options = xasprintf("+%s", short_options_);
-    const char *target;
-    int e_options;
+    struct cmdl_args *args = cmdl_args_create();
     unsigned int timeout = 0;
+    int e_options;
 
-    target = NULL;
     e_options = 0;
     for (;;) {
         int option;
@@ -141,10 +200,10 @@  parse_command_line(int argc, char *argv[])
         }
         switch (option) {
         case 't':
-            if (target) {
+            if (args->target) {
                 ovs_fatal(0, "-t or --target may be specified only once");
             }
-            target = optarg;
+            args->target = optarg;
             break;
 
         case 'e':
@@ -157,6 +216,12 @@  parse_command_line(int argc, char *argv[])
             }
             break;
 
+        case 'f':
+            if (!unixctl_output_fmt_from_string(optarg, &args->format)) {
+                ovs_fatal(0, "value %s on -f or --format is invalid", optarg);
+            }
+            break;
+
         case 'h':
             usage();
             break;
@@ -194,7 +259,10 @@  parse_command_line(int argc, char *argv[])
                   "(use --help for help)");
     }
 
-    return target ? target : "ovs-vswitchd";
+    if (!args->target) {
+        args->target = "ovs-vswitchd";
+    }
+    return args;
 }
 
 static struct jsonrpc *
@@ -236,3 +304,27 @@  connect_to_target(const char *target)
     return client;
 }
 
+/* The caller is responsible for freeing the returned string, with free(), when
+ * it is no longer needed. */
+static char *
+reply_to_string(struct json *reply, enum unixctl_output_fmt fmt)
+{
+    ovs_assert(reply);
+
+    if (fmt == UNIXCTL_OUTPUT_FMT_TEXT && reply->type == JSON_STRING) {
+        const char * body = json_string(reply);
+
+        if (body[0] && body[strlen(body) - 1] != '\n') {
+            return xasprintf("%s\n", body);
+        } else {
+            return xstrdup(body);
+        }
+    }
+
+    if (fmt != UNIXCTL_OUTPUT_FMT_JSON) {
+        ovs_error(0, "Unexpected reply type in JSON rpc reply: %s",
+                  json_type_to_string(reply->type));
+    }
+
+    return json_to_string(reply, JSSF_SORT);
+}