From 3d6ca1f056ad2b38b2b514975f87206cbceedcad Mon Sep 17 00:00:00 2001 From: Luca Ferrari Date: Wed, 6 Dec 2023 08:14:47 -0500 Subject: [PATCH] [#385][#390] JSON command output implementation for `pgagroal-cli` This commit introduces the capaiblity for `pgagroal-cli` to print out the command results in a JSON format. A new dependency on the library 'cJSON' has been introduced; see . A new set of functions, named 'json_xxx' have been added in a separated file json.c (include file json.h) to handle JSON structures in a more consistent way. Each function that manipulates a JSON object has been named with the 'json_' prefix. Each management function that reads data out of the protocol and creates or handles json data has been named with the prefix 'pgagroal_management_json_'. A few functions with a prefix name 'pgagroal_management_json_print' are used to print out a JSON object as normal text. Every command output, in the JSON format, is structured with a 'command' is the main object contained in the output, that in turns has different attributes: - 'output' is an object that contains the command specific information (e.g., list of databases, messages, and so on); - `error` a boolean-like value that indicates if the command was in error or not (0 means success, 1 means error); - `status` a string that contains either 'OK' or an error message in the case the `error` flag is set; - `exit-status` an integer value with the exit status of the command, 0 in case of success, a different value in case of failure. The JSON object for a result includes also another object, named 'application', that can be used for introspection: such object describes which executable has launched the command (so far, only `pgagroal-cli`) and at which version. In the 'output' object, every thing that has a list (e.g., connections, limits, databases, servers) will be wrapped into an object with the attributes 'list' and 'count': the former contains the details of every "thing", while the latter contains the count (i.e., the size of the array 'list' ). Whenever possible, a 'state' string is placed to indicate the state of the single entry. The command `status` and `status details` have been unified on the management side. There is a single function that handles both the cases of reading the answer for the `status` or the `status details` commands. This has been done because `status details` includes the output of `status`. The function `pgagroal_management_json_read_status_details` is in charge of getting the answer of the management protocol (i.e., read the socket) and invoke the printing function in the case the output is of type text. The above `pgagroal_management_json_read_status_details` returns always a JSON object, that can be converted back to the text output via `pgagroal_management_json_print_status_details`. In this way, the output between the JSON and the text formats are always coherent for these commands. The `ping` (i.e., `is-alive`) command does not print nothing by default. In the JSON version it provides the numerical status of the server and a string that represents a human-readable status. The `conf get` command has been refactored to be symmetric with other commands: the logic to print out the result is now within the management function (pgagroal_management_read_config_get) as per other commands. The JSON provides the `config-key` and the `config-value` as strings. See #390 The `conf set` command has been refactored similarly to `conf get` in order to have all the logic to print out the information into the management read method (see #390). The exit status provided by the command is now the result of the matching within the JSON object of the expected configuration value and the requested configuration value. The `conf ls` command has been refactored to produce a JSON object when reading data out of the management socket. Such JSON object is then printed as normal text if required. A new utility function, named 'pgagroal_server_status_as_string' has been added to the utils.c stuff. The idea is to have a consistent way to translate the numerical status representation into an human readable string. The text output format of commands has slightly changed due to the refactoring of some internal methods. Documentation updated. CI workflow updated. Close #385 Close #390 --- .github/workflows/ci.yml | 4 + CMakeLists.txt | 10 + doc/CLI.md | 206 +++++++++ src/CMakeLists.txt | 3 + src/cli.c | 146 ++---- src/include/json.h | 162 +++++++ src/include/management.h | 29 +- src/include/utils.h | 14 + src/libpgagroal/json.c | 268 +++++++++++ src/libpgagroal/management.c | 873 +++++++++++++++++++++++++++-------- src/libpgagroal/utils.c | 31 ++ 11 files changed, 1449 insertions(+), 297 deletions(-) create mode 100644 src/include/json.h create mode 100644 src/libpgagroal/json.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb1429ec..d16daa19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: run: sudo apt update -y - name: Install libev run: sudo apt install -y libev4 libev-dev + - name: Install cJSON + run: sudo apt install -y libcjson1 libcjson-dev - name: Install systemd run: sudo apt install -y libsystemd-dev - name: Install rst2man @@ -65,6 +67,8 @@ jobs: run: brew install libev - name: Install rst2man run: brew install docutils + - name: Install cJSON + run: brew install cjson - name: Install clang run: brew install llvm - name: GCC/mkdir diff --git a/CMakeLists.txt b/CMakeLists.txt index 94c34ae8..cb4a4e90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,16 @@ else () message(FATAL_ERROR "rst2man needed") endif() +# search for cJSON library +# +find_package(cJSON) +if (cJSON_FOUND) + message(STATUS "cJSON found") +else () + message(FATAL_ERROR "cJSON needed") +endif() + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") find_package(Libatomic) if (LIBATOMIC_FOUND) diff --git a/doc/CLI.md b/doc/CLI.md index 3a3d5a6a..5bf6056f 100644 --- a/doc/CLI.md +++ b/doc/CLI.md @@ -22,6 +22,7 @@ Available options are the following ones: -U, --user USERNAME Set the user name -P, --password PASSWORD Set the password -L, --logfile FILE Set the log file +-F, --format text|json Set the output format -v, --verbose Output text string of result -V, --version Display version information -?, --help Display help @@ -30,6 +31,11 @@ Available options are the following ones: Options can be specified either in short or long form, in any position of the command line. +By default the command output, if any, is reported as text. It is possible to specify JSON as the output format, +and this is the suggested format if there is the need to automtically parse the command output, since the text format +could be subject to changes in future releases. For more information about the JSON output format, +please see the [JSON Output Format](#json-output-format) section. + ## Commands ### flush @@ -380,3 +386,203 @@ pgagroal-cli reset-server 2>/dev/null There is a minimal shell completion support for `pgagroal-cli`. Please refer to the [Install pgagroal](https://github.com/agroal/pgagroal/blob/master/doc/tutorial/01_install.md) tutorial for detailed information about how to enable and use shell completions. + + +## JSON Output Format + +It is possible to obtain the output of a command in a JSON format by specyfing the `-F` (`--format`) option on the command line. +Supported output formats are: +- `text` (the default) +- `json` + +As an example, the following are invocations of commands with different output formats: + +``` +pgagroal-cli status # defaults to text output format + +pgagroal-cli status --format text # same as above +pgagroal-cli status -F text # same as above + +pgagroal-cli status --format json # outputs as JSON text +pgagroal-cli status -F json # same as above +``` + +Whenever a command produces output, the latter can be obtained in a JSON format. +Every command output consists of an object that contains two other objects: +- a `command` object, with all the details about the command and its output; +- an `application` object, with all the details about the executable that launched the command (e.g., `pgagroal-cli`). + +In the following, details about every object are provided: + +### The `application` object + +The `application` object is made by the following attributes: +- `name` a string representing the name of the executable that launched the command; +- `version` a string representing the version of the executable; +- `major`, `minor`, `patch` are integers representing every single part of the version of the application. + +As an example, when `pgagroal-cli` launches a command, the output includes an `application` object like the following: + +``` + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +``` + + +### The `command` object + +The `command` object represents the launched command and contains also the answer from the `pgagroal`. +The object is made by the following attributes: +- `name` a string representing the command launched (e.g., `status`); +- `status` a string that contains either "OK" or an error string if the command failed; +- `error` an interger value used as a flag to indicate if the command was in error or not, where `0` means success and `1` means error; +- `exit-status` an integer that contains zero if the command run succesfully, another value depending on the specific command in case of failure; +- `output` an object that contains the details of the executed command. + +The `output` object is *the variable part* in the JSON command output, that means its effective content depends on the launched command. + +Whenever the command output includes an array of stuff, for example a connection list, such array is wrapped into a `list` JSON array with a sibling named `count` that contains the integer size of the array (number of elements). + + +The following are a few examples of commands that provide output in JSON: + + +``` +pgagroal-cli ping --format json +{ + "command": { + "name": "ping", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": 1, + "message": "running" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} + + + +pgagroal-cli status --format json +{ + "command": { + "name": "status", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "status": { + "message": "Running", + "status": 1 + }, + "connections": { + "active": 0, + "total": 2, + "max": 15 + }, + "databases": { + "disabled": { + "count": 0, + "state": "disabled", + "list": [] + } + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` + +As an example, the following is the output of a faulty `conf set` command (note the `status`, `error` and `exist-status` values): + +``` +pgagroal-cli conf set max_connections 1000 --format json +{ + "command": { + "name": "conf set", + "status": "Current and expected values are different", + "error": true, + "exit-status": 2, + "output": { + "key": "max_connections", + "value": "15", + "expected": "1000" + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` + + +The `conf ls` command returns an array named `files` where each entry is made by a couple `description` and `path`, where the former +is the mnemonic name of the configuration file, and the latter is the value of the configuration file used: + +``` +$ pgagroal-cli conf ls --format json +{ + "command": { + "name": "conf ls", + "status": "OK", + "error": 0, + "exit-status": 0, + "output": { + "files": { + "list": [{ + "description": "Main Configuration file", + "path": "/etc/pgagroal/pgagroal.conf" + }, { + "description": "HBA File", + "path": "/etc/pgagroal/pgagroal_hba.conf" + }, { + "description": "Limit file", + "path": "/etc/pgagroal/pgagroal_databases.conf" + }, { + "description": "Frontend users file", + "path": "/etc/pgagroal/pgagroal_frontend_users.conf" + }, { + "description": "Admins file", + "path": "/etc/pgagroal/pgagroal_admins.conf" + }, { + "description": "Superuser file", + "path": "" + }, { + "description": "Users file", + "path": "/etc/pgagroal/pgagroal_users.conf" + }] + } + } + }, + "application": { + "name": "pgagroal-cli", + "major": 1, + "minor": 6, + "patch": 0, + "version": "1.6.0" + } +} +``` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 51ce9027..e15bedef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") ${LIBEV_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIR} ${SYSTEMD_INCLUDE_DIRS} + ${CJSON_INCLUDE_DIRS} ) # @@ -33,6 +34,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") ${OPENSSL_SSL_LIBRARY} ${SYSTEMD_LIBRARIES} ${LIBATOMIC_LIBRARY} + ${CJSON_LIBRARIES} ) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") @@ -314,3 +316,4 @@ endif() target_link_libraries(pgagroal-admin-bin pgagroal) install(TARGETS pgagroal-admin-bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + diff --git a/src/cli.c b/src/cli.c index 75c47ee5..adfe1756 100644 --- a/src/cli.c +++ b/src/cli.c @@ -74,16 +74,16 @@ static int disabledb(SSL* ssl, int socket, char* database); static int gracefully(SSL* ssl, int socket); static int stop(SSL* ssl, int socket); static int cancel_shutdown(SSL* ssl, int socket); -static int status(SSL* ssl, int socket); -static int details(SSL* ssl, int socket); -static int isalive(SSL* ssl, int socket); +static int status(SSL* ssl, int socket, char output_format); +static int details(SSL* ssl, int socket, char output_format); +static int isalive(SSL* ssl, int socket, char output_format); static int reset(SSL* ssl, int socket); static int reset_server(SSL* ssl, int socket, char* server); static int switch_to(SSL* ssl, int socket, char* server); static int reload(SSL* ssl, int socket); -static int config_get(SSL* ssl, int socket, char* config_key, bool verbose); -static int config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verbose); -static int config_ls(SSL* ssl, int socket); +static int config_ls(SSL* ssl, int socket, char output_format); +static int config_get(SSL* ssl, int socket, char* config_key, bool verbose, char output_format); +static int config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verbose, char output_format); static void version(void) @@ -110,6 +110,7 @@ usage(void) printf(" -U, --user USERNAME Set the user name\n"); printf(" -P, --password PASSWORD Set the password\n"); printf(" -L, --logfile FILE Set the log file\n"); + printf(" -F, --format text|json Set the output format\n"); printf(" -v, --verbose Output text string of result\n"); printf(" -V, --version Display version information\n"); printf(" -?, --help Display help\n"); @@ -176,6 +177,7 @@ main(int argc, char** argv) long l_port; char* config_key = NULL; /* key for a configuration setting */ char* config_value = NULL; /* value for a configuration setting */ + char output_format = COMMAND_OUTPUT_FORMAT_TEXT; while (1) { @@ -187,12 +189,13 @@ main(int argc, char** argv) {"user", required_argument, 0, 'U'}, {"password", required_argument, 0, 'P'}, {"logfile", required_argument, 0, 'L'}, + {"format", required_argument, 0, 'F' }, {"verbose", no_argument, 0, 'v'}, {"version", no_argument, 0, 'V'}, {"help", no_argument, 0, '?'} }; - c = getopt_long(argc, argv, "vV?c:h:p:U:P:L:", + c = getopt_long(argc, argv, "vV?c:h:p:U:P:L:F:", long_options, &option_index); if (c == -1) @@ -220,6 +223,16 @@ main(int argc, char** argv) case 'L': logfile = optarg; break; + case 'F': + if (!strncmp(optarg, "json", MISC_LENGTH)) + { + output_format = COMMAND_OUTPUT_FORMAT_JSON; + } + else + { + output_format = COMMAND_OUTPUT_FORMAT_TEXT; + } + break; case 'v': verbose = true; break; @@ -580,15 +593,15 @@ main(int argc, char** argv) } else if (action == ACTION_STATUS) { - exit_code = status(s_ssl, socket); + exit_code = status(s_ssl, socket, output_format); } else if (action == ACTION_STATUS_DETAILS) { - exit_code = details(s_ssl, socket); + exit_code = details(s_ssl, socket, output_format); } else if (action == ACTION_ISALIVE) { - exit_code = isalive(s_ssl, socket); + exit_code = isalive(s_ssl, socket, output_format); } else if (action == ACTION_RESET) { @@ -608,15 +621,15 @@ main(int argc, char** argv) } else if (action == ACTION_CONFIG_GET) { - exit_code = config_get(s_ssl, socket, config_key, verbose); + exit_code = config_get(s_ssl, socket, config_key, verbose, output_format); } else if (action == ACTION_CONFIG_SET) { - exit_code = config_set(s_ssl, socket, config_key, config_value, verbose); + exit_code = config_set(s_ssl, socket, config_key, config_value, verbose, output_format); } else if (action == ACTION_CONFIG_LS) { - exit_code = config_ls(s_ssl, socket); + exit_code = config_ls(s_ssl, socket, output_format); } done: @@ -743,11 +756,11 @@ cancel_shutdown(SSL* ssl, int socket) } static int -status(SSL* ssl, int socket) +status(SSL* ssl, int socket, char output_format) { if (pgagroal_management_status(ssl, socket) == 0) { - return pgagroal_management_read_status(ssl, socket); + return pgagroal_management_read_status(ssl, socket, output_format); } else { @@ -756,14 +769,12 @@ status(SSL* ssl, int socket) } static int -details(SSL* ssl, int socket) +details(SSL* ssl, int socket, char output_format) { if (pgagroal_management_details(ssl, socket) == 0) { - if (pgagroal_management_read_status(ssl, socket) == 0) - { - return pgagroal_management_read_details(ssl, socket); - } + return pgagroal_management_read_details(ssl, socket, output_format); + } // if here, an error occurred @@ -772,18 +783,18 @@ details(SSL* ssl, int socket) } static int -isalive(SSL* ssl, int socket) +isalive(SSL* ssl, int socket, char output_format) { int status = -1; if (pgagroal_management_isalive(ssl, socket) == 0) { - if (pgagroal_management_read_isalive(ssl, socket, &status)) + if (pgagroal_management_read_isalive(ssl, socket, &status, output_format)) { return EXIT_STATUS_CONNECTION_ERROR; } - if (status != 1 && status != 2) + if (status != PING_STATUS_RUNNING && status != PING_STATUS_SHUTDOWN_GRACEFULLY) { return EXIT_STATUS_CONNECTION_ERROR; } @@ -851,12 +862,12 @@ reload(SSL* ssl, int socket) * @param config_key the key of the configuration parameter, that is the name * of the configuration parameter to read. * @param verbose if true the function will print on STDOUT also the config key + * @param output_format the format for the output (e.g., json) * @returns 0 on success, 1 on network failure, 2 on data failure */ static int -config_get(SSL* ssl, int socket, char* config_key, bool verbose) +config_get(SSL* ssl, int socket, char* config_key, bool verbose, char output_format) { - char* buffer = NULL; if (!config_key || strlen(config_key) > MISC_LENGTH) { @@ -867,40 +878,10 @@ config_get(SSL* ssl, int socket, char* config_key, bool verbose) { goto error; } - else - { - buffer = calloc(1, MISC_LENGTH); - if (buffer == NULL) - { - goto error; - } - if (pgagroal_management_read_config_get(socket, &buffer)) - { - free(buffer); - goto error; - } - // an empty response means that the - // requested configuration parameter has not been - // found, so throw an error - if (buffer && strlen(buffer)) - { - if (verbose) - { - printf("%s = %s\n", config_key, buffer); - } - else - { - printf("%s\n", buffer); - } - } - else - { - free(buffer); - return EXIT_STATUS_DATA_ERROR; - } - - free(buffer); + if (pgagroal_management_read_config_get(socket, config_key, NULL, verbose, output_format)) + { + goto error; } return EXIT_STATUS_OK; @@ -923,10 +904,10 @@ config_get(SSL* ssl, int socket, char* config_key, bool verbose) * @return 0 on success */ static int -config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verbose) +config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verbose, char output_format) { - char* buffer = NULL; - int status = EXIT_STATUS_DATA_ERROR; + + int status = EXIT_STATUS_OK; if (!config_key || strlen(config_key) > MISC_LENGTH || !config_value || strlen(config_value) > MISC_LENGTH) @@ -938,45 +919,8 @@ config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verb { goto error; } - else - { - buffer = malloc(MISC_LENGTH); - memset(buffer, 0, MISC_LENGTH); - if (pgagroal_management_read_config_get(socket, &buffer)) - { - free(buffer); - goto error; - } - // if the setting we sent is different from the setting we get - // than the system has not applied, so it is an error - if (strncmp(config_value, buffer, MISC_LENGTH) == 0) - { - status = EXIT_STATUS_OK; - } - else - { - status = EXIT_STATUS_DATA_ERROR; - } - - // assume an empty response is ok, - // do not throw an error to indicate no configuration - // setting with such name as been found - if (buffer && strlen(buffer)) - { - if (verbose) - { - printf("%s = %s\n", config_key, buffer); - } - else - { - printf("%s\n", buffer); - } - } - - free(buffer); - return status; - } + status = pgagroal_management_read_config_get(socket, config_key, config_value, verbose, output_format); return status; error: @@ -989,7 +933,7 @@ config_set(SSL* ssl, int socket, char* config_key, char* config_value, bool verb * @returns 0 on success */ static int -config_ls(SSL* ssl, int socket) +config_ls(SSL* ssl, int socket, char output_format) { if (pgagroal_management_conf_ls(ssl, socket)) @@ -997,7 +941,7 @@ config_ls(SSL* ssl, int socket) goto error; } - if (pgagroal_management_read_conf_ls(ssl, socket)) + if (pgagroal_management_read_conf_ls(ssl, socket, output_format)) { goto error; } diff --git a/src/include/json.h b/src/include/json.h new file mode 100644 index 00000000..7087aab1 --- /dev/null +++ b/src/include/json.h @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2023 Red Hat + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include + +#include + +/** + * JSON related command tags, used to build and retrieve + * a JSON piece of information related to a single command + */ +#define JSON_TAG_COMMAND "command" +#define JSON_TAG_COMMAND_NAME "name" +#define JSON_TAG_COMMAND_STATUS "status" +#define JSON_TAG_COMMAND_ERROR "error" +#define JSON_TAG_COMMAND_OUTPUT "output" +#define JSON_TAG_COMMAND_EXIT_STATUS "exit-status" + +#define JSON_TAG_APPLICATION_NAME "name" +#define JSON_TAG_APPLICATION_VERSION_MAJOR "major" +#define JSON_TAG_APPLICATION_VERSION_MINOR "minor" +#define JSON_TAG_APPLICATION_VERSION_PATCH "patch" +#define JSON_TAG_APPLICATION_VERSION "version" + +#define JSON_TAG_ARRAY_NAME "list" + +/** + * JSON pre-defined values + */ +#define JSON_STRING_SUCCESS "OK" +#define JSON_STRING_ERROR "KO" +#define JSON_BOOL_SUCCESS 0 +#define JSON_BOOL_ERROR 1 + +/** + * Utility method to create a new JSON object that wraps a + * single command. This method should be called to initialize the + * object and then the other specific methods that read the + * answer from pgagroal should populate the object accordingly. + * + * Moreover, an 'application' object is placed to indicate from + * where the command has been launched (i.e., which executable) + * and at which version. + * + * @param command_name the name of the command this object wraps + * an answer for + * @param success true if the command is supposed to be succesfull + * @returns the new JSON object to use and populate + * @param executable_name the name of the executable that is creating this + * response object + */ +cJSON* json_create_new_command_object(char* command_name, bool success, char* executable_name); + +/** + * Utility method to "jump" to the output JSON object wrapped into + * a command object. + * + * The "output" object is the one that every single method that reads + * back an answer from pgagroal has to populate in a specific + * way according to the data received from pgagroal. + * + * @param json the command object that wraps the command + * @returns the pointer to the output object of NULL in case of an error + */ +cJSON* json_extract_command_output_object(cJSON* json); + +/** + * Utility function to set a command JSON object as faulty, that + * means setting the 'error' and 'status' message accordingly. + * + * @param json the whole json object that must include the 'command' + * tag + * @param message the message to use to set the faulty diagnostic + * indication + * + * @param exit status + * + * @returns 0 on success + * + * Example: + * json_set_command_object_faulty( json, strerror( errno ) ); + */ +int json_set_command_object_faulty(cJSON* json, char* message, int exit_status); + +/** + * Utility method to inspect if a JSON object that wraps a command + * is faulty, that means if it has the error flag set to true. + * + * @param json the json object to analyzer + * @returns the value of the error flag in the object, or false if + * the object is not valid + */ +bool json_is_command_object_faulty(cJSON* json); + +/** + * Utility method to extract the message related to the status + * of the command wrapped in the JSON object. + * + * @param json the JSON object to analyze + * #returns the status message or NULL in case the JSON object is not valid + */ +const char* json_get_command_object_status(cJSON* json); + +/** + * Utility method to check if a JSON object wraps a specific command name. + * + * @param json the JSON object to analyze + * @param command_name the name to search for + * @returns true if the command name matches, false otherwise and in case + * the JSON object is not valid or the command name is not valid + */ +bool json_is_command_name_equals_to(cJSON* json, char* command_name); + +/** + * Utility method to print out the JSON object + * on standard output. + * + * After the object has been printed, it is destroyed, so + * calling this method will make the pointer invalid + * and the jeon object cannot be used anymore. + * + * This should be the last method to be called + * when there is the need to print out the information + * contained in a json object. + * + * @param json the json object to print + */ +void json_print_and_free_json_object(cJSON* json); + +/** + * Utility function to get the exit status of a given command wrapped in a JSON object. + * + * @param json the json object + * @returns the exit status of the command + */ +int json_command_object_exit_status(cJSON* json); diff --git a/src/include/management.h b/src/include/management.h index 0f8ad2ea..8b0f11fb 100644 --- a/src/include/management.h +++ b/src/include/management.h @@ -63,6 +63,18 @@ extern "C" { #define MANAGEMENT_CONFIG_SET 21 #define MANAGEMENT_CONFIG_LS 22 +/** + * Status for the 'ping' (i.e., is-alive) command + */ +#define PING_STATUS_RUNNING 1 +#define PING_STATUS_SHUTDOWN_GRACEFULLY 2 + +/** + * Available command output formats + */ +#define COMMAND_OUTPUT_FORMAT_TEXT 'T' +#define COMMAND_OUTPUT_FORMAT_JSON 'J' + /** * Read the management header * @param socket The socket descriptor @@ -179,10 +191,11 @@ pgagroal_management_status(SSL* ssl, int socket); /** * Management: Read status * @param socket The socket + * @param output_format a char describing the type of output (text or json) * @return 0 upon success, otherwise 1 */ int -pgagroal_management_read_status(SSL* ssl, int socket); +pgagroal_management_read_status(SSL* ssl, int socket, char output_format); /** * Management: Write status @@ -205,10 +218,11 @@ pgagroal_management_details(SSL* ssl, int socket); /** * Management: Read details * @param socket The socket + * @param output_format the output format for this command (text, json) * @return 0 upon success, otherwise 1 */ int -pgagroal_management_read_details(SSL* ssl, int socket); +pgagroal_management_read_details(SSL* ssl, int socket, char output_format); /** * Management: Write details @@ -233,7 +247,7 @@ pgagroal_management_isalive(SSL* ssl, int socket); * @return 0 upon success, otherwise 1 */ int -pgagroal_management_read_isalive(SSL* ssl, int socket, int* status); +pgagroal_management_read_isalive(SSL* ssl, int socket, int* status, char output_format); /** * Management: Write isalive @@ -332,10 +346,14 @@ pgagroal_management_config_get(SSL* ssl, int socket, char* config_key); * @see pgagroal_management_read_payload * * @param ssl the socket file descriptor + * @param config_key the key to read (is used only to print in the output) + * @param verbose verbosity flag + * @param output_format the output format + * @param expected_value if set, a value that the configuration should match * @return 0 on success */ int -pgagroal_management_read_config_get(int socket, char** data); +pgagroal_management_read_config_get(int socket, char* config_key, char* expected_value, bool verbose, char output_format); /** * Management operation: write the result of a config_get action on the socket. @@ -414,10 +432,11 @@ pgagroal_management_conf_ls(SSL* ssl, int fd); * * @param socket the file descriptor of the open socket * @param ssl the SSL handler + * @param output_format the format to output the command result * @returns 0 on success */ int -pgagroal_management_read_conf_ls(SSL* ssl, int socket); +pgagroal_management_read_conf_ls(SSL* ssl, int socket, char output_format); /** * The management function responsible for sending diff --git a/src/include/utils.h b/src/include/utils.h index 4cb000ed..f9dd06a3 100644 --- a/src/include/utils.h +++ b/src/include/utils.h @@ -494,6 +494,20 @@ parse_deprecated_command(int argc, char* deprecated_by, unsigned int deprecated_since_major, unsigned int deprecated_since_minor); + +/** + * Given a server state, it returns a string that + * described the state in a human-readable form. + * + * If the state cannot be determined, the numeric + * form of the state is returned as a string. + * + * @param state the value of the sate for the server + * @returns the string representing the state + */ +char* +pgagroal_server_state_as_string(signed char state); + #ifdef __cplusplus } #endif diff --git a/src/libpgagroal/json.c b/src/libpgagroal/json.c new file mode 100644 index 00000000..53d97399 --- /dev/null +++ b/src/libpgagroal/json.c @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2023 Red Hat + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may + * be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT + * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* pgagroal */ +#include +#include + +cJSON* +json_create_new_command_object(char* command_name, bool success, char* executable_name) +{ + // root of the JSON structure + cJSON* json = cJSON_CreateObject(); + + if (!json) + { + goto error; + } + + // the command structure + cJSON* command = cJSON_CreateObject(); + if (!command) + { + goto error; + } + + // insert meta-data about the command + cJSON_AddStringToObject(command, JSON_TAG_COMMAND_NAME, command_name); + cJSON_AddStringToObject(command, JSON_TAG_COMMAND_STATUS, success ? JSON_STRING_SUCCESS : JSON_STRING_ERROR); + cJSON_AddNumberToObject(command, JSON_TAG_COMMAND_ERROR, success ? JSON_BOOL_SUCCESS : JSON_BOOL_ERROR); + cJSON_AddNumberToObject(command, JSON_TAG_COMMAND_EXIT_STATUS, success ? 0 : EXIT_STATUS_DATA_ERROR); + + // the output of the command, this has to be filled by the caller + cJSON* output = cJSON_CreateObject(); + if (!output) + { + goto error; + } + + cJSON_AddItemToObject(command, JSON_TAG_COMMAND_OUTPUT, output); + + // who has launched the command ? + cJSON* application = cJSON_CreateObject(); + if (!application) + { + goto error; + } + + cJSON_AddStringToObject(application, JSON_TAG_APPLICATION_NAME, executable_name); + cJSON_AddNumberToObject(application, JSON_TAG_APPLICATION_VERSION_MAJOR, PGAGROAL_MAJOR_VERSION); + cJSON_AddNumberToObject(application, JSON_TAG_APPLICATION_VERSION_MINOR, PGAGROAL_MINOR_VERSION); + cJSON_AddNumberToObject(application, JSON_TAG_APPLICATION_VERSION_PATCH, PGAGROAL_PATCH_VERSION); + cJSON_AddStringToObject(application, JSON_TAG_APPLICATION_VERSION, PGAGROAL_VERSION); + + // add objects to the whole json thing + cJSON_AddItemToObject(json, "command", command); + cJSON_AddItemToObject(json, "application", application); + + return json; + +error: + if (json) + { + cJSON_Delete(json); + } + + return NULL; + +} + +cJSON* +json_extract_command_output_object(cJSON* json) +{ + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + return cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_OUTPUT); + +error: + return NULL; + +} + +bool +json_is_command_name_equals_to(cJSON* json, char* command_name) +{ + if (!json || !command_name || strlen(command_name) <= 0) + { + goto error; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + cJSON* cName = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_NAME); + if (!cName || !cJSON_IsString(cName) || !cName->valuestring) + { + goto error; + } + + return !strncmp(command_name, + cName->valuestring, + MISC_LENGTH); + +error: + return false; +} + +int +json_set_command_object_faulty(cJSON* json, char* message, int exit_status) +{ + if (!json) + { + goto error; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + cJSON* current = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_STATUS); + if (!current) + { + goto error; + } + + cJSON_SetValuestring(current, message); + + current = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_ERROR); + if (!current) + { + goto error; + } + + cJSON_SetIntValue(current, JSON_BOOL_ERROR); // cannot use cJSON_SetBoolValue unless cJSON >= 1.7.16 + + current = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_EXIT_STATUS); + if (!current) + { + goto error; + } + + cJSON_SetIntValue(current, exit_status); + + return 0; + +error: + return 1; + +} + +bool +json_is_command_object_faulty(cJSON* json) +{ + if (!json) + { + goto error; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + cJSON* status = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_ERROR); + if (!status || !cJSON_IsNumber(status)) + { + goto error; + } + + return status->valueint == JSON_BOOL_SUCCESS ? false : true; + +error: + return false; + +} + +int +json_command_object_exit_status(cJSON* json) +{ + if (!json) + { + goto error; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + cJSON* status = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_EXIT_STATUS); + if (!status || !cJSON_IsNumber(status)) + { + goto error; + } + + return status->valueint; + +error: + return EXIT_STATUS_DATA_ERROR; +} + +const char* +json_get_command_object_status(cJSON* json) +{ + if (!json) + { + goto error; + } + + cJSON* command = cJSON_GetObjectItemCaseSensitive(json, JSON_TAG_COMMAND); + if (!command) + { + goto error; + } + + cJSON* status = cJSON_GetObjectItemCaseSensitive(command, JSON_TAG_COMMAND_STATUS); + if (!cJSON_IsString(status) || (status->valuestring == NULL)) + { + goto error; + } + + return status->valuestring; +error: + return NULL; + +} + +void +json_print_and_free_json_object(cJSON* json) +{ + printf("%s\n", cJSON_Print(json)); + cJSON_Delete(json); +} diff --git a/src/libpgagroal/management.c b/src/libpgagroal/management.c index 908b6766..b1ccc525 100644 --- a/src/libpgagroal/management.c +++ b/src/libpgagroal/management.c @@ -35,6 +35,7 @@ #include #include #include +#include /* system */ #include @@ -62,6 +63,14 @@ static int write_header(SSL* ssl, int fd, signed char type, int slot); static int pgagroal_management_write_conf_ls_detail(int socket, char* what); static int pgagroal_management_read_conf_ls_detail(SSL* ssl, int socket, char* buffer); +static int pgagroal_management_json_print_status_details(cJSON* json); + +static cJSON* pgagroal_management_json_read_status_details(SSL* ssl, int socket, bool include_details); +static cJSON* pgagroal_managment_json_read_config_get(int socket, char* config_key, char* expected_value); + +static cJSON* pgagroal_management_json_read_conf_ls(SSL* ssl, int socket); +static int pgagroal_management_json_print_conf_ls(cJSON* json); + int pgagroal_management_read_header(int socket, signed char* id, int32_t* slot) { @@ -560,7 +569,50 @@ pgagroal_management_status(SSL* ssl, int fd) } int -pgagroal_management_read_status(SSL* ssl, int socket) +pgagroal_management_read_status(SSL* ssl, int socket, char output_format) +{ + cJSON* json = pgagroal_management_json_read_status_details(ssl, socket, false); + + // check we have an answer and it is not an error + if (!json || json_is_command_object_faulty(json)) + { + goto error; + } + + // print out the command answer + if (output_format == COMMAND_OUTPUT_FORMAT_JSON) + { + json_print_and_free_json_object(json); + } + else + { + pgagroal_management_json_print_status_details(json); + } + + return 0; + +error: + pgagroal_log_warn("pgagroal_management_read_status: command error [%s]", + (json == NULL ? "" : json_get_command_object_status(json))); + return 1; +} + +/** + * Utility method that reads the answer from pgagroal about + * either the 'status' or the 'status details' command. + * The answer is then wrapped into a JSON object + * that contains all the information needed to be printed out in either + * JSON format or text format. + * + * @param ssl the SSL file descriptor for the socket + * @param socket the socket file descriptor + * @param include_details true if the method has to handle the 'status details' command + * or false if the answer is related only to the 'status' command + * + * @returns the json object, faulty if something goes wrong + */ +static cJSON* +pgagroal_management_json_read_status_details(SSL* ssl, int socket, bool include_details) { char buf[16]; char disabled[NUMBER_OF_DISABLED][MAX_DATABASE_LENGTH]; @@ -568,21 +620,27 @@ pgagroal_management_read_status(SSL* ssl, int socket) int active; int total; int max; + int max_connections = 0; + int limits = 0; + int servers = 0; + char header[12 + MAX_NUMBER_OF_CONNECTIONS]; memset(&buf, 0, sizeof(buf)); memset(&disabled, 0, sizeof(disabled)); + memset(&header, 0, sizeof(header)); + + cJSON* json = json_create_new_command_object(include_details ? "status details" : "status", true, "pgagroal-cli"); + cJSON* output = json_extract_command_output_object(json); if (read_complete(ssl, socket, &buf[0], sizeof(buf))) { - pgagroal_log_warn("pgagroal_management_read_status: read: %d %s", socket, strerror(errno)); - errno = 0; + pgagroal_log_warn("pgagroal_management_json_read_status_details: read: %d %s", socket, strerror(errno)); goto error; } if (read_complete(ssl, socket, &disabled[0], sizeof(disabled))) { - pgagroal_log_warn("pgagroal_management_read_status: read: %d %s", socket, strerror(errno)); - errno = 0; + pgagroal_log_warn("pgagroal_management_json_read_status_details: read: %d %s", socket, strerror(errno)); goto error; } @@ -591,10 +649,24 @@ pgagroal_management_read_status(SSL* ssl, int socket) total = pgagroal_read_int32(&(buf[8])); max = pgagroal_read_int32(&(buf[12])); - printf("Status: %s\n", (status == 1 ? "Running" : "Graceful shutdown")); - printf("Active connections: %d\n", active); - printf("Total connections: %d\n", total); - printf("Max connections: %d\n", max); + // status information + cJSON* status_json = cJSON_CreateObject(); + cJSON_AddStringToObject(status_json, "message", (status == 1 ? "Running" : "Graceful shutdown")); + cJSON_AddNumberToObject(status_json, "status", status); + cJSON_AddItemToObject(output, "status", status_json); + + // define all the information about connections + cJSON* connections = cJSON_CreateObject(); + cJSON_AddNumberToObject(connections, "active", active); + cJSON_AddNumberToObject(connections, "total", total); + cJSON_AddNumberToObject(connections, "max", max); + cJSON_AddItemToObject(output, "connections", connections); + + // define all the information about disabled databases + cJSON* databases = cJSON_CreateObject(); + cJSON* databases_array = cJSON_CreateArray(); + + int counter = 0; for (int i = 0; i < NUMBER_OF_DISABLED; i++) { @@ -602,20 +674,165 @@ pgagroal_management_read_status(SSL* ssl, int socket) { if (!strcmp(disabled[i], "*")) { - printf("Disabled database: ALL\n"); + cJSON_AddItemToArray(databases_array, cJSON_CreateString("ALL")); + counter = -1; } else { - printf("Disabled database: %s\n", disabled[i]); + cJSON_AddItemToArray(databases_array, cJSON_CreateString(disabled[i])); + counter++; } } } - return 0; + cJSON* disabled_databases = cJSON_CreateObject(); + cJSON_AddNumberToObject(disabled_databases, "count", counter); + cJSON_AddStringToObject(disabled_databases, "state", "disabled"); + cJSON_AddItemToObject(disabled_databases, JSON_TAG_ARRAY_NAME, databases_array); + cJSON_AddItemToObject(databases, "disabled", disabled_databases); + cJSON_AddItemToObject(output, "databases", databases); -error: + // the 'status' command ends here + if (!include_details) + { + goto end; + } - return 1; + /*********** 'status details ************/ + + memset(&header, 0, sizeof(header)); + + if (read_complete(ssl, socket, &header[0], sizeof(header))) + { + goto error; + } + + // quantity informations + max_connections = pgagroal_read_int32(&header); + limits = pgagroal_read_int32(&(header[4])); + servers = pgagroal_read_int32(&(header[8])); + + cJSON* json_servers = cJSON_CreateObject(); + cJSON* json_servers_array = cJSON_CreateArray(); + cJSON_AddItemToObject(output, "servers", json_servers); + cJSON_AddNumberToObject(json_servers, "count", servers); + + // details about the servers + for (int i = 0; i < servers; i++) + { + char server[5 + MISC_LENGTH + MISC_LENGTH]; + + memset(&server, 0, sizeof(server)); + + if (read_complete(ssl, socket, &server[0], sizeof(server))) + { + goto error; + } + + cJSON* current_server_json = cJSON_CreateObject(); + cJSON_AddStringToObject(current_server_json, "server", pgagroal_read_string(&(server[0]))); + cJSON_AddStringToObject(current_server_json, "host", pgagroal_read_string(&(server[MISC_LENGTH]))); + cJSON_AddNumberToObject(current_server_json, "port", pgagroal_read_int32(&(server[MISC_LENGTH + MISC_LENGTH]))); + cJSON_AddStringToObject(current_server_json, "state", pgagroal_server_state_as_string(pgagroal_read_byte(&(server[MISC_LENGTH + MISC_LENGTH + 4])))); + + cJSON_AddItemToArray(json_servers_array, current_server_json); + } + + cJSON_AddItemToObject(json_servers, JSON_TAG_ARRAY_NAME, json_servers_array); + + // details about the limits + cJSON* json_limits = cJSON_CreateObject(); + cJSON* json_limits_array = cJSON_CreateArray(); + cJSON_AddItemToObject(json_limits, JSON_TAG_ARRAY_NAME, json_limits_array); + cJSON_AddItemToObject(output, "limits", json_limits); + cJSON_AddNumberToObject(json_limits, "count", limits); + + for (int i = 0; i < limits; i++) + { + char limit[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH]; + memset(&limit, 0, sizeof(limit)); + + if (read_complete(ssl, socket, &limit[0], sizeof(limit))) + { + goto error; + } + + cJSON* current_limit_json = cJSON_CreateObject(); + + cJSON_AddStringToObject(current_limit_json, "database", pgagroal_read_string(&(limit[16]))); + cJSON_AddStringToObject(current_limit_json, "username", pgagroal_read_string(&(limit[16 + MAX_DATABASE_LENGTH]))); + + cJSON* current_connections = cJSON_CreateObject(); + + cJSON_AddNumberToObject(current_connections, "active", pgagroal_read_int32(&(limit))); + cJSON_AddNumberToObject(current_connections, "max", pgagroal_read_int32(&(limit[4]))); + cJSON_AddNumberToObject(current_connections, "initial", pgagroal_read_int32(&(limit[8]))); + cJSON_AddNumberToObject(current_connections, "min", pgagroal_read_int32(&(limit[12]))); + + cJSON_AddItemToObject(current_limit_json, "connections", current_connections); + cJSON_AddItemToArray(json_limits_array, current_limit_json); + + } + + // max connections details (note that the connections json object has been created + // as part of the status output) + cJSON* connections_array = cJSON_CreateArray(); + cJSON_AddItemToObject(connections, JSON_TAG_ARRAY_NAME, connections_array); + + for (int i = 0; i < max_connections; i++) + { + char details[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH + MAX_APPLICATION_NAME]; + signed char state; + long time; + time_t t; + char ts[20] = {0}; + int pid; + char p[10] = {0}; + int fd; + char f[10] = {0}; + + memset(&details, 0, sizeof(details)); + + if (read_complete(ssl, socket, &details[0], sizeof(details))) + { + + goto error; + } + + state = (signed char)header[12 + i]; + time = pgagroal_read_long(&(details[0])); + pid = pgagroal_read_int32(&(details[8])); + fd = pgagroal_read_int32(&(details[12])); + + t = time; + strftime(ts, 20, "%Y-%m-%d %H:%M:%S", localtime(&t)); + + sprintf(p, "%d", pid); + sprintf(f, "%d", fd); + + cJSON* current_connection_json = cJSON_CreateObject(); + + cJSON_AddNumberToObject(current_connection_json, "number", i); + cJSON_AddStringToObject(current_connection_json, "state", pgagroal_server_state_as_string(state)); + cJSON_AddStringToObject(current_connection_json, "time", time > 0 ? ts : ""); + cJSON_AddStringToObject(current_connection_json, "pid", pid > 0 ? p : ""); + cJSON_AddStringToObject(current_connection_json, "fd", fd > 0 ? f : ""); + cJSON_AddStringToObject(current_connection_json, "database", pgagroal_read_string(&(details[16]))); + cJSON_AddStringToObject(current_connection_json, "user", pgagroal_read_string(&(details[16 + MAX_DATABASE_LENGTH]))); + cJSON_AddStringToObject(current_connection_json, "detail", pgagroal_read_string(&(details[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH]))); + + cJSON_AddItemToArray(connections_array, current_connection_json); + + } + +end: + return json; + +error: + // set the json object as faulty and erase the errno + json_set_command_object_faulty(json, strerror(errno), errno); + errno = 0; + return json; } int @@ -706,143 +923,31 @@ pgagroal_management_details(SSL* ssl, int fd) } int -pgagroal_management_read_details(SSL* ssl, int socket) +pgagroal_management_read_details(SSL* ssl, int socket, char output_format) { - char header[12 + MAX_NUMBER_OF_CONNECTIONS]; - int max_connections = 0; - int limits = 0; - int servers = 0; + cJSON* json = pgagroal_management_json_read_status_details(ssl, socket, true); - memset(&header, 0, sizeof(header)); - - if (read_complete(ssl, socket, &header[0], sizeof(header))) + // check we have an answer and it is not an error + if (!json || json_is_command_object_faulty(json)) { - pgagroal_log_warn("pgagroal_management_read_details: read: %d %s", socket, strerror(errno)); - errno = 0; goto error; } - max_connections = pgagroal_read_int32(&header); - limits = pgagroal_read_int32(&(header[4])); - servers = pgagroal_read_int32(&(header[8])); - - for (int i = 0; i < servers; i++) + // print out the command answer + if (output_format == COMMAND_OUTPUT_FORMAT_JSON) { - char server[5 + MISC_LENGTH + MISC_LENGTH]; - signed char state; - - memset(&server, 0, sizeof(server)); - - if (read_complete(ssl, socket, &server[0], sizeof(server))) - { - pgagroal_log_warn("pgagroal_management_read_details: read: %d %s", socket, strerror(errno)); - errno = 0; - goto error; - } - - state = pgagroal_read_byte(&(server[MISC_LENGTH + MISC_LENGTH + 4])); - - printf("---------------------\n"); - printf("Server: %s\n", pgagroal_read_string(&(server[0]))); - printf("Host: %s\n", pgagroal_read_string(&(server[MISC_LENGTH]))); - printf("Port: %d\n", pgagroal_read_int32(&(server[MISC_LENGTH + MISC_LENGTH]))); - - switch (state) - { - case SERVER_NOTINIT: - printf("State: Not init\n"); - break; - case SERVER_NOTINIT_PRIMARY: - printf("State: Not init (primary)\n"); - break; - case SERVER_PRIMARY: - printf("State: Primary\n"); - break; - case SERVER_REPLICA: - printf("State: Replica\n"); - break; - case SERVER_FAILOVER: - printf("State: Failover\n"); - break; - case SERVER_FAILED: - printf("State: Failed\n"); - break; - default: - printf("State: %d\n", state); - break; - } - } - - printf("---------------------\n"); - - for (int i = 0; i < limits; i++) - { - char limit[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH]; - - memset(&limit, 0, sizeof(limit)); - - if (read_complete(ssl, socket, &limit[0], sizeof(limit))) - { - pgagroal_log_warn("pgagroal_management_read_details: read: %d %s", socket, strerror(errno)); - errno = 0; - goto error; - } - - printf("Database: %s\n", pgagroal_read_string(&(limit[16]))); - printf("Username: %s\n", pgagroal_read_string(&(limit[16 + MAX_DATABASE_LENGTH]))); - printf("Active connections: %d\n", pgagroal_read_int32(&(limit))); - printf("Max connections: %d\n", pgagroal_read_int32(&(limit[4]))); - printf("Initial connections: %d\n", pgagroal_read_int32(&(limit[8]))); - printf("Min connections: %d\n", pgagroal_read_int32(&(limit[12]))); - printf("---------------------\n"); + json_print_and_free_json_object(json); } - - for (int i = 0; i < max_connections; i++) + else { - char details[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH + MAX_APPLICATION_NAME]; - signed char state; - long time; - time_t t; - char ts[20] = {0}; - int pid; - char p[10] = {0}; - int fd; - char f[10] = {0}; - - memset(&details, 0, sizeof(details)); - - if (read_complete(ssl, socket, &details[0], sizeof(details))) - { - pgagroal_log_warn("pgagroal_management_read_details: read: %d %s", socket, strerror(errno)); - errno = 0; - goto error; - } - - state = (signed char)header[12 + i]; - time = pgagroal_read_long(&(details[0])); - pid = pgagroal_read_int32(&(details[8])); - fd = pgagroal_read_int32(&(details[12])); - - t = time; - strftime(ts, 20, "%Y-%m-%d %H:%M:%S", localtime(&t)); - - sprintf(p, "%d", pid); - sprintf(f, "%d", fd); - - printf("Connection %4d: %-15s %-19s %-6s %-6s %s %s %s\n", - i, - pgagroal_get_state_string(state), - time > 0 ? ts : "", - pid > 0 ? p : "", - fd > 0 ? f : "", - pgagroal_read_string(&(details[16])), - pgagroal_read_string(&(details[16 + MAX_DATABASE_LENGTH])), - pgagroal_read_string(&(details[16 + MAX_DATABASE_LENGTH + MAX_USERNAME_LENGTH]))); + pgagroal_management_json_print_status_details(json); } return 0; error: + pgagroal_log_warn("pgagroal_management_read_details: command error [%s]", + (json == NULL ? "" : json_get_command_object_status(json))); return 1; } @@ -962,7 +1067,7 @@ pgagroal_management_isalive(SSL* ssl, int fd) } int -pgagroal_management_read_isalive(SSL* ssl, int socket, int* status) +pgagroal_management_read_isalive(SSL* ssl, int socket, int* status, char output_format) { char buf[4]; @@ -977,6 +1082,31 @@ pgagroal_management_read_isalive(SSL* ssl, int socket, int* status) *status = pgagroal_read_int32(&buf); + // do I need to provide JSON output? + if (output_format == COMMAND_OUTPUT_FORMAT_JSON) + { + cJSON* json = json_create_new_command_object("ping", true, "pgagroal-cli"); + cJSON* output = json_extract_command_output_object(json); + + cJSON_AddNumberToObject(output, "status", *status); + + if (*status == PING_STATUS_RUNNING) + { + cJSON_AddStringToObject(output, "message", "running"); + } + else if (*status == PING_STATUS_SHUTDOWN_GRACEFULLY) + { + cJSON_AddStringToObject(output, "message", "shutdown gracefully"); + } + else + { + cJSON_AddStringToObject(output, "message", "unknown"); + } + + json_print_and_free_json_object(json); + + } + return 0; error: @@ -993,11 +1123,11 @@ pgagroal_management_write_isalive(int socket, bool gracefully) if (!gracefully) { - pgagroal_write_int32(buf, 1); + pgagroal_write_int32(buf, PING_STATUS_RUNNING); } else { - pgagroal_write_int32(buf, 2); + pgagroal_write_int32(buf, PING_STATUS_SHUTDOWN_GRACEFULLY); } if (write_complete(NULL, socket, &buf, sizeof(buf))) @@ -1608,11 +1738,105 @@ pgagroal_management_write_config_get(int socket, char* config_key) } -int -pgagroal_management_read_config_get(int socket, char** data) +/** + * Utility method to wrap the answer about a configuration setting + * into a JSON object. + * + * @param socket the socket from which reading the data from + * @param config_key the key requested, used only to populate the json + * @param expected_value the config value expected in the case of a `config set`. + * If the expetced_value is not null, the function checks if the obtained config value and + * the expected one are equal, and in case are not set the JSON object as faulty. + * + * @return the JSON object + */ +static cJSON* +pgagroal_managment_json_read_config_get(int socket, char* config_key, char* expected_value) { + int size = MISC_LENGTH; - return pgagroal_management_read_payload(socket, MANAGEMENT_CONFIG_GET, &size, data); + char* buffer = NULL; + bool is_config_set = false; + + buffer = calloc(1, size); + if (buffer == NULL) + { + goto error; + } + + if (pgagroal_management_read_payload(socket, MANAGEMENT_CONFIG_GET, &size, &buffer)) + { + goto error; + } + + // is this the answer from a 'conf set' command ? + is_config_set = (expected_value && strlen(expected_value) > 0); + + cJSON* json = json_create_new_command_object(is_config_set ? "conf set" : "conf get", true, "pgagroal-cli"); + cJSON* output = json_extract_command_output_object(json); + cJSON_AddStringToObject(output, "key", config_key); + cJSON_AddStringToObject(output, "value", buffer); + + if (is_config_set) + { + cJSON_AddStringToObject(output, "expected", expected_value); + + // if the expected value is not what we get, this means there is an error + // (e.g., cannot apply the config set) + if (strncmp(buffer, expected_value, size)) + { + json_set_command_object_faulty(json, "Current and expected values are different", EXIT_STATUS_DATA_ERROR); + } + } + + free(buffer); + return json; +error: + if (buffer) + { + free(buffer); + } + return NULL; +} + +int +pgagroal_management_read_config_get(int socket, char* config_key, char* expected_value, bool verbose, char output_format) +{ + + cJSON* json = pgagroal_managment_json_read_config_get(socket, config_key, expected_value); + + if (!json) + { + goto error; + } + + if (output_format == COMMAND_OUTPUT_FORMAT_JSON) + { + json_print_and_free_json_object(json); + goto end; + } + + // if here, print out in text format + cJSON* output = json_extract_command_output_object(json); + cJSON* value = cJSON_GetObjectItemCaseSensitive(output, "value"); + cJSON* key = cJSON_GetObjectItemCaseSensitive(output, "key"); + if (verbose) + { + printf("%s = %s\n", key->valuestring, value->valuestring); + } + else + { + printf("%s\n", value->valuestring); + } + +end: + return json_command_object_exit_status(json); + +error: + + pgagroal_log_warn("pgagroal_management_read_config_get : error retrieving configuration for <%s> : %s", config_key, strerror(errno)); + errno = 0; + return EXIT_STATUS_DATA_ERROR; } int @@ -1750,69 +1974,31 @@ pgagroal_management_conf_ls(SSL* ssl, int fd) } int -pgagroal_management_read_conf_ls(SSL* ssl, int socket) +pgagroal_management_read_conf_ls(SSL* ssl, int socket, char output_format) { - char buf[4]; - char* buffer; - - memset(&buf, 0, sizeof(buf)); - buffer = calloc(1, MAX_PATH); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) - { - goto error; - } - printf("Main Configuration file: %s\n", buffer); + // get the JSON output + cJSON* json = pgagroal_management_json_read_conf_ls(ssl, socket); - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + // check we have an answer and it is not an error + if (!json || json_is_command_object_faulty(json)) { goto error; } - printf("HBA file: %s\n", buffer); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + // print out the command answer + if (output_format == COMMAND_OUTPUT_FORMAT_JSON) { - goto error; + json_print_and_free_json_object(json); } - - printf("Limit file: %s\n", buffer); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) - { - goto error; - } - - printf("Frontend users file: %s\n", buffer); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) - { - goto error; - } - - printf("Admins file: %s\n", buffer); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) - { - goto error; - } - - printf("Superuser file: %s\n", buffer); - - if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + else { - goto error; + pgagroal_management_json_print_conf_ls(json); } - printf("Users file: %s\n", buffer); - - free(buffer); - return 0; error: - free(buffer); pgagroal_log_warn("pgagroal_management_read_conf_ls: read: %d %s", socket, strerror(errno)); errno = 0; @@ -1971,3 +2157,308 @@ pgagroal_management_read_conf_ls_detail(SSL* ssl, int socket, char* buffer) return 1; } + +/** + * Utility function to print out the result of a 'status' + * or a 'status details' command already wrapped into a + * JSON object. + * The function tries to understand from the command name + * within the JSON object if the output refers to the + * 'status' or 'status details' command. + * + * If the command is faulty, this method does nothing, therefore + * printing out information about faulty commands has to be done + * at an higher level. + * + * @param json the JSON object + * + * @returns 0 on success + */ +int +pgagroal_management_json_print_status_details(cJSON* json) +{ + bool is_command_details = false; /* is this command 'status details' ? */ + + // sanity check + if (!json || json_is_command_object_faulty(json)) + { + return 1; + } + + // the command must be 'status' or 'status details' + if (json_is_command_name_equals_to(json, "status")) + { + is_command_details = false; + } + else if (json_is_command_name_equals_to(json, "status details")) + { + is_command_details = true; + } + else + { + goto error; + } + + // now get the output and start printing it + cJSON* output = json_extract_command_output_object(json); + + // overall status + printf("Status: %s\n", + cJSON_GetObjectItemCaseSensitive(cJSON_GetObjectItemCaseSensitive(output, "status"), "message")->valuestring); + + // connections + cJSON* connections = cJSON_GetObjectItemCaseSensitive(output, "connections"); + if (!connections) + { + goto error; + } + + printf("Active connections: %d\n", cJSON_GetObjectItemCaseSensitive(connections, "active")->valueint); + printf("Total connections: %d\n", cJSON_GetObjectItemCaseSensitive(connections, "total")->valueint); + printf("Max connections: %d\n", cJSON_GetObjectItemCaseSensitive(connections, "max")->valueint); + + // databases + cJSON* databases = cJSON_GetObjectItemCaseSensitive(output, "databases"); + if (!databases) + { + goto error; + } + + cJSON* disabled_databases = cJSON_GetObjectItemCaseSensitive(databases, "disabled"); + if (!disabled_databases) + { + goto error; + } + + cJSON* disabled_databases_list = cJSON_GetObjectItemCaseSensitive(disabled_databases, JSON_TAG_ARRAY_NAME); + cJSON* current; + cJSON_ArrayForEach(current, disabled_databases_list) + { + printf("Disabled database: %s\n", current->valuestring); + } + + // the status command ends here + if (!is_command_details) + { + goto end; + } + + // dump the servers information + cJSON* servers = cJSON_GetObjectItemCaseSensitive(output, "servers"); + if (!servers) + { + goto error; + } + + cJSON* servers_list = cJSON_GetObjectItemCaseSensitive(servers, JSON_TAG_ARRAY_NAME); + cJSON_ArrayForEach(current, servers_list) + { + printf("---------------------\n"); + printf("Server: %s\n", cJSON_GetObjectItemCaseSensitive(current, "server")->valuestring); + printf("Host: %s\n", cJSON_GetObjectItemCaseSensitive(current, "host")->valuestring); + printf("Port: %d\n", cJSON_GetObjectItemCaseSensitive(current, "port")->valueint); + printf("State: %s\n", cJSON_GetObjectItemCaseSensitive(current, "state")->valuestring); + printf("---------------------\n"); + + } + + // dump the limits information + cJSON* limits = cJSON_GetObjectItemCaseSensitive(output, "limits"); + cJSON* limits_list = cJSON_GetObjectItemCaseSensitive(limits, JSON_TAG_ARRAY_NAME); + cJSON_ArrayForEach(current, limits_list) + { + printf("---------------------\n"); + printf("Database: %s\n", cJSON_GetObjectItemCaseSensitive(current, "database")->valuestring); + printf("Username: %s\n", cJSON_GetObjectItemCaseSensitive(current, "username")->valuestring); + cJSON* current_connections = cJSON_GetObjectItemCaseSensitive(current, "connections"); + printf("Active connections: %d\n", cJSON_GetObjectItemCaseSensitive(current_connections, "active")->valueint); + printf("Max connections: %d\n", cJSON_GetObjectItemCaseSensitive(current_connections, "max")->valueint); + printf("Initial connections: %d\n", cJSON_GetObjectItemCaseSensitive(current_connections, "initial")->valueint); + printf("Min connections: %d\n", cJSON_GetObjectItemCaseSensitive(current_connections, "min")->valueint); + printf("---------------------\n"); + } + + // print the connection information + int i = 0; + cJSON_ArrayForEach(current, cJSON_GetObjectItemCaseSensitive(connections, JSON_TAG_ARRAY_NAME)) + { + printf("Connection %4d: %-15s %-19s %-6s %-6s %s %s %s\n", + i++, + cJSON_GetObjectItemCaseSensitive(current, "state")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "time")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "pid")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "fd")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "user")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "database")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "detail")->valuestring); + + } + +end: + return 0; + +error: + return 1; + +} + +/** + * Utility method to get the information about the `conf ls` command. + * This method produces a cJSON object that needs to be printed out in textual format. + * + * @param ssl the SSL file descriptor + * @param socket the file descriptor for the socket + * + * @returns the cJSON object, faulty if something went wrong + */ +static cJSON* +pgagroal_management_json_read_conf_ls(SSL* ssl, int socket) +{ + char buf[4]; + char* buffer; + + cJSON* json = json_create_new_command_object("conf ls", true, "pgagroal-cli"); + cJSON* output = json_extract_command_output_object(json); + + // add an array that will contain the files + cJSON* files = cJSON_CreateObject(); + cJSON* files_array = cJSON_CreateArray(); + cJSON_AddItemToObject(output, "files", files); + cJSON_AddItemToObject(files, JSON_TAG_ARRAY_NAME, files_array); + // cJSON_AddItemToArray(databases_array, cJSON_CreateString("ALL")); + + memset(&buf, 0, sizeof(buf)); + buffer = calloc(1, MAX_PATH); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the main configuration file entry + cJSON* mainConf = cJSON_CreateObject(); + cJSON_AddStringToObject(mainConf, "description", "Main Configuration file"); + cJSON_AddStringToObject(mainConf, "path", buffer); + cJSON_AddItemToArray(files_array, mainConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the HBA file + cJSON* hbaConf = cJSON_CreateObject(); + cJSON_AddStringToObject(hbaConf, "description", "HBA File"); + cJSON_AddStringToObject(hbaConf, "path", buffer); + cJSON_AddItemToArray(files_array, hbaConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the limit file + cJSON* limitConf = cJSON_CreateObject(); + cJSON_AddStringToObject(limitConf, "description", "Limit file"); + cJSON_AddStringToObject(limitConf, "path", buffer); + cJSON_AddItemToArray(files_array, limitConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the frontend file + cJSON* frontendConf = cJSON_CreateObject(); + cJSON_AddStringToObject(frontendConf, "description", "Frontend users file"); + cJSON_AddStringToObject(frontendConf, "path", buffer); + cJSON_AddItemToArray(files_array, frontendConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the admins file + cJSON* adminsConf = cJSON_CreateObject(); + cJSON_AddStringToObject(adminsConf, "description", "Admins file"); + cJSON_AddStringToObject(adminsConf, "path", buffer); + cJSON_AddItemToArray(files_array, adminsConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the superuser file + cJSON* superuserConf = cJSON_CreateObject(); + cJSON_AddStringToObject(superuserConf, "description", "Superuser file"); + cJSON_AddStringToObject(superuserConf, "path", buffer); + cJSON_AddItemToArray(files_array, superuserConf); + + if (pgagroal_management_read_conf_ls_detail(ssl, socket, buffer)) + { + goto error; + } + + // add the users file + cJSON* usersConf = cJSON_CreateObject(); + cJSON_AddStringToObject(usersConf, "description", "Users file"); + cJSON_AddStringToObject(usersConf, "path", buffer); + cJSON_AddItemToArray(files_array, usersConf); + + // all done + goto end; + +error: + free(buffer); + pgagroal_log_warn("pgagroal_management_json_read_conf_ls: read: %d %s", socket, strerror(errno)); + errno = 0; + json_set_command_object_faulty(json, strerror(errno), errno); + +end: + free(buffer); + return json; + +} + +/** + * Utility function to handle a JSON object and print it out + * as normal text. + * + * @param json the JSON object + * @returns 0 on success + */ +static int +pgagroal_management_json_print_conf_ls(cJSON* json) +{ + // sanity check + if (!json || json_is_command_object_faulty(json)) + { + goto error; + } + + // now get the output and start printing it + cJSON* output = json_extract_command_output_object(json); + + // files + cJSON* files = cJSON_GetObjectItemCaseSensitive(output, "files"); + if (!files) + { + goto error; + } + + cJSON* files_array = cJSON_GetObjectItemCaseSensitive(files, JSON_TAG_ARRAY_NAME); + cJSON* current; + cJSON_ArrayForEach(current, files_array) + { + // the current JSON object is made by two different values + printf("%-25s : %s\n", + cJSON_GetObjectItemCaseSensitive(current, "description")->valuestring, + cJSON_GetObjectItemCaseSensitive(current, "path")->valuestring); + } + +error: + cJSON_Delete(json); + return 1; +} diff --git a/src/libpgagroal/utils.c b/src/libpgagroal/utils.c index 52406571..d1b67043 100644 --- a/src/libpgagroal/utils.c +++ b/src/libpgagroal/utils.c @@ -1077,3 +1077,34 @@ parse_command_simple(int argc, { return parse_command(argc, argv, offset, command, subcommand, NULL, NULL, NULL, NULL); } + +/** + * Given a server state, it returns a string that + * described the state in a human-readable form. + * + * If the state cannot be determined, the numeric + * form of the state is returned as a string. + * + * @param state the value of the sate for the server + * @returns the string representing the state + */ +char* +pgagroal_server_state_as_string(signed char state) +{ + char* buf; + + switch (state) + { + case SERVER_NOTINIT: return "Not init"; + case SERVER_NOTINIT_PRIMARY: return "Not init (primary)"; + case SERVER_PRIMARY: return "Primary"; + case SERVER_REPLICA: return "Replica"; + case SERVER_FAILOVER: return "Failover"; + case SERVER_FAILED: return "Failed"; + default: + buf = malloc(5); + memset(buf, 0, 5); + snprintf(buf, 5, "%d", state); + return buf; + } +}