diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb1429ec..9cd0fa05 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 @@ -63,6 +65,8 @@ jobs: run: brew install openssl - name: Install libev run: brew install libev + - name: Install cJSON + run: brew install cjson - name: Install rst2man run: brew install docutils - name: Install clang diff --git a/CMakeLists.txt b/CMakeLists.txt index 94c34ae8..a60633b4 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 version ${CJSON_VERSION}") +else () + message(FATAL_ERROR "cJSON needed") +endif() + + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") find_package(Libatomic) if (LIBATOMIC_FOUND) diff --git a/README.md b/README.md index bebe5bb8..148be58f 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,15 @@ See [Architecture](./doc/ARCHITECTURE.md) for the architecture of `pgagroal`. * [OpenSSL](http://www.openssl.org/) * [systemd](https://www.freedesktop.org/wiki/Software/systemd/) * [rst2man](https://docutils.sourceforge.io/) +* [cJSON](https://github.com/DaveGamble/cJSON) + +Example of installation of the requirements on a Rocky Linux (or similar) system: ```sh -dnf install git gcc cmake make libev libev-devel openssl openssl-devel systemd systemd-devel python3-docutils +dnf install git gcc cmake make libev libev-devel openssl openssl-devel \ + systemd systemd-devel python3-docutils \ + cjson cjson-devel + ``` Alternative [clang 8+](https://clang.llvm.org/) can be used. diff --git a/cmake/FindcJSON.cmake b/cmake/FindcJSON.cmake new file mode 100644 index 00000000..13530aea --- /dev/null +++ b/cmake/FindcJSON.cmake @@ -0,0 +1,51 @@ +# FindcJSON.cmake +# Tries to find cJSON libraries on the system +# (e.g., on Rocky Linux: cjson and cjson-devel) +# +# Inspired by +# +# If cJSON is found, sets the following variables: +# - CJSON_INCLUDE_DIR +# - CJSON_LIBRARY +# - CJSON_VERSION +# +# In the header file cJSON.h the library version is specified as: +# #define CJSON_VERSION_MAJOR 1 +# #define CJSON_VERSION_MINOR 7 +# #define CJSON_VERSION_PATCH 14 + + +find_path( + CJSON_INCLUDE_DIR + NAMES cjson/cJSON.h + PATH_SUFFIXES include) +find_library( + CJSON_LIBRARY + NAMES cjson + PATH_SUFFIXES lib) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(cJSON REQUIRED_VARS CJSON_INCLUDE_DIR + CJSON_LIBRARY) +if(CJSON_FOUND) + # these variables are needed for the build + set( CJSON_INCLUDE_DIRS "${CJSON_INCLUDE_DIR}" ) + set( CJSON_LIBRARIES "${CJSON_LIBRARY}" ) + + # try to get out the library version from the headers + file(STRINGS "${CJSON_INCLUDE_DIR}/cjson/cJSON.h" + CJSON_VERSION_MAJOR REGEX "^#define[ \t]+CJSON_VERSION_MAJOR[ \t]+[0-9]+") + file(STRINGS "${CJSON_INCLUDE_DIR}/cjson/cJSON.h" + CJSON_VERSION_MINOR REGEX "^#define[ \t]+CJSON_VERSION_MINOR[ \t]+[0-9]+") + file(STRINGS "${CJSON_INCLUDE_DIR}/cjson/cJSON.h" + CJSON_VERSION_PATCH REGEX "^#define[ \t]+CJSON_VERSION_PATCH[ \t]+[0-9]+") + string(REGEX REPLACE "[^0-9]+" "" CJSON_VERSION_MAJOR "${CJSON_VERSION_MAJOR}") + string(REGEX REPLACE "[^0-9]+" "" CJSON_VERSION_MINOR "${CJSON_VERSION_MINOR}") + string(REGEX REPLACE "[^0-9]+" "" CJSON_VERSION_PATCH "${CJSON_VERSION_PATCH}") + set(CJSON_VERSION "${CJSON_VERSION_MAJOR}.${CJSON_VERSION_MINOR}.${CJSON_VERSION_PATCH}") + unset(CJSON_VERSION_MINOR) + unset(CJSON_VERSION_MAJOR) + unset(CJSON_VERSION_PATCH) +endif() + +mark_as_advanced( CJSON_INCLUDE_DIR CJSON_LIBRARY ) 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 d59fd0ec..0ce08efc 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 cd640027..167473e4 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 f9fd7db5..adbdd7f9 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 3dd4719e..647e18fe 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 896e8ca1..d24064fa 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; + } +}