diff --git a/Documentation/ref/ovs-appctl.8.rst b/Documentation/ref/ovs-appctl.8.rst index 3ce02e984..148cc7632 100644 --- a/Documentation/ref/ovs-appctl.8.rst +++ b/Documentation/ref/ovs-appctl.8.rst @@ -8,6 +8,7 @@ Synopsis ``ovs-appctl`` [``--target=`` | ``-t`` ] [``--timeout=`` | ``-T`` ] +[``--format=`` | ``-f`` ] [...] ``ovs-appctl --help`` @@ -67,6 +68,17 @@ In normal use only a single option is accepted: runtime to approximately seconds. If the timeout expires, ``ovs-appctl`` exits with a ``SIGALRM`` signal. +* ``-f `` or ``--format=`` + + Tells ``ovs-appctl`` which output format to use. By default, or with a + of ``text``, ``ovs-appctl`` will print plain-text for humans. + When 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 e0359b759..f182647c7 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ Post-v3.3.0 -------------------- - Option '--mlockall' now only locks memory pages on fault, if possible. This also makes it compatible with vHost Post-copy Live Migration. + - 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..e7ce77e2c 100644 --- a/lib/unixctl.c +++ b/lib/unixctl.c @@ -17,7 +17,9 @@ #include #include "unixctl.h" #include +#include #include +#include "command-line.h" #include "coverage.h" #include "dirs.h" #include "openvswitch/dynamic-string.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,30 @@ 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 ""; + } +} + +bool +unixctl_output_fmt_from_string(const char *string, + enum unixctl_output_fmt *fmt) +{ + if (!strcasecmp(string, "text")) { + *fmt = UNIXCTL_OUTPUT_FMT_TEXT; + } else if (!strcasecmp(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 +122,52 @@ 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]; + + switch (parsed_option->o->val) { + case 'f': + if (!unixctl_output_fmt_from_string(parsed_option->arg, + &conn->fmt)) { + error = xasprintf("option format has invalid value %s", + parsed_option->arg); + goto error; + } + break; + + default: + 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 +202,35 @@ 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; +} + +/* 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 +242,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 +352,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]", 1, 2, + unixctl_set_options, NULL); struct unixctl_server *server = xmalloc(sizeof *server); server->listener = listener; @@ -381,6 +485,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,7 +588,7 @@ 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; @@ -506,24 +611,15 @@ 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; - } + if (reply->result && reply->error) { + VLOG_WARN("unexpected response when communicating with %s: %s\n %s", + jsonrpc_get_name(client), + json_to_string(reply->result, JSSF_SORT), + json_to_string(reply->error, JSSF_SORT)); + error = EINVAL; + } else { + *result = json_nullable_clone(reply->result); + *err = json_nullable_clone(reply->error); } jsonrpc_msg_destroy(reply); diff --git a/lib/unixctl.h b/lib/unixctl.h index 4562dbc49..1965f100d 100644 --- a/lib/unixctl.h +++ b/lib/unixctl.h @@ -17,10 +17,21 @@ #ifndef UNIXCTL_H #define UNIXCTL_H 1 +#include + #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); +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 +47,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 +56,12 @@ 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_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 3a6351a2f..84e8c4966 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..b1ae1ae1e 100644 --- a/tests/ovs-vswitchd.at +++ b/tests/ovs-vswitchd.at @@ -265,3 +265,15 @@ 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..721698755 100644 --- a/utilities/ovs-appctl.c +++ b/utilities/ovs-appctl.c @@ -26,57 +26,105 @@ #include "daemon.h" #include "dirs.h" #include "openvswitch/dynamic-string.h" +#include "openvswitch/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); + free(msg); + 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); + free(msg); + 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); + free(msg); } else { OVS_NOT_REACHED(); } jsonrpc_close(client); - free(cmd_result); - free(cmd_error); + json_destroy(cmd_result); + json_destroy(cmd_error); + free(args); return 0; } @@ -101,13 +149,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 +178,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 +188,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 +202,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 +218,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 +261,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 +306,30 @@ 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) { + ovs_error(0, "Unexpected reply type in JSON rpc reply: %s", + json_type_to_string(reply->type)); + exit(2); + } + + struct ds ds = DS_EMPTY_INITIALIZER; + + if (fmt == UNIXCTL_OUTPUT_FMT_TEXT) { + ds_put_cstr(&ds, json_string(reply)); + } else { + json_to_ds(reply, JSSF_SORT, &ds); + } + + if (ds_last(&ds) != EOF && ds_last(&ds) != '\n') { + ds_put_char(&ds, '\n'); + } + + return ds_steal_cstr(&ds); +}