From 135499bfddb6177775d678c89e2866804c5ee62e Mon Sep 17 00:00:00 2001 From: Atanas Vladimirov Date: Sun, 9 Nov 2025 21:40:06 +0200 Subject: [PATCH 1/5] upsstats: Add JSON output mode via ?json parameter When the ?json (or &json) parameter is added to the URL, upsstats.cgi will now bypass all HTML template processing. Instead, it returns a "Content-Type: application/json" response containing the full data for the requested UPS(es), including all variables, raw status, and parsed status. This provides a modern API for external monitoring tools without affecting the existing HTML template functionality. Signed-off-by: Atanas Vladimirov --- NEWS.adoc | 3 + clients/upsstats.c | 301 +++++++++++++++++++++++++++++++++----- docs/man/upsstats.cgi.txt | 36 +++++ 3 files changed, 301 insertions(+), 39 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 9c3f60f73a..79f2c9eb26 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -193,6 +193,9 @@ several `FSD` notifications into one executed action. [PR #3097] - `upsset` should now recognize `RANGE NUMBER` and `NUMBER` types. [#3164] + - `upsstats` has now JSON output mode via ?json (or &json) + parameter. [issue #2524 and #2525, PR #3171] + - `upssched` tool updates: * Previously in PR #2896 (NUT releases v2.8.3 and v2.8.4) the `UPSNAME` and `NOTIFYTYPE` environment variables were neutered for the timer daemon, diff --git a/clients/upsstats.c b/clients/upsstats.c index 32c93055fb..dd8d5c92a0 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -36,6 +36,7 @@ static char *monhost = NULL; static int use_celsius = 1, refreshdelay = -1, treemode = 0; +static int output_json = 0; /* from cgilib's checkhost() */ static char *monhostdesc = NULL; @@ -71,6 +72,10 @@ void parsearg(char *var, char *value) /* FIXME: Validate that treemode is allowed */ treemode = 1; } + + if (!strcmp(var, "json")) { + output_json = 1; + } } static void report_error(void) @@ -111,7 +116,8 @@ static int get_var(const char *var, char *buf, size_t buflen, int verbose) const char *query[4]; char **answer; - if (!check_ups_fd(1)) + /* pass verbose to check_ups_fd */ + if (!check_ups_fd(verbose)) return 0; if (!upsname) { @@ -354,13 +360,13 @@ static void ups_connect(void) { static ulist_t *lastups = NULL; char *newups, *newhost; - uint16_t newport; + uint16_t newport = 0; /* try to minimize reconnects */ if (lastups) { /* don't reconnect if these are both the same UPS */ - if (!strcmp(lastups->sys, currups->sys)) { + if (currups && !strcmp(lastups->sys, currups->sys)) { lastups = currups; return; } @@ -368,7 +374,7 @@ static void ups_connect(void) /* see if it's just on the same host */ newups = newhost = NULL; - if (upscli_splitname(currups->sys, &newups, &newhost, + if (currups && upscli_splitname(currups->sys, &newups, &newhost, &newport) != 0) { printf("Unusable UPS definition [%s]\n", currups->sys); fprintf(stderr, "Unusable UPS definition [%s]\n", @@ -376,7 +382,7 @@ static void ups_connect(void) exit(EXIT_FAILURE); } - if ((!strcmp(newhost, hostname)) && (port == newport)) { + if (currups && hostname && (!strcmp(newhost, hostname)) && (port == newport)) { free(upsname); upsname = newups; @@ -394,14 +400,16 @@ static void ups_connect(void) free(upsname); free(hostname); + upsname = NULL; + hostname = NULL; - if (upscli_splitname(currups->sys, &upsname, &hostname, &port) != 0) { + if (currups && upscli_splitname(currups->sys, &upsname, &hostname, &port) != 0) { printf("Unusable UPS definition [%s]\n", currups->sys); fprintf(stderr, "Unusable UPS definition [%s]\n", currups->sys); exit(EXIT_FAILURE); } - if (upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) + if (currups && upscli_connect(&ups, hostname, port, UPSCLI_CONN_TRYSSL) < 0) fprintf(stderr, "UPS [%s]: can't connect to server: %s\n", currups->sys, upscli_strerror(&ups)); lastups = currups; @@ -597,7 +605,7 @@ static void do_degrees(void) static void do_statuscolor(void) { int severity, i; - char stat[SMALLBUF], *sp, *ptr; + char stat[SMALLBUF], *ptr, *last = NULL; if (!check_ups_fd(0)) { @@ -613,20 +621,15 @@ static void do_statuscolor(void) } severity = 0; - sp = stat; - while (sp) { - ptr = strchr(sp, ' '); - if (ptr) - *ptr++ = '\0'; + /* Use strtok_r for safety */ + for (ptr = strtok_r(stat, " \n", &last); ptr != NULL; ptr = strtok_r(NULL, " \n", &last)) { /* expand from table in status.h */ for (i = 0; stattab[i].name != NULL; i++) - if (!strcmp(stattab[i].name, sp)) + if (stattab[i].name && ptr && !strcmp(stattab[i].name, ptr)) if (stattab[i].severity > severity) severity = stattab[i].severity; - - sp = ptr; } switch(severity) { @@ -978,13 +981,18 @@ static void load_hosts_conf(void) if (!pconf_file_begin(&ctx, fn)) { pconf_finish(&ctx); - printf("\n"); - printf("\n"); - printf("Error: can't open hosts.conf\n"); - printf("\n"); - printf("Error: can't open hosts.conf\n"); - printf("\n"); + /* Don't print HTML here if we are in JSON mode. + * The JSON function will handle the error. + */ + if (!output_json) { + printf("\n"); + printf("\n"); + printf("Error: can't open hosts.conf\n"); + printf("\n"); + printf("Error: can't open hosts.conf\n"); + printf("\n"); + } /* leave something for the admin */ fprintf(stderr, "upsstats: %s\n", ctx.errmsg); @@ -1010,13 +1018,18 @@ static void load_hosts_conf(void) pconf_finish(&ctx); if (!ulhead) { - printf("\n"); - printf("\n"); - printf("Error: no hosts to monitor\n"); - printf("\n"); - printf("Error: no hosts to monitor (check hosts.conf)\n"); - printf("\n"); + /* Don't print HTML here if we are in JSON mode. + * The JSON function will handle the error. + */ + if (!output_json) { + printf("\n"); + printf("\n"); + printf("Error: no hosts to monitor\n"); + printf("\n"); + printf("Error: no hosts to monitor (check hosts.conf)\n"); + printf("\n"); + } /* leave something for the admin */ fprintf(stderr, "upsstats: no hosts to monitor\n"); @@ -1046,6 +1059,182 @@ static void display_single(void) upscli_disconnect(&ups); } +/* ------------------------------------------------------------- */ +/* ---NEW FUNCTIONS FOR JSON API ------------------------------- */ +/* ------------------------------------------------------------- */ + +/** + * @brief Helper function to print a string as a JSON-safe string + * @param in The raw C-string to escape + */ +static void json_escape(const char *in) { + if (!in) { + printf("null"); + return; + } + + printf("\""); + while (*in) { + switch (*in) { + case '\"': printf("\\\""); break; + case '\\': printf("\\\\"); break; + case '\b': printf("\\b"); break; + case '\f': printf("\\f"); break; + case '\n': printf("\\n"); break; + case '\r': printf("\\r"); break; + case '\t': printf("\\t"); break; + default: + if (*in >= 0 && (unsigned char)*in < 32) { + /* Print control characters as unicode */ + printf("\\u%04x", (int)*in); + } else { + putchar(*in); + } + break; + } + in++; + } + printf("\""); +} + +/** + * @brief Main JSON output function. + * This function replaces all template logic and outputs a JSON object + * containing data for one or all devices. + */ +static void display_json(void) +{ + size_t numq, numa; + const char *query[4]; + char **answer; + char status_buf[SMALLBUF], status_copy[SMALLBUF]; + int i; + int is_first_status; + int is_first_ups = 1; + int is_first_var = 1; + char *ptr, *last = NULL; + + /* If monhost is set, we're in single-host mode. + * If not, we're in multi-host mode. + * We need to load hosts.conf ONLY in multi-host mode. + */ + if (monhost) { + if (!checkhost(monhost, &monhostdesc)) { + printf("{\"error\": \"Access to host %s is not authorized.\"}", monhost); + return; + } + add_ups(monhost, monhostdesc); + currups = ulhead; + } else { + load_hosts_conf(); /* This populates ulhead */ + currups = ulhead; + } + + if (!currups) { + /* load_hosts_conf() would have exited, but check anyway */ + printf("{\"error\": \"No hosts to monitor.\"}"); + return; + } + + /* In multi-host mode, wrap in a root object. + * In single-host mode, just output the single device object. + */ + if (!monhost) { + printf("{\"devices\": [\n"); + } + + /* Loop through all devices (in single-host mode, this is just one) */ + for (currups = ulhead; currups != NULL; currups = currups->next) { + ups_connect(); + + if (!is_first_ups) printf(",\n"); + + if (upscli_fd(&ups) == -1) { + printf(" {\"host\": "); + json_escape(currups->sys); + printf(", \"desc\": "); + json_escape(currups->desc); + printf(", \"error\": \"Connection failed: %s\"}", upscli_strerror(&ups)); + is_first_ups = 0; + continue; + } + + printf(" {\n"); /* Start UPS object */ + printf(" \"host\": "); + json_escape(currups->sys); + printf(",\n"); + printf(" \"desc\": "); + json_escape(currups->desc); + printf(",\n"); + + /* Add pre-processed status, as the old template did */ + if (get_var("ups.status", status_buf, sizeof(status_buf), 0)) { + printf(" \"status_raw\": "); + json_escape(status_buf); + printf(",\n"); + printf(" \"status_parsed\": ["); + + is_first_status = 1; + /* Copy status_buf as strtok_r is destructive */ + strncpy(status_copy, status_buf, sizeof(status_copy)); + status_copy[sizeof(status_copy) - 1] = '\0'; + + for (ptr = strtok_r(status_copy, " \n", &last); ptr != NULL; ptr = strtok_r(NULL, " \n", &last)) { + for (i = 0; stattab[i].name != NULL; i++) { + if (stattab[i].name && ptr && !strcasecmp(stattab[i].name, ptr)) { + if (!is_first_status) printf(", "); + json_escape(stattab[i].desc); + is_first_status = 0; + } + } + } + printf("],\n"); + } + + printf(" \"vars\": {\n"); /* Start vars object */ + is_first_var = 1; + + /* Full tree mode: list all variables */ + query[0] = "VAR"; + query[1] = upsname; + numq = 2; + + if (upscli_list_start(&ups, numq, query) < 0) { + printf(" \"error\": \"Failed to list variables: %s\"", upscli_strerror(&ups)); + } else { + while (upscli_list_next(&ups, numq, query, &numa, &answer) == 1) { + if (numa < 4) continue; /* Invalid response */ + + if (!is_first_var) printf(",\n"); + + printf(" "); + json_escape(answer[2]); /* var name */ + printf(": "); + json_escape(answer[3]); /* var value */ + + is_first_var = 0; + } + } + + printf("\n }\n"); /* End vars object */ + printf(" }"); /* End UPS object */ + + is_first_ups = 0; + upscli_disconnect(&ups); /* Disconnect after each UPS */ + } + + /* Close the root object in multi-host mode */ + if (!monhost) { + printf("\n]}\n"); + } +} + + +/* ------------------------------------------------------------- */ +/* --- END: NEW JSON FUNCTIONS --------------------------------- */ +/* ------------------------------------------------------------- */ + + int main(int argc, char **argv) { NUT_UNUSED_VARIABLE(argc); @@ -1055,6 +1244,33 @@ int main(int argc, char **argv) upscli_init_default_connect_timeout(NULL, NULL, UPSCLI_DEFAULT_CONNECT_TIMEOUT); + /* + * If json is in the query, bypass all HTML and call display_json() + */ + if (output_json) { + printf("Content-type: application/json; charset=utf-8\n"); + printf("Pragma: no-cache\n"); + printf("\n"); + + display_json(); + + /* Clean up memory */ + free(monhost); + while (ulhead) { + currups = ulhead->next; + free(ulhead->sys); + free(ulhead->desc); + free(ulhead); + ulhead = currups; + } + free(upsname); + free(hostname); + + exit(EXIT_SUCCESS); + } + + /* --- Original HTML logic continues below --- */ + printf("Content-type: text/html\n"); printf("Pragma: no-cache\n"); printf("\n"); @@ -1062,18 +1278,25 @@ int main(int argc, char **argv) /* if a host is specified, use upsstats-single.html instead */ if (monhost) { display_single(); - exit(EXIT_SUCCESS); + } else { + /* default: multimon replacement mode */ + load_hosts_conf(); + currups = ulhead; + display_template("upsstats.html"); } - /* default: multimon replacement mode */ - - load_hosts_conf(); - - currups = ulhead; - - display_template("upsstats.html"); - + /* Clean up memory */ + free(monhost); upscli_disconnect(&ups); + free(upsname); + free(hostname); + while (ulhead) { + currups = ulhead->next; + free(ulhead->sys); + free(ulhead->desc); + free(ulhead); + ulhead = currups; + } return 0; } diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index 4215008d15..9958287e0e 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -48,6 +48,42 @@ When monitoring a single UPS, the file displayed is The format of these files, including the possible commands, is documented in linkman:upsstats.html[5]. +JSON OUTPUT +----------- + +In addition to processing HTML templates, *upsstats.cgi* can be +invoked as a JSON API by adding the *json* parameter to the +query string (e.g., *?json* or *&json*). + +When this parameter is present, the CGI script bypasses all HTML +template parsing and instead returns a JSON object. + +The structure of the JSON output depends on whether the *host* +parameter is also provided: + +* *Multi-host (Default):* + If no *host* parameter is specified, the script returns a + single JSON object containing an *"upses"* key. This key + holds an array, with each element in the array being a JSON + object representing a monitored UPS. + +* *Single-host Mode:* + If a *host* parameter is provided (e.g., + *?host=myups@localhost&json*), the script returns a single + JSON object for that UPS (it will not be wrapped in an + "upses" array). + +In both modes, each UPS object includes: + +* *host*: The UPS identifier (e.g., "myups@localhost") +* *desc*: The host description from ``hosts.conf`` +* *status_raw*: The raw status string (e.g., "OL") +* *status_parsed*: An array of human-readable status strings + (e.g., ["Online"]) +* *vars*: An object containing the full tree of all available + variables for that UPS (e.g., "battery.charge": "100", + "ups.model": "...") + FILES ----- From fd629dfc6281ea369ac101b6f9ed60e1be9d1c8d Mon Sep 17 00:00:00 2001 From: Atanas Vladimirov Date: Mon, 10 Nov 2025 11:12:10 +0200 Subject: [PATCH 2/5] docs/man/upsstats.cgi.txt: Use `devices` instead of upses. Signed-off-by: Atanas Vladimirov --- docs/man/upsstats.cgi.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/man/upsstats.cgi.txt b/docs/man/upsstats.cgi.txt index 9958287e0e..0d814b049a 100644 --- a/docs/man/upsstats.cgi.txt +++ b/docs/man/upsstats.cgi.txt @@ -21,7 +21,7 @@ DESCRIPTION *upsstats.cgi* uses template files to build web pages containing status information from UPS hardware. It can repeat sections of those template files to -monitor several UPSes simultaneously, or focus on a single UPS. +monitor several devices simultaneously, or focus on a single UPS. These templates can also include references to linkman:upsimage.cgi[8] for graphical displays of battery charge levels, voltage readings, and @@ -63,7 +63,7 @@ parameter is also provided: * *Multi-host (Default):* If no *host* parameter is specified, the script returns a - single JSON object containing an *"upses"* key. This key + single JSON object containing an *"devices"* key. This key holds an array, with each element in the array being a JSON object representing a monitored UPS. @@ -71,7 +71,7 @@ parameter is also provided: If a *host* parameter is provided (e.g., *?host=myups@localhost&json*), the script returns a single JSON object for that UPS (it will not be wrapped in an - "upses" array). + "devices" array). In both modes, each UPS object includes: From 31265296a5792c142666d4aa63aef59d9afae00e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 10 Nov 2025 12:25:48 +0100 Subject: [PATCH 3/5] NEWS.adoc: rephrase the upsstats+JSON entry [#3171] Signed-off-by: Jim Klimov --- NEWS.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 79f2c9eb26..1e2c260bda 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -193,8 +193,8 @@ several `FSD` notifications into one executed action. [PR #3097] - `upsset` should now recognize `RANGE NUMBER` and `NUMBER` types. [#3164] - - `upsstats` has now JSON output mode via ?json (or &json) - parameter. [issue #2524 and #2525, PR #3171] + - `upsstats` has now JSON output mode via `?json` (or `&json`) CGI query + parameter. [issue #2524, PR #3171] - `upssched` tool updates: * Previously in PR #2896 (NUT releases v2.8.3 and v2.8.4) the `UPSNAME` and From fdf88f9ceb919144f7e793a611d48441e2790fbb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 10 Nov 2025 22:07:06 +0100 Subject: [PATCH 4/5] clients/upsstats.c: printf("%x") requires an UNSIGNED int argument [#3171] Signed-off-by: Jim Klimov --- clients/upsstats.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/upsstats.c b/clients/upsstats.c index dd8d5c92a0..41337c4aa5 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -1086,7 +1086,7 @@ static void json_escape(const char *in) { default: if (*in >= 0 && (unsigned char)*in < 32) { /* Print control characters as unicode */ - printf("\\u%04x", (int)*in); + printf("\\u%04x", (unsigned int)*in); } else { putchar(*in); } From 0d8aa8bff16269ee703bbbfaa3103005a29034b8 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Tue, 11 Nov 2025 07:56:00 +0100 Subject: [PATCH 5/5] clients/upsstats.c: json_escape(): reduce the char value range-check [#3171] Avoid `comparison is always true due to limited range of data type` warning. Signed-off-by: Jim Klimov --- clients/upsstats.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/upsstats.c b/clients/upsstats.c index 41337c4aa5..9cb79f7865 100644 --- a/clients/upsstats.c +++ b/clients/upsstats.c @@ -1084,7 +1084,7 @@ static void json_escape(const char *in) { case '\r': printf("\\r"); break; case '\t': printf("\\t"); break; default: - if (*in >= 0 && (unsigned char)*in < 32) { + if ((unsigned char)*in < 32) { /* Print control characters as unicode */ printf("\\u%04x", (unsigned int)*in); } else {