MXS-1220: Add output for module commands

The module commands can now produce JSON formatted output which is passed
to the caller. The output should conform to the JSON API as closely as
possible.

Currently, the REST API wraps all JSON produced by module commands inside
a meta-object of the following type:

  {
    "meta": <output of module command>
  }

This allows the output to be JSON API conformant without modifying the
modules and allows incremental updates to code.
This commit is contained in:
Markus Mäkelä
2017-06-09 10:13:41 +03:00
parent 66a45fc37e
commit b3c1e15f22
14 changed files with 119 additions and 40 deletions

View File

@ -96,7 +96,7 @@ static modulecmd_arg_type_t custom_cmd_args[] =
{(MODULECMD_ARG_BOOLEAN | MODULECMD_ARG_OPTIONAL), "This is an optional bool parameter"}
};
bool custom_cmd_example(const MODULECMD_ARG *argv);
bool custom_cmd_example(const MODULECMD_ARG *argv, json_t** output);
using std::string;
using std::cout;
@ -890,7 +890,7 @@ static void process_finish()
* A function executed as a custom module command through MaxAdmin
* @param argv The arguments
*/
bool custom_cmd_example(const MODULECMD_ARG *argv)
bool custom_cmd_example(const MODULECMD_ARG *argv, json_t** output)
{
cout << MXS_MODULE_NAME << " wishes the Admin a good day.\n";
int n_args = argv->argc;

View File

@ -109,6 +109,7 @@ extern const char CN_LOG_THROTTLING[];
extern const char CN_MAXSCALE[];
extern const char CN_MAX_CONNECTIONS[];
extern const char CN_MAX_RETRY_INTERVAL[];
extern const char CN_META[];
extern const char CN_METHOD[];
extern const char CN_MODULE[];
extern const char CN_MODULES[];

View File

@ -14,6 +14,8 @@
/**
* @file Helper functions for creating JSON API conforming objects
*
* @see http://jsonapi.org/format/
*/
#include <maxscale/cppdefs.hh>

View File

@ -29,6 +29,7 @@
#include <maxscale/server.h>
#include <maxscale/service.h>
#include <maxscale/session.h>
#include <maxscale/jansson.h>
MXS_BEGIN_DECLS
@ -118,10 +119,20 @@ typedef struct
* the argument was not provided, the type of the argument will be
* MODULECMD_ARG_NONE.
*
* @param argv Argument list
* If the module command produces output, it should be stored in the @c output
* parameter as a json_t pointer. The output should conform as closely as possible
* to the JSON API specification. The minimal requirement for a JSON API conforming
* object is that it has a `meta` field. Ideally, the `meta` field should not
* be used as it offloads the work to the client.
*
* @see http://jsonapi.org/format/
*
* @param argv Argument list
* @param output JSON formatted output from the command
*
* @return True on success, false on error
*/
typedef bool (*MODULECMDFN)(const MODULECMD_ARG *argv);
typedef bool (*MODULECMDFN)(const MODULECMD_ARG *argv, json_t** output);
/**
* A registered command
@ -224,11 +235,13 @@ bool modulecmd_requires_output_dcb(const MODULECMD* cmd);
* on the length of the call or whether it will block. All of this depends on the
* module and what the command does.
*
* @param cmd Command to call
* @param args Parsed command arguments
* @param cmd Command to call
* @param args Parsed command arguments, pass NULL for no arguments
* @param output JSON output of the called command, pass NULL to ignore output
*
* @return True on success, false on error
*/
bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args);
bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args, json_t** output);
/**
* @brief Set the current error message

View File

@ -92,6 +92,7 @@ const char CN_LOG_THROTTLING[] = "log_throttling";
const char CN_MAXSCALE[] = "maxscale";
const char CN_MAX_CONNECTIONS[] = "max_connections";
const char CN_MAX_RETRY_INTERVAL[] = "max_retry_interval";
const char CN_META[] = "meta";
const char CN_METHOD[] = "method";
const char CN_MODULE[] = "module";
const char CN_MODULES[] = "modules";

View File

@ -534,7 +534,7 @@ void modulecmd_arg_free(MODULECMD_ARG* arg)
}
}
bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args)
bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args, json_t** output)
{
bool rval = false;
reset_error();
@ -550,7 +550,9 @@ bool modulecmd_call_command(const MODULECMD *cmd, const MODULECMD_ARG *args)
args = &MODULECMD_NO_ARGUMENTS;
}
rval = cmd->func(args);
json_t* discard = NULL;
rval = cmd->func(args, output ? output : &discard);
json_decref(discard);
}
return rval;

View File

@ -581,10 +581,11 @@ HttpResponse cb_modulecmd(const HttpRequest& request)
MODULECMD_ARG* args = modulecmd_arg_parse(cmd, n_opts, (const void**)opts);
bool rval = false;
json_t* output = NULL;
if (args)
{
rval = modulecmd_call_command(cmd, args);
rval = modulecmd_call_command(cmd, args, &output);
}
for (int i = 0; i < n_opts; i++)
@ -592,7 +593,28 @@ HttpResponse cb_modulecmd(const HttpRequest& request)
MXS_FREE(opts[i]);
}
return HttpResponse(rval ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR);
int rc = MHD_HTTP_INTERNAL_SERVER_ERROR;
if (rval)
{
if (output)
{
rc = MHD_HTTP_OK;
/** Store the command output in the meta field. This allows
* all the commands to conform to the JSON API even though
* the content of the field can vary from command to command. */
json_t* obj = json_object();
json_object_set_new(obj, CN_META, output);
output = obj;
}
else
{
rc = MHD_HTTP_NO_CONTENT;
}
}
return HttpResponse(rc, output);
}
}

