diff --git a/NEWS.adoc b/NEWS.adoc index 9c3f60f73a..1e2c260bda 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`) 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 `NOTIFYTYPE` environment variables were neutered for the timer daemon, diff --git a/clients/upsstats.c b/clients/upsstats.c index 32c93055fb..9cb79f7865 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("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("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 ((unsigned char)*in < 32) {
+ /* Print control characters as unicode */
+ printf("\\u%04x", (unsigned 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..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
@@ -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 *"devices"* 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
+ "devices" 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
-----