View File

@ -20,6 +20,7 @@
#include <maxscale/paths.h>
#include <maxscale/modulecmd.h>
#include <maxscale/session.h>
#include <maxscale/json_api.h>
#include "../maxscale/monitor.h"
@ -27,7 +28,7 @@
static bool ok = false;
bool test_fn(const MODULECMD_ARG *arg)
bool test_fn(const MODULECMD_ARG *arg, json_t** output)
{
ok = (arg->argc == 2 && strcmp(arg->argv[0].value.string, "Hello") == 0 &&
@ -104,12 +105,12 @@ int test_arguments()
TEST(alist, "Arguments should be parsed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, alist), "Module call should be successful");
TEST(modulecmd_call_command(cmd, alist, NULL), "Module call should be successful");
TEST(ok, "Function should receive right parameters");
ok = false;
TEST(modulecmd_call_command(cmd, alist), "Second Module call should be successful");
TEST(modulecmd_call_command(cmd, alist, NULL), "Second Module call should be successful");
TEST(ok, "Function should receive right parameters");
@ -118,7 +119,7 @@ int test_arguments()
TEST((alist = modulecmd_arg_parse(cmd, 2, params2)), "Arguments should be parsed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, alist), "Module call should be successful");
TEST(modulecmd_call_command(cmd, alist, NULL), "Module call should be successful");
TEST(ok, "Function should receive right parameters");
modulecmd_arg_free(alist);
@ -128,21 +129,21 @@ int test_arguments()
*/
TEST((alist = modulecmd_arg_parse(cmd, 2, wrong_params1)), "Arguments should be parsed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, alist), "Module call should be successful");
TEST(modulecmd_call_command(cmd, alist, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(!ok, "Function should receive wrong parameters");
modulecmd_arg_free(alist);
TEST((alist = modulecmd_arg_parse(cmd, 2, wrong_params2)), "Arguments should be parsed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, alist), "Module call should be successful");
TEST(modulecmd_call_command(cmd, alist, NULL), "Module call should be successful");
TEST(!ok, "Function should receive wrong parameters");
modulecmd_arg_free(alist);
return 0;
}
bool test_fn2(const MODULECMD_ARG *arg)
bool test_fn2(const MODULECMD_ARG *arg, json_t** output)
{
return true;
}
@ -171,59 +172,59 @@ int test_optional_arguments()
MODULECMD_ARG *arg = modulecmd_arg_parse(cmd, 2, params1);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 2, params2);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 2, params3);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 2, params4);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 1, params1);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 1, params2);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
arg = modulecmd_arg_parse(cmd, 0, params1);
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
TEST(modulecmd_call_command(cmd, NULL), "Module call should be successful");
TEST(modulecmd_call_command(cmd, NULL, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
return 0;
}
bool test_fn3(const MODULECMD_ARG *arg)
bool test_fn3(const MODULECMD_ARG *arg, json_t** output)
{
modulecmd_set_error("Something went wrong!");
return false;
@ -240,13 +241,13 @@ int test_module_errors()
const MODULECMD *cmd = modulecmd_find_command(ns, id);
TEST(cmd, "The registered command should be found");
TEST(!modulecmd_call_command(cmd, NULL), "Module call should fail");
TEST(!modulecmd_call_command(cmd, NULL, NULL), "Module call should fail");
TEST(strlen(modulecmd_get_error()), "Error message should not be empty");
return 0;
}
bool test_fn_map(const MODULECMD_ARG *args)
bool test_fn_map(const MODULECMD_ARG *arg, json_t** output)
{
return true;
}
@ -310,7 +311,7 @@ int test_map()
static DCB my_dcb;
bool ptrfn(const MODULECMD_ARG *argv)
bool ptrfn(const MODULECMD_ARG *argv, json_t** output)
{
bool rval = false;
@ -345,14 +346,14 @@ int test_pointers()
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
return 0;
}
bool monfn(const MODULECMD_ARG *argv)
bool monfn(const MODULECMD_ARG *arg, json_t** output)
{
return true;
}
@ -385,13 +386,48 @@ int test_domain_matching()
TEST(arg, "Parsing arguments should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(modulecmd_call_command(cmd, arg), "Module call should be successful");
TEST(modulecmd_call_command(cmd, arg, NULL), "Module call should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
modulecmd_arg_free(arg);
return 0;
}
bool outputfn(const MODULECMD_ARG *arg, json_t** output)
{
json_t* obj = json_object();
json_object_set_new(obj, "hello", json_string("world"));
*output = obj;
return output != NULL;
}
int test_output()
{
const char *ns = "test_output";
const char *id = "test_output";
TEST(modulecmd_register_command(ns, id, MODULECMD_TYPE_ACTIVE, outputfn, 0, NULL),
"Registering a command should succeed");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
const MODULECMD *cmd = modulecmd_find_command(ns, id);
TEST(cmd, "The registered command should be found");
json_t* output = NULL;
TEST(modulecmd_call_command(cmd, NULL, &output), "Module call should be successful");
TEST(output, "Output should be non-NULL");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
TEST(json_is_string(mxs_json_pointer(output, "/hello")), "Value should be correct");
json_decref(output);
TEST(modulecmd_call_command(cmd, NULL, NULL), "Module call with NULL output should be successful");
TEST(strlen(modulecmd_get_error()) == 0, "Error message should be empty");
return 0;
}
int main(int argc, char **argv)
{
int rc = 0;
@ -402,6 +438,7 @@ int main(int argc, char **argv)
rc += test_map();
rc += test_pointers();
rc += test_domain_matching();
rc += test_output();
return rc;
}

View File

@ -78,7 +78,7 @@ static int cdc_auth_set_client_data(
* @param args Arguments for this command
* @return True if user was successfully added
*/
static bool cdc_add_new_user(const MODULECMD_ARG *args)
static bool cdc_add_new_user(const MODULECMD_ARG *args, json_t** output)
{
const char *user = args->argv[1].value.string;
size_t userlen = strlen(user);

View File

@ -85,7 +85,7 @@ void cache_config_reset(CACHE_CONFIG& config)
*
* @return True, if the command was handled.
*/
bool cache_command_show(const MODULECMD_ARG* pArgs)
bool cache_command_show(const MODULECMD_ARG* pArgs, json_t** output)
{
ss_dassert(pArgs->argc == 2);
ss_dassert(MODULECMD_GET_TYPE(&pArgs->argv[0].type) == MODULECMD_ARG_OUTPUT);

View File

@ -712,7 +712,7 @@ TIMERANGE* split_reverse_time(TIMERANGE* tr)
return tmp;
}
bool dbfw_reload_rules(const MODULECMD_ARG *argv)
bool dbfw_reload_rules(const MODULECMD_ARG *argv, json_t** output)
{
bool rval = true;
MXS_FILTER_DEF *filter = argv->argv[0].value.filter;
@ -776,7 +776,7 @@ bool dbfw_reload_rules(const MODULECMD_ARG *argv)
return rval;
}
bool dbfw_show_rules(const MODULECMD_ARG *argv)
bool dbfw_show_rules(const MODULECMD_ARG *argv, json_t** output)
{
DCB *dcb = argv->argv[0].value.dcb;
MXS_FILTER_DEF *filter = argv->argv[1].value.filter;

View File

@ -32,7 +32,7 @@ char VERSION_STRING[] = "V1.0.0";
*
* @return True, if the command was handled.
*/
bool masking_command_reload(const MODULECMD_ARG* pArgs)
bool masking_command_reload(const MODULECMD_ARG* pArgs, json_t** output)
{
ss_dassert(pArgs->argc == 2);
ss_dassert(MODULECMD_GET_TYPE(&pArgs->argv[0].type) == MODULECMD_ARG_OUTPUT);

View File

@ -101,7 +101,7 @@ static bool conversion_task_ctl(AVRO_INSTANCE *inst, bool start);
static SPINLOCK instlock;
static AVRO_INSTANCE *instances;
bool avro_handle_convert(const MODULECMD_ARG *args)
bool avro_handle_convert(const MODULECMD_ARG *args, json_t** output)
{
bool rval = false;

View File

@ -1717,7 +1717,8 @@ static void callModuleCommand(DCB *dcb, char *domain, char *id, char *v3,
if (arg)
{
if (!modulecmd_call_command(cmd, arg))
// TODO: Print output instead of passing a writable DCB to the command
if (!modulecmd_call_command(cmd, arg, NULL))
{
dcb_printf(dcb, "Error: %s\n", modulecmd_get_error());
}