From 8dae3b548921481dac0aa3d55275095f70c3ddf8 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 08:59:35 +0200 Subject: [PATCH 01/41] drivers/, docs/: introduce failover driver Signed-off-by: Sebastian Kuttnig --- docs/man/Makefile.am | 3 + docs/man/failover.txt | 229 ++++ docs/nut.dict | 16 +- drivers/Makefile.am | 5 +- drivers/failover.c | 2382 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 2633 insertions(+), 2 deletions(-) create mode 100644 docs/man/failover.txt create mode 100644 drivers/failover.c diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index 481373768b..fccdd10e9b 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -856,6 +856,7 @@ SRC_SERIAL_PAGES = \ clone.txt \ clone-outlet.txt \ dummy-ups.txt \ + failover.txt \ etapro.txt \ everups.txt \ gamatronic.txt \ @@ -908,6 +909,7 @@ INST_MAN_SERIAL_PAGES = \ dummy-ups.$(MAN_SECTION_CMD_SYS) \ etapro.$(MAN_SECTION_CMD_SYS) \ everups.$(MAN_SECTION_CMD_SYS) \ + failover.$(MAN_SECTION_CMD_SYS) \ gamatronic.$(MAN_SECTION_CMD_SYS) \ genericups.$(MAN_SECTION_CMD_SYS) \ isbmex.$(MAN_SECTION_CMD_SYS) \ @@ -975,6 +977,7 @@ INST_HTML_SERIAL_MANS = \ dummy-ups.html \ etapro.html \ everups.html \ + failover.html \ gamatronic.html \ genericups.html \ isbmex.html \ diff --git a/docs/man/failover.txt b/docs/man/failover.txt new file mode 100644 index 0000000000..0f26bc9f2f --- /dev/null +++ b/docs/man/failover.txt @@ -0,0 +1,229 @@ +FAILOVER(8) +========== + +NAME +---- + +failover - UPS Failover Driver + +SYNOPSIS +-------- + +*failover* -h + +*failover* -a 'UPS_NAME' ['OPTIONS'] + +NOTE: This man page only documents the specific features of the failover driver. +For information about the core driver, see linkman:nutupsdrv[8]. + +DESCRIPTION +----------- + +The `failover` driver acts as a smart proxy for multiple "real" UPS drivers. It +connects to and monitors these underlying UPS drivers through their local UNIX +sockets (or Windows named pipes), continuously evaluating health and suitability +for "primary" duty according to a set of user configurable rules and priorities. + +At any given time, `failover` designates one UPS driver as the *primary*, and +presents its commands, variables and status to the outside world as if it were +directly talking to that UPS. From the perspective of the clients (such as +linkman:upsmon[8] or linkman:upsc[8]), the `failover` driver behaves like any +single UPS, abstracting away the underlying redundancy, and allowing for +seamless transitioning between all monitored UPS drivers and their datasets. + +The driver dynamically promotes or demotes the primary UPS driver based on: + +- Socket availability and communication status +- Data freshness and UPS online/offline indicators +- User-defined status filters (e.g., presence or absence of `OL`, `LB`, ...) +- Administrative override via control commands (`force.primary`, `force.ignore`) + +If the current primary becomes unavailable or no longer meets the criteria, the +driver automatically fails over to a more suitable driver. During transitions, +it ensures that any data is switched out instantly, without the linkman:upsd[8] +considering it as stale or the clients acting on any previously degraded status. + +When no suitable primary is available, a configurable fallback state is entered: + +- Keep last primary and declare the data as stale +- Raise `ALARM` and declare the data as stale +- Raise `ALARM` and set forced shutdown (`FSD`) + +How the UPS are connected (be it corded, networked, ...) to the machine does not +matter, `failover` is also not reliant on linkman:upsd[8] itself running. In +principle, it could even be used on multiple drivers connected to the same UPS, +but do note that any missing data would not be multiplexed between the drivers. + +In summary, `failover` simplifies multi UPS driver setups by consolidating +monitoring and control into a single NUT-visible "device", reducing complexity +and ensuring seamless transitions in high-availability environments. + +EXTRA ARGUMENTS +--------------- + +This driver supports the following settings: + +*port*='drivername-devicename,drivername2-devicename2,...':: +Required. Specifies the local socket names (or Windows named pipes) of the +underlying UPS drivers to be tracked. Entries must follow the format +`drivername-devicename`, as used by NUT's internal socket naming convention +(e.g. `usbhid-ups-ups1`). Multiple entries are comma-separated with no spaces. + +*inittime*='seconds':: +Optional. Sets a grace period after driver startup during which the absence of a +primary UPS is tolerated. This allows time for underlying drivers to initialize. +Defaults to 30 seconds. + +*deadtime*='seconds':: +Optional. Sets a grace period in seconds after which a non-responsive UPS driver +is considered dead. Defaults to 30 seconds. + +*relogtime*='seconds':: +Optional. Time interval in which repeated connection failure logs are emitted +for a UPS, reducing log spam during unstable conditions. Defaults to 5 seconds. + +*noprimarytime*='seconds':: +Optional. Duration to wait without a suitable primary UPS driver before entering +the configured fallback mode (`fsdmode`). Defaults to 15 seconds. + +*maxconnfails*='count':: +Optional. Number of consecutive connection failures allowed per UPS driver +before entering into the cooldown period (`coolofftime`). Defaults to 5. + +*coolofftime*='seconds':: +Optional. Cooldown period during which the driver pauses reconnect attempts +after exceeding `maxconnfails`. Defaults to 15 seconds. + +*fsdmode*='0|1|2':: +Optional. Defines the behavior when no suitable primary UPS driver is found +after `noprimarytime` has elapsed. Defaults to 0. + +- `0`: *Do not demote the last primary, but mark its data stale.* This is +similar to how a regular UPS driver would behave when it loses its connection to +the target UPS device. linkman:upsmon[8] will act on the last known (online or +not) status, and decide itself whether that UPS should be considered critical. + +- `1`: *Demote the primary, raise `ALARM` and mark the data stale after an +additional few seconds have elapsed (ensuring full propagation).* This will +force monitoring linkman:upsmon[8] to see a previously in an alarm state device +having lost its connection and consider the UPS driver critical, possibly +resulting in forced shutdown (`FSD`) by depletion of `MINSUPPLIES`. + +- `2`: *Demote the primary, raise `ALARM` and set immediate `FSD`.* This will +set `FSD` from the driver side and omit linkman:upsmon[8] to raise it itself. +This mode is for setups where immediate shutdown is warranted, regardless of +anything else, and getting `FSD` out to the clients as fast as just possible. + +*strictfiltering*='0|1':: Optional. If set to 1, only UPS matching the +configured status filters are considered for promotion to primary. If set to 0, +the hard-coded default logic is also considered when no status filters match +(read more about this further down). Defaults to 0. + +*status_have_any*='OL,CHRG,...':: +Optional. If any of these comma-separated tokens are present in a UPS driver's +`ups.status`, it qualifies for promotion to primary. Defaults to unset. + +*status_have_all*='OL,CHRG,...':: +Optional. All listed comma-separated tokens must be present in `ups.status` for +the UPS driver to be eligible for promotion to primary. Defaults to unset. + +*status_nothave_any*='OB,OFF,...':: +Optional. If any of these comma-separated tokens are present in `ups.status`, +the UPS driver is disqualified as a primary candidate. Defaults to unset. + +*status_nothave_all*='OB,LB,...':: +Optional. If all of these comma-separated tokens are present in `ups.status`, +the UPS driver is disqualified as a primary candidate. Defaults to unset. + +IMPLEMENTATION +-------------- + +The port argument in the linkman:ups.conf[5] should reference the local driver +sockets (or Windows named pipes) that the "real" UPS drivers are using. A most +basic defaults setup with multiple drivers could look like this: + +------ + [realups] + driver = usbhid-ups + port = auto + + [realups2] + driver = usbhid-ups + port = auto + + [failover] + driver = failover + port = usbhid-ups-realups,usbhid-ups-realups2 +------ + +Any linkman:upsmon[8] clients would be set to monitor the `failover` UPS. + +The driver fully supports setting variables and performing instant commands on +the currently elected primary UPS driver, which are proxied and with end-to-end +tracking also being possible (linkman:upscmd[1] and linkman:upsrw[1] `-w`). You +may notice some variables and commands will be prefixed with `upstream.`, this +is to clearly separate the upstream commands from those of `failover` itself. + +For your convenience, additional administrative commands are exposed to directly +influence and override the primary election process, e.g. for maintenance: + +- `.force.ignore [seconds]` will prevent that UPS driver from ever +becoming primary within the given timeframe, or permanently in case of a +negative value. A value of 0 resets the override state back to disabled. + +- `.force.primary [seconds]` will force that UPS driver to the +highest priority within the given timeframe, or permanently in case of a +negative value. A value of 0 resets the override state back to disabled. + +If either command is executed without any argument, active overrides for that +UPS driver will be reset and returned to their default state of being disabled. + +PRIORITIES +---------- + +As outlined above, primaries are dynamically elected based on their current +state and according to a strict set of user influenceable priorities, which are: + +- `0` (highest): UPS driver was forced to the top by administrative command. +- `1`: UPS driver has passed the user-defined status filters. +- `2`: UPS driver has fresh data and is online (in status `OL`). +- `3`: UPS driver has fresh data, but may not be fully online. +- `4` (lowest): UPS driver is alive, but may not have fresh data. + +The UPS driver with the highest calculated priority is chosen as primary, ties +are resolved through order of the socket names given within the `port` argument. + +For the user-defined status filters, the following internal order is respected: + +1. `status_nothave_any` (first) +2. `status_have_all` +3. `status_nothave_all` +4. `status_have_any` (last) + +If `strictfiltering` is enabled, priorities 2 to 4 are not applicable. + +If no user-defined status filters are set, the priority 1 is not applicable. + +NOTE: The base requirement for any election is the UPS socket being connectable +and the UPS driver having published at least one full batch of data during its +lifetime. UPS driver not fulfilling that requirement are always disqualified. + +AUTHOR +------ + +Sebastian Kuttnig + +SEE ALSO +-------- + +linkman:upscmd[1], +linkman:upsrw[1], +linkman:ups.conf[5], +linkman:upsc[8], +linkman:upsmon[8], +linkman:nutupsdrv[8] + +Internet Resources: +~~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ diff --git a/docs/nut.dict b/docs/nut.dict index 73c1619e0b..a98ac1b2c5 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3495 utf-8 +personal_ws-1.1 en 3509 utf-8 AAC AAS ABI @@ -371,6 +371,7 @@ Exar ExecCGI ExecStart ExecStartPre +FAILOVER FBCA FD FDE @@ -1807,6 +1808,8 @@ configureaza confpath const contrib +cooldown +coolofftime copyrightable coreutils cout @@ -1876,6 +1879,7 @@ ddk ddl de deUNV +deadtime debian debootstrap debouncing @@ -2035,6 +2039,7 @@ faa fabula facto failmode +failover fallthrough fasttrack fatalx @@ -2076,6 +2081,7 @@ frob frontends fs fsd +fsdmode fsr fstab ftdi @@ -2227,6 +2233,7 @@ includePath includedir inductor inet +influenceable infos infoval inh @@ -2237,6 +2244,7 @@ initializer initializers initinfo initscripts +inittime initups inline inlined @@ -2478,6 +2486,7 @@ manpage manpages masterguard matcher +maxconnfails maxd maxlength maxreport @@ -2632,12 +2641,14 @@ nombattvolt noncommercially noout nooutstats +noprimarytime norating noro noscanlangid nosnap nosuid notAfter +nothave notifyflags notifyme notifymsg @@ -2826,6 +2837,7 @@ proc productid prog progname +proxied prtconf ps psu @@ -2887,6 +2899,7 @@ regtype relatime releasekeyring relicensing +relogtime remoteip renderer renderers @@ -3102,6 +3115,7 @@ strcpy strdup strerror strftime +strictfiltering stringify strlen strncpy diff --git a/drivers/Makefile.am b/drivers/Makefile.am index b02cfd049f..1610501f61 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -62,7 +62,7 @@ endif HAVE_LIBREGEX # in top level. NUTSW_DRIVERLIST_DUMMY_UPS = dummy-ups$(EXEEXT) NUTSW_DRIVERLIST = $(NUTSW_DRIVERLIST_DUMMY_UPS) \ - clone clone-outlet apcupsd-ups skel + clone clone-outlet failover apcupsd-ups skel SERIAL_DRIVERLIST = al175 bcmxcp belkin belkinunv bestfcom \ bestfortress bestuferrups bestups etapro everups \ gamatronic genericups isbmex liebert liebert-esp2 liebert-gxe masterguard metasys \ @@ -226,6 +226,9 @@ endif WITH_SSL clone_SOURCES = clone.c clone_outlet_SOURCES = clone-outlet.c +# failover driver (in NUTSW_DRIVERLIST) +failover_SOURCES = failover.c + # apcupsd client driver (in NUTSW_DRIVERLIST) apcupsd_ups_SOURCES = apcupsd-ups.c apcupsd_ups_CFLAGS = $(AM_CFLAGS) diff --git a/drivers/failover.c b/drivers/failover.c new file mode 100644 index 0000000000..83ccf8b003 --- /dev/null +++ b/drivers/failover.c @@ -0,0 +1,2382 @@ +/* failover.c - UPS Failover Driver + + Copyright (C) + 2025 - Sebastian Kuttnig + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#include "config.h" +#include "main.h" +#include "nut_stdint.h" +#include "parseconf.h" +#include "timehead.h" +#include "upsdrvquery.h" + +#define DRIVER_NAME "UPS Failover Driver" +#define DRIVER_VERSION "0.01" + +#define VAR_ALLOC_BATCH 50 +#define SUBVAR_ALLOC_BATCH 10 +#define CMD_ALLOC_BATCH 20 +#define CONN_READ_TIMEOUT 3 +#define CONN_CMD_TIMEOUT 3 +#define ALARM_PROPAG_TIME 15 + +#define DEFAULT_INIT_TIMEOUT 30 +#define DEFAULT_DEAD_TIMEOUT 30 +#define DEFAULT_CONNECTION_COOLOFF 15 +#define DEFAULT_NO_PRIMARY_TIMEOUT 15 +#define DEFAULT_MAX_CONNECT_FAILS 5 +#define DEFAULT_RELOG_TIMEOUT 5 +#define DEFAULT_FSD_MODE 0 +#define DEFAULT_STRICT_FILTERING 0 + +#define PRIORITY_SKIPPED -1 +#define PRIORITY_FORCED 0 +#define PRIORITY_USERFILTERS 1 +#define PRIORITY_GOOD 2 +#define PRIORITY_WEAK 3 +#define PRIORITY_LASTRESORT 4 + +upsdrv_info_t upsdrv_info = { + DRIVER_NAME, + DRIVER_VERSION, + "Sebastian Kuttnig ", + DRV_EXPERIMENTAL, + { NULL } +}; + +typedef enum { + UPS_FLAG_NONE = 0, + UPS_FLAG_ALIVE = 1 << 0, + UPS_FLAG_DUMPED = 1 << 1, + UPS_FLAG_DATA_OK = 1 << 2, + UPS_FLAG_ONLINE = 1 << 3, + UPS_FLAG_PRIMARY = 1 << 4 +} ups_flags_t; + +typedef struct { + int min; + int max; +} var_range_t; + +typedef struct { + char *key; + char *value; + + char **enum_list; + var_range_t **range_list; + + size_t enum_count; + size_t enum_allocs; + size_t range_count; + size_t range_allocs; + + long aux; + + int flags; + int needs_export; +} ups_var_t; + +typedef struct { + char *value; + int needs_export; +} ups_cmd_t; + +typedef struct { + char **have_any; + size_t have_any_count; + + char **have_all; + size_t have_all_count; + + char **nothave_any; + size_t nothave_any_count; + + char **nothave_all; + size_t nothave_all_count; +} status_filters_t; + +typedef struct { + char *name; + char *drivername; + char *socketname; + + udq_pipe_conn_t *conn; + PCONF_CTX_t parse_ctx; + + ups_var_t **var_list; + ups_cmd_t **cmd_list; + + size_t var_count; + size_t var_allocs; + size_t cmd_count; + size_t cmd_allocs; + + char *status; + + time_t last_heard_time; + time_t last_pinged_time; + time_t last_failure_time; + time_t force_ignore_time; + time_t force_primary_time; + + ups_flags_t flags; + int priority; + int failure_count; + + int force_ignore; + int force_primary; + int force_dstate_export; +} ups_device_t; + +static status_filters_t arg_status_filters; + +static int arg_init_timeout = DEFAULT_INIT_TIMEOUT; +static int arg_dead_timeout = DEFAULT_DEAD_TIMEOUT; +static int arg_relog_timeout = DEFAULT_RELOG_TIMEOUT; +static int arg_noprimary_timeout = DEFAULT_NO_PRIMARY_TIMEOUT; +static int arg_maxconnfails = DEFAULT_MAX_CONNECT_FAILS; +static int arg_coolofftimeout = DEFAULT_CONNECTION_COOLOFF; +static int arg_fsdmode = DEFAULT_FSD_MODE; +static int arg_strict_filtering = DEFAULT_STRICT_FILTERING; + +static int init_time_elapsed; +static int primaries_gone; + +static time_t drv_startup_time; +static time_t primaries_gone_time; + +static ups_device_t **ups_list; +static ups_device_t *primary_ups; +static ups_device_t *last_primary_ups; + +static size_t ups_count; +static size_t ups_alive_count; +static size_t ups_online_count; +static size_t ups_primary_count; + +static int instcmd(const char *cmdname, const char *extra); +static int setvar(const char *varname, const char *val); + +static void handle_arguments(void); +static void parse_port_argument(void); +static void parse_status_filters(void); +static void handle_connections(void); +static void export_driver_state(void); + +static void handle_no_primaries(void); +static int handle_init_time(const ups_device_t *primary_candidate); + +static int ups_connect(ups_device_t *ups); +static int ups_read_data(ups_device_t *ups); +static void ups_disconnect(ups_device_t *ups); +static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg); + +static int is_ups_alive(ups_device_t *ups); +static void ups_is_alive(ups_device_t *ups); +static void ups_is_dead(ups_device_t *ups); +static void ups_is_online(ups_device_t *ups); +static void ups_is_offline(ups_device_t *ups); + +static ups_device_t *get_primary_candidate(); +static int ups_passes_status_filters(const ups_device_t *ups); +static void ups_promote_primary(ups_device_t *ups); +static void ups_demote_primary(ups_device_t *ups); +static void ups_export_dstate(ups_device_t *ups); +static void ups_clean_dstate(ups_device_t *ups); + +static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd); +static int ups_add_cmd(ups_device_t *ups, const char *val); +static int ups_del_cmd(ups_device_t *ups, const char *val); + +static int ups_get_var_pos(const ups_device_t *ups, const char *key); +static int ups_set_var(ups_device_t *ups, const char *key, const char *value); +static int ups_del_var(ups_device_t *ups, const char *key); +static int ups_set_var_flags(ups_device_t *ups, const char *key, const int flag); +static int ups_set_var_aux(ups_device_t *ups, const char *key, const long aux); +static int ups_add_range(ups_device_t *ups, const char *varkey, const int min, const int max); +static int ups_del_range(ups_device_t *ups, const char *varkey, const int min, const int max); +static int ups_add_enum(ups_device_t *ups, const char *varkey, const char *enumval); +static int ups_del_enum(ups_device_t *ups, const char *varkey, const char *enumval); + +static void free_status_filters(void); +static void ups_free_ups_state(ups_device_t *ups); +static void ups_free_var_state(ups_var_t *var); +static const char *rewrite_driver_prefix(const char *in, char *out, size_t outlen); +static int split_socket_name(const char *input, char **driver, char **ups); +static void csv_arg_to_array(const char *argname, const char *argcsv, char ***array, size_t *countvar); + +static inline void ups_set_flag(ups_device_t *ups, ups_flags_t flag); +static inline void ups_clear_flag(ups_device_t *ups, ups_flags_t flag); +static inline int ups_has_flag(const ups_device_t *ups, ups_flags_t flag); + +void upsdrv_initups(void) +{ + handle_arguments(); +} + +void upsdrv_initinfo(void) +{ + char buf[SMALLBUF]; + size_t i = 0; + int required = -1; + + for (i = 0; i < ups_count; ++i) { + ups_device_t *ups = ups_list[i]; + + ups_connect(ups); + + required = snprintf(buf, sizeof(buf), "%s.force.ignore", ups->socketname); + dstate_addcmd(buf); + + if ((size_t)required >= sizeof(buf)) { + upslogx(LOG_WARNING, "%s: truncated administrative command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, buf, (size_t)required, sizeof(buf)); + } + + required = snprintf(buf, sizeof(buf), "%s.force.primary", ups->socketname); + dstate_addcmd(buf); + + if ((size_t)required >= sizeof(buf)) { + upslogx(LOG_WARNING, "%s: truncated administrative command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, buf, (size_t)required, sizeof(buf)); + } + } + + if (!ups_alive_count) { + fatalx(EXIT_FAILURE, "%s: %s: none of the tracked UPS drivers were connectable", + progname, __func__); + } + + status_init(); + status_set("WAIT"); + status_commit(); + + time(&drv_startup_time); + + upsh.instcmd = instcmd; + upsh.setvar = setvar; + + dstate_dataok(); +} + +void upsdrv_updateinfo(void) +{ + ups_device_t *primary_candidate = NULL; + + handle_connections(); + + primary_candidate = get_primary_candidate(); + + export_driver_state(); + + if (handle_init_time(primary_candidate)) { + return; + } + + if (!primary_candidate) { + handle_no_primaries(); + + return; + } + + if (primaries_gone) { + if (primary_candidate == primary_ups) { + /* Special handling for fsdmode 0 where primary was never demoted */ + upslogx(LOG_NOTICE, "%s: [%s] was declared to be a suitable primary (again)", + __func__, primary_candidate->socketname); + primary_candidate->force_dstate_export = 1; + } + primaries_gone = 0; + primaries_gone_time = 0; + } + + if (primary_ups != primary_candidate) { + ups_promote_primary(primary_candidate); + } else { + ups_export_dstate(primary_ups); + } + + if(!ups_has_flag(primary_candidate, UPS_FLAG_DATA_OK)) { + dstate_datastale(); + + return; + } + + dstate_dataok(); +} + +void upsdrv_shutdown(void) +{ + +} + +void upsdrv_help(void) +{ + +} + +void upsdrv_makevartable(void) +{ + char buf[SMALLBUF]; + + snprintf(buf, sizeof(buf), + "Grace period in seconds during which no primaries found are " + "acceptable (for driver startup) (default: %d)", + arg_init_timeout); + addvar(VAR_VALUE, "inittime", buf); + + snprintf(buf, sizeof(buf), + "Grace period in seconds after which a non-responsive UPS " + "driver is considered dead (default: %d)", + arg_dead_timeout); + addvar(VAR_VALUE, "deadtime", buf); + + snprintf(buf, sizeof(buf), + "Grace period in seconds until connection failures are logged " + "again (to reduce spamming logs) (default: %d)", + arg_relog_timeout); + addvar(VAR_VALUE, "relogtime", buf); + + snprintf(buf, sizeof(buf), + "Grace period in seconds until 'fsdmode' is entered into after " + "not finding any primaries (default: %d)", + arg_noprimary_timeout); + addvar(VAR_VALUE, "noprimarytime", buf); + + snprintf(buf, sizeof(buf), + "Maximum amount of failures connecting to a driver until " + "'coolofftime' is entered into (default: %d)", + arg_maxconnfails); + addvar(VAR_VALUE, "maxconnfails", buf); + + snprintf(buf, sizeof(buf), + "Period in seconds during which driver connections are not " + "retried after exceeding 'maxconnfails' (default: %d)", + arg_coolofftimeout); + addvar(VAR_VALUE, "coolofftime", buf); + + snprintf(buf, sizeof(buf), + "Sets no primary behavior (0: last primary data + stale, 1: no " + "data + alarm + stale, 2: no data + fsd + alarm) (default: %d)", + arg_fsdmode); + addvar(VAR_VALUE, "fsdmode", buf); + + snprintf(buf, sizeof(buf), + "Sets if only the given status filters should be considered for " + "UPS driver to be electable as primary (default: %d)", + arg_strict_filtering); + addvar(VAR_VALUE, "strictfiltering", buf); + + addvar(VAR_VALUE, "status_have_any", + "Comma separated list of status tokens, any present qualifies " + "the UPS driver for primary (default: unset)"); + addvar(VAR_VALUE, "status_have_all", + "Comma separated list of status tokens, only all present " + "qualifies the UPS driver for primary (default: unset)"); + addvar(VAR_VALUE, "status_nothave_any", + "Comma separated list of status tokens, any present disqualifies " + "the UPS driver for primary (default: unset)"); + addvar(VAR_VALUE, "status_nothave_all", + "Comma separated list of status tokens, only all present " + "disqualifies the UPS driver for primary (default: unset)"); +} + +void upsdrv_cleanup(void) +{ + size_t i = 0; + + for (i = 0; i < ups_count; ++i) { + ups_device_t *ups = ups_list[i]; + + if (primary_ups == ups) { + primary_ups = NULL; + } + + if (last_primary_ups == ups) { + last_primary_ups = NULL; + } + + ups_disconnect(ups); /* free conn + ctx */ + + ups_free_ups_state(ups); /* free status, vars, subvars + cmds */ + + if (ups->name) { + free(ups->name); + ups->name = NULL; + } + + if (ups->drivername) { + free(ups->drivername); + ups->drivername = NULL; + } + + if (ups->socketname) { + free(ups->socketname); + ups->socketname = NULL; + } + + free(ups); + ups_list[i] = NULL; + } + + free(ups_list); + ups_list = NULL; + + free_status_filters(); /* free status filters */ +} + +static int instcmd(const char *cmdname, const char *extra) +{ + size_t i = 0; + + upsdebug_INSTCMD_STARTING(cmdname, extra); + + for (i = 0; i < ups_count; ++i) { + ups_device_t *ups = ups_list[i]; + size_t len = strlen(ups->socketname); + + if (!strncmp(cmdname, ups->socketname, len)) { + const char *subcmd = cmdname + len; + + if (!strcmp(subcmd, ".force.ignore")) { + time_t now; + + time(&now); + + ups->force_ignore = extra ? atoi(extra) : 0; + ups->force_ignore_time = ups->force_ignore ? now : 0; + + upslogx(LOG_NOTICE, "%s: set [force_ignore] to [%d] on [%s]", + __func__, ups->force_ignore, ups->socketname); + + return STAT_INSTCMD_HANDLED; + } + + if (!strcmp(subcmd, ".force.primary")) { + time_t now; + + time(&now); + + ups->force_primary = extra ? atoi(extra) : 0; + ups->force_primary_time = ups->force_primary ? now : 0; + + upslogx(LOG_NOTICE, "%s: set [force_primary] to [%d] on [%s]", + __func__, ups->force_primary, ups->socketname); + + return STAT_INSTCMD_HANDLED; + } + } + } + + if (!primary_ups) { + upslogx(LOG_INSTCMD_FAILED, "%s: received [%s] [%s], but" + "there is currently no elected primary able to handle it", + __func__, cmdname, extra ? extra : ""); + + return STAT_INSTCMD_FAILED; + } + + if(ups_get_cmd_pos(primary_ups, cmdname) >= 0) { + const char *cmd = NULL; + char msgbuf[SMALLBUF]; + struct timeval tv; + ssize_t cmdret = -1; + int required = -1; + + if (!strncmp(cmdname, "upstream.", 9)) { + cmd = cmdname + 9; + upsdebugx(3, "%s: rewriting from [%s] to [%s] for upstream driver", + __func__, cmdname, cmd); + } else { + cmd = cmdname; + } + + if (extra) { + required = snprintf(msgbuf, sizeof(msgbuf), "INSTCMD %s %s\n", cmd, extra); + } else { + required = snprintf(msgbuf, sizeof(msgbuf), "INSTCMD %s\n", cmd); + } + + if ((size_t)required >= sizeof(msgbuf)) { + upslogx(LOG_WARNING, "%s: truncated INSTCMD command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, msgbuf, (size_t)required, sizeof(msgbuf)); + } + + tv.tv_sec = CONN_CMD_TIMEOUT; + tv.tv_usec = 0; + + cmdret = upsdrvquery_oneshot(primary_ups->drivername, primary_ups->name, + msgbuf, NULL, 0, &tv); + + if (cmdret >= 0) { + upslogx(LOG_NOTICE, "%s: sent [%s] [%s], " + "received response code: [%" PRIiSIZE "]", + __func__, cmdname, extra ? extra : "", cmdret); + + return cmdret; + } else { + upslog_with_errno(LOG_INSTCMD_FAILED, "%s: sent [%s] [%s], " + "received no response code due to socket failure", + __func__, cmdname, extra ? extra : ""); + + return STAT_INSTCMD_FAILED; + } + } + + upslogx(LOG_INSTCMD_UNKNOWN, "%s: received [%s] [%s], " + "but it is not among the primary's supported commands", + __func__, cmdname, extra ? extra : ""); + + return STAT_INSTCMD_UNKNOWN; +} + +static int setvar(const char *varname, const char *val) +{ + upsdebug_SET_STARTING(varname, val); + + if (!primary_ups) { + upslogx(LOG_SET_FAILED, "%s: received [%s] [%s], but " + "there is currently no elected primary able to handle it", + __func__, varname, val); + + return STAT_SET_FAILED; + } + + if(ups_get_var_pos(primary_ups, varname) >= 0) { + const char *var = NULL; + char msgbuf[SMALLBUF]; + struct timeval tv; + ssize_t cmdret = -1; + int required = -1; + + if (!strncmp(varname, "upstream.", 9)) { + var = varname + 9; + upsdebugx(3, "%s: rewriting from [%s] to [%s] for upstream driver", + __func__, varname, var); + } else { + var = varname; + } + + required = snprintf(msgbuf, sizeof(msgbuf), "SET %s \"%s\"\n", var, val); + + if ((size_t)required >= sizeof(msgbuf)) { + upslogx(LOG_WARNING, "%s: truncated SET command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, msgbuf, (size_t)required, sizeof(msgbuf)); + } + + tv.tv_sec = CONN_CMD_TIMEOUT; + tv.tv_usec = 0; + + cmdret = upsdrvquery_oneshot(primary_ups->drivername, primary_ups->name, + msgbuf, NULL, 0, &tv); + + if (cmdret >= 0) { + upslogx(LOG_NOTICE, "%s: sent [%s] [%s], " + "received response code: [%" PRIiSIZE "]", + __func__, varname, val, cmdret); + + return cmdret; + } else { + upslog_with_errno(LOG_SET_FAILED, "%s: sent [%s] [%s], " + "received no response code due to socket failure", + __func__, varname, val); + + return STAT_SET_FAILED; + } + } + + upslogx(LOG_SET_UNKNOWN, "%s: received [%s] [%s], " + "but it is not among the primary's supported variables", + __func__, varname, val); + + return STAT_SET_UNKNOWN; +} + +static void handle_arguments(void) +{ + const char *val = NULL; + + parse_port_argument(); + parse_status_filters(); + + val = getval("inittime"); + if (val) { + arg_init_timeout = atoi(val); + upsdebugx(1, "%s: set 'inittime' to [%d] from configuration", + __func__, arg_init_timeout); + } + + val = getval("deadtime"); + if (val) { + arg_dead_timeout = atoi(val); + upsdebugx(1, "%s: set 'deadtime' to [%d] from configuration", + __func__, arg_dead_timeout); + } + + val = getval("relogtime"); + if (val) { + arg_relog_timeout = atoi(val); + upsdebugx(1, "%s: set 'relogtime' to [%d] from configuration", + __func__, arg_relog_timeout); + } + + val = getval("noprimarytime"); + if (val) { + arg_noprimary_timeout = atoi(val); + upsdebugx(1, "%s: set 'noprimarytime' to [%d] from configuration", + __func__, arg_noprimary_timeout); + } + + val = getval("maxconnfails"); + if (val) { + arg_maxconnfails = atoi(val); + upsdebugx(1, "%s: set 'maxconnfails' to [%d] from configuration", + __func__, arg_maxconnfails); + } + + val = getval("coolofftime"); + if (val) { + arg_coolofftimeout = atoi(val); + upsdebugx(1, "%s: set 'coolofftime' to [%d] from configuration", + __func__, arg_coolofftimeout); + } + + val = getval("fsdmode"); + if (val) { + arg_fsdmode = atoi(val); + if (arg_fsdmode >= 0 && arg_fsdmode <= 2) { + upsdebugx(1, "%s: set 'fsdmode' to [%d] from configuration", + __func__, arg_fsdmode); + } else { + upslogx(LOG_ERR, "%s: invalid 'fsdmode' of [%d] from configuration, " + "set to the default 'fsdmode' value of [%d] instead", + __func__, arg_fsdmode, DEFAULT_FSD_MODE); + arg_fsdmode = DEFAULT_FSD_MODE; + } + } + + val = getval("strictfiltering"); + if (val) { + arg_strict_filtering = atoi(val); + upsdebugx(1, "%s: set 'strictfiltering' to [%d] from configuration", + __func__, arg_strict_filtering); + } +} + +static void parse_port_argument(void) +{ + char *tmp = NULL; + char *token = NULL; + const char *str = device_path; + + tmp = xstrdup(str); + + token = strtok(tmp, ","); + while (token) { + ups_device_t *new_ups = NULL; + + str_trim_space(token); + + new_ups = xcalloc(1, sizeof(**ups_list)); + new_ups->socketname = xstrdup(token); + + if (!split_socket_name(new_ups->socketname, &new_ups->drivername, &new_ups->name)) { + free(new_ups->socketname); + free(new_ups); + fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument has an invalid format, " + "[%s] is not a valid splittable socket name, please correct the argument", + progname, __func__, token); + } else { + upsdebugx(3, "%s: [%s] was parsed into UPS driver [%s] and UPS [%s]", + __func__, new_ups->socketname, new_ups->drivername, new_ups->name); + } + + ups_list = xrealloc(ups_list, sizeof(*ups_list) * (ups_count + 1)); + ups_list[ups_count] = new_ups; + ups_count++; + + upsdebugx(1, "%s: [%s]: was added to the list of tracked UPS drivers", + __func__, new_ups->socketname); + + token = strtok(NULL, ","); + } + + free(tmp); +} + +static void parse_status_filters(void) +{ + csv_arg_to_array("status_have_any", + getval("status_have_any"), + &arg_status_filters.have_any, + &arg_status_filters.have_any_count); + + csv_arg_to_array("status_have_all", + getval("status_have_all"), + &arg_status_filters.have_all, + &arg_status_filters.have_all_count); + + csv_arg_to_array("status_nothave_any", + getval("status_nothave_any"), + &arg_status_filters.nothave_any, + &arg_status_filters.nothave_any_count); + + csv_arg_to_array("status_nothave_all", + getval("status_nothave_all"), + &arg_status_filters.nothave_all, + &arg_status_filters.nothave_all_count); +} + +static void handle_connections(void) +{ + size_t i = 0; + + for (i = 0; i < ups_count; ++i) { + ups_device_t *ups = ups_list[i]; + + if (!ups_has_flag(ups, UPS_FLAG_ALIVE) && !ups_connect(ups)) { + /* Reconnecting a dead UPS has failed... skip it */ + + continue; + } + else if (!is_ups_alive(ups)) { + /* UPS is dead long enough... disconnect it */ + upslogx(LOG_WARNING, "%s: [%s]: connection to UPS driver was lost (declared dead)", + __func__, ups->socketname); + ups_disconnect(ups); + + continue; + } + + if (ups_read_data(ups) < 0 ) { + /* Socket failure... warrants immediate disconnect */ + upslog_with_errno(LOG_ERR, "%s: [%s]: connection to UPS driver was lost (socket failure)", + __func__, ups->socketname); + ups_disconnect(ups); + } + } +} + +static void export_driver_state(void) +{ + dstate_setinfo("driver.stats.alive_drivers", "%" PRIuSIZE, ups_alive_count); + dstate_setinfo("driver.stats.online_drivers", "%" PRIuSIZE, ups_online_count); + dstate_setinfo("driver.stats.primary_drivers", "%" PRIuSIZE, ups_primary_count); + dstate_setinfo("driver.stats.total_drivers", "%" PRIuSIZE, ups_count); + + if (primary_ups) { + dstate_setinfo("driver.primary.upsname", "%s", primary_ups->name); + dstate_setinfo("driver.primary.drvname", "%s", primary_ups->drivername); + dstate_setinfo("driver.primary.sockname", "%s", primary_ups->socketname); + dstate_setinfo("driver.primary.priority", "%d", primary_ups->priority); + dstate_setinfo("driver.primary.stats.cmds", "%" PRIuSIZE, primary_ups->cmd_count); + dstate_setinfo("driver.primary.stats.vars", "%" PRIuSIZE, primary_ups->var_count); + } else { + dstate_delinfo("driver.primary.upsname"); + dstate_delinfo("driver.primary.drvname"); + dstate_delinfo("driver.primary.sockname"); + dstate_delinfo("driver.primary.priority"); + dstate_delinfo("driver.primary.stats.cmds"); + dstate_delinfo("driver.primary.stats.vars"); + } + + upsdebugx(4, "%s: exported internal driver state to dstate", + __func__); +} + +static void handle_no_primaries(void) +{ + time_t now; + double elapsed; + + if (!primaries_gone_time) { + time(&primaries_gone_time); + } + + if (primary_ups && arg_fsdmode > 0) { + ups_demote_primary(primary_ups); + } + + time(&now); + + elapsed = difftime(now, primaries_gone_time); + + if (elapsed > arg_noprimary_timeout) { + if (!primaries_gone) { + upslogx(LOG_WARNING, "%s: none of the tracked UPS drivers are suitable primaries", + __func__); + } + + switch (arg_fsdmode) { + case 0: + if (!primaries_gone) { + upslogx(LOG_WARNING, "%s: 'fsdmode' is [0]: " + "keeping last primary and declaring data stale immediately", + __func__); + } + + dstate_datastale(); + break; + + case 1: + if (!primaries_gone) { + upslogx(LOG_WARNING, "%s: 'fsdmode' is [1]: " + "demoting last primary, raising alarm, and declaring data stale " + "after another %d seconds elapse to ensure full ALARM propagation", + __func__, ALARM_PROPAG_TIME); + } + + status_init(); + alarm_init(); + alarm_set("No suitable primaries for failover"); + alarm_commit(); + status_commit(); + + if (elapsed < arg_noprimary_timeout + ALARM_PROPAG_TIME) { + /* make sure ALARM propagates to all clients first... */ + dstate_dataok(); + } else { + /* ... and then eventually declare the data as stale */ + dstate_datastale(); + } + break; + + case 2: + if (!primaries_gone) { + upslogx(LOG_WARNING, "%s: 'fsdmode' is [2]: " + "demoting last primary, raising alarm, and setting FSD", + __func__); + } + + status_init(); + alarm_init(); + status_set("FSD"); + alarm_set("No suitable primaries for failover"); + alarm_commit(); + status_commit(); + + dstate_dataok(); + break; + } + primaries_gone = 1; + } else { + upslogx(LOG_WARNING, "%s: No suitable primaries, " + "waiting for one to emerge... (%.0fs of %ds max)", + __func__, elapsed, arg_noprimary_timeout); + + dstate_dataok(); + } +} + +static int handle_init_time(const ups_device_t *primary_candidate) +{ + if (!init_time_elapsed) { + time_t now; + double elapsed; + + time(&now); + elapsed = difftime(now, drv_startup_time); + + if (!primary_candidate && elapsed <= arg_init_timeout) { + upslogx(LOG_NOTICE, "%s: still waiting for " + "first primary to emerge... (%.0fs of %ds max), if this was " + "too short for drivers to start, consider increasing 'inittime'", + __func__, elapsed, arg_init_timeout); + + dstate_dataok(); + + return 1; + } + + init_time_elapsed = 1; + } + + return 0; +} + +static int ups_connect(ups_device_t *ups) +{ + time_t now; + double elapsed; + int ret = 0; + int report_failure = 1; + udq_pipe_conn_t *conn = NULL; + + time(&now); + + elapsed = difftime(now, ups->last_failure_time); + + if (ups->failure_count > arg_maxconnfails && elapsed <= arg_coolofftimeout) { + upsdebugx(4, "%s: [%s]: not retrying in cooloff phase (%.0fs < %ds max)", + __func__, ups->socketname, elapsed, arg_coolofftimeout); + + return 0; + } + + if (nut_debug_level < 1 && elapsed <= arg_relog_timeout) { + report_failure = 0; + nut_upsdrvquery_debug_level = 0; + } else { + report_failure = 1; + nut_upsdrvquery_debug_level = NUT_UPSDRVQUERY_DEBUG_LEVEL_DEFAULT; + } + + conn = upsdrvquery_connect(ups->socketname); + + if (conn) { + pconf_init(&ups->parse_ctx, NULL); + ups->conn = conn; + + upslogx(LOG_NOTICE, "%s: [%s]: connection is now established", + __func__, ups->socketname); + + if (upsdrvquery_write(ups->conn, "DUMPALL\n") >= 0) { + ups_free_ups_state(ups); /* free any previous state */ + ups->force_dstate_export = 1; + + ups_is_alive(ups); + time(&ups->last_heard_time); + + ups->failure_count = 0; + ups->last_failure_time = 0; + + upsdebugx(2, "%s: [%s]: requested first batch of data (DUMPALL)", + __func__, ups->socketname); + + ret = 1; + } else { + if (report_failure) { + upslog_with_errno(LOG_ERR, "%s: [%s]: communication failed " + "sending DUMPALL, disconnecting and re-trying it later", + __func__, ups->socketname); + } + ups_disconnect(ups); + + ups->failure_count++; + ups->last_failure_time = now; + + ret = 0; + } + } else { + if (report_failure) { + upslog_with_errno(LOG_ERR, "%s: [%s]: failed to establish connection", + __func__, ups->socketname); + } + + ups->failure_count++; + ups->last_failure_time = now; + + ret = 0; + } + + nut_upsdrvquery_debug_level = NUT_UPSDRVQUERY_DEBUG_LEVEL_DEFAULT; + + return ret; +} + +static int ups_read_data(ups_device_t *ups) +{ + int i = 0; + ssize_t ret; + struct timeval tv; + + tv.tv_sec = CONN_READ_TIMEOUT; + tv.tv_usec = 0; + + ret = upsdrvquery_read_timeout(ups->conn, tv); + + if (ret == -1) { + upsdebug_with_errno(2, "%s: [%s]: read from UPS driver has failed", + __func__, ups->socketname); + + return ret; + } + + if (ret == -2) { + upsdebug_with_errno(2, "%s: [%s]: read from UPS driver has timed out", + __func__, ups->socketname); + + return ret; + } + + for (i = 0; i < ret; ++i) { + switch (pconf_char(&ups->parse_ctx, ups->conn->buf[i])) + { + case 1: + if (ups_parse_protocol(ups, ups->parse_ctx.numargs, ups->parse_ctx.arglist)) { + time(&ups->last_heard_time); + } + continue; + + case 0: + continue; /* no complete line yet */ + + default: + upsdebug_with_errno(2, "%s: [%s]: parse error on read data: %s", + __func__, ups->socketname, ups->parse_ctx.errmsg); + + return -1; + } + } + + return ret; +} + +static void ups_disconnect(ups_device_t *ups) +{ + ups_is_dead(ups); + pconf_finish(&ups->parse_ctx); + + ups->flags = UPS_FLAG_NONE; + + if (ups->conn) { + upsdrvquery_close(ups->conn); + free(ups->conn); + ups->conn = NULL; + } + + upsdebugx(2, "%s: [%s]: connection was destroyed", + __func__, ups->socketname); +} + +static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) +{ + char buf[SMALLBUF]; + const char *varptr = NULL; + int required = -1; + + if (numargs < 1) { + goto skip_out; + } + + if (!strcasecmp(arg[0], "PONG")) { + upsdebugx(6, "%s: [%s]: got PONG from UPS driver", + __func__, ups->socketname); + + return 1; + } + + if (!strcasecmp(arg[0], "DUMPDONE")) { + upsdebugx(6, "%s: [%s]: got DUMPDONE from UPS driver", + __func__, ups->socketname); + + ups_set_flag(ups, UPS_FLAG_DUMPED); + + return 1; + } + + if (!strcasecmp(arg[0], "DATASTALE")) { + upsdebugx(6, "%s: [%s]: got DATASTALE from UPS driver", + __func__, ups->socketname); + + ups_clear_flag(ups, UPS_FLAG_DATA_OK); + + return 1; + } + + if (!strcasecmp(arg[0], "DATAOK")) { + upsdebugx(6, "%s: [%s]: got DATAOK from UPS driver", + __func__, ups->socketname); + + ups_set_flag(ups, UPS_FLAG_DATA_OK); + + return 1; + } + + if (numargs < 2) { + goto skip_out; + } + + /* DELCMD */ + if (!strcasecmp(arg[0], "DELCMD")) { + upsdebugx(6, "%s: [%s]: got DELCMD [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + required = snprintf(buf, sizeof(buf), "upstream.%s", arg[1]); + + if ((size_t)required >= sizeof(buf)) { + upslogx(LOG_WARNING, "%s: truncated DELCMD command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, buf, (size_t)required, sizeof(buf)); + } + + ups_del_cmd(ups, buf); + + return 1; + } + + /* ADDCMD */ + if (!strcasecmp(arg[0], "ADDCMD")) { + upsdebugx(6, "%s: [%s]: got ADDCMD [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + required = snprintf(buf, sizeof(buf), "upstream.%s", arg[1]); + + if ((size_t)required >= sizeof(buf)) { + upslogx(LOG_WARNING, "%s: truncated ADDCMD command [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, buf, (size_t)required, sizeof(buf)); + } + + ups_add_cmd(ups, buf); + + return 1; + } + + /* DELINFO */ + if (!strcasecmp(arg[0], "DELINFO")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got DELINFO [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_del_var(ups, varptr); + + return 1; + } + + if (numargs < 3) { + goto skip_out; + } + + /* SETFLAGS ... */ + if (!strcasecmp(arg[0], "SETFLAGS")) { + size_t i = 0; + int varflags = 0; + + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got SETFLAGS [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + for (i = 2; i < numargs; ++i) { + if (!strcasecmp(arg[i], "RW")) { + varflags |= ST_FLAG_RW; + } + else if (!strcasecmp(arg[i], "STRING")) { + varflags |= ST_FLAG_STRING; + } + else if (!strcasecmp(arg[i], "NUMBER")) { + varflags |= ST_FLAG_NUMBER; + } + else { + upsdebugx(6, "%s: [%s]: got unknown SETFLAGS [%s] from UPS driver", + __func__, ups->socketname, arg[i]); + } + } + + ups_set_var_flags(ups, varptr, varflags); + + return 1; + } + + /* SETAUX */ + if (!strcasecmp(arg[0], "SETAUX")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got SETAUX [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_set_var_aux(ups, varptr, atol(arg[2])); + + return 1; + } + + /* DELENUM */ + if (!strcasecmp(arg[0], "DELENUM")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got DELENUM [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_del_enum(ups, varptr, arg[2]); + + return 1; + } + + /* ADDENUM */ + if (!strcasecmp(arg[0], "ADDENUM")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got ADDENUM [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_add_enum(ups, varptr, arg[2]); + + return 1; + } + + /* SETINFO */ + if (!strcasecmp(arg[0], "SETINFO")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got SETINFO [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + if (!strcmp(arg[1], "ups.status")) { + if (ups->status) { + free(ups->status); + ups->status = NULL; + } + ups->status = xstrdup(arg[2]); + + if(str_contains_token(arg[2], "OL")) { + ups_is_online(ups); + } else { + ups_is_offline(ups); + } + } + + ups_set_var(ups, varptr, arg[2]); + + return 1; + } + + if (numargs < 4) { + goto skip_out; + } + + /* DELRANGE */ + if (!strcasecmp(arg[0], "DELRANGE")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got DELRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_del_range(ups, varptr, atoi(arg[2]), atoi(arg[3])); + + return 1; + } + + /* ADDRANGE */ + if (!strcasecmp(arg[0], "ADDRANGE")) { + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + + upsdebugx(6, "%s: [%s]: got ADDRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + + ups_add_range(ups, varptr, atoi(arg[2]), atoi(arg[3])); + + return 1; + } + +skip_out: + if (nut_debug_level > 0) { + char msgbuf[LARGEBUF]; + size_t i = 0; + int len = -1; + + memset(msgbuf, 0, sizeof(msgbuf)); + for (i = 0; i < numargs; ++i) { + len = snprintfcat(msgbuf, sizeof(msgbuf), "[%s] ", arg[i]); + } + if (len > 0) { + msgbuf[len - 1] = '\0'; + } + if ((size_t)len >= sizeof(msgbuf)) { + upsdebugx(6, "%s: truncated DBG output [%s]" + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, msgbuf, (size_t)len, sizeof(msgbuf)); + } + + upsdebugx(6, "%s: [%s]: ignored protocol line with %" PRIuSIZE " keyword(s): %s", + __func__, ups->socketname, numargs, numargs < 1 ? "" : msgbuf); + } + + return 0; +} + +static int is_ups_alive(ups_device_t *ups) +{ + time_t now; + double elapsed; + + if (!ups->conn || INVALID_FD(ups->conn->sockfd)) { + upsdebugx(2, "%s: [%s]: socket connection lost - declaring it dead", + __func__, ups->socketname); + + return 0; + } + + time(&now); + + elapsed = difftime(now, ups->last_heard_time); + + if ((elapsed > (arg_dead_timeout / 3)) && + (difftime(now, ups->last_pinged_time) > (arg_dead_timeout / 3))) { + + nut_upsdrvquery_debug_level = 0; + upsdrvquery_write(ups->conn, "PING\n"); + nut_upsdrvquery_debug_level = NUT_UPSDRVQUERY_DEBUG_LEVEL_DEFAULT; + + upsdebugx(3, "%s: [%s]: have not heard from driver, sent a PING to it", + __func__, ups->socketname); + + ups->last_pinged_time = now; + } + + if (elapsed > arg_dead_timeout) { + upsdebugx(2, "%s: [%s]: did not hear from driver " + "for at least %.0fs (of %ds max) - declaring it dead", + __func__, ups->socketname, elapsed, arg_dead_timeout); + + return 0; + } + + return 1; +} + +static void ups_is_alive(ups_device_t *ups) { + if (!ups_has_flag(ups, UPS_FLAG_ALIVE)) { + ups_set_flag(ups, UPS_FLAG_ALIVE); + ups_alive_count++; + upsdebugx(2, "%s: [%s]: is now alive (alive devices: %" PRIuSIZE ")", + __func__, ups->socketname, ups_alive_count); + } +} + +static void ups_is_dead(ups_device_t *ups) { + if (ups_has_flag(ups, UPS_FLAG_ALIVE)) { + ups_alive_count--; + upsdebugx(2, "%s: [%s]: is now dead " + "with (last known) status [%s] (alive devices: %" PRIuSIZE ")", + __func__, ups->socketname, ups->status, ups_alive_count); + } + + if (ups_has_flag(ups, UPS_FLAG_ONLINE)) { + ups_online_count--; + upsdebugx(3, "%s: [%s]: was online with (last known) " + "status [%s] and is now dead (online devices: %" PRIuSIZE ")", + __func__, ups->socketname, ups->status, ups_online_count); + } +} + +static void ups_is_online(ups_device_t *ups) { + if (!ups_has_flag(ups, UPS_FLAG_ONLINE)) { + ups_set_flag(ups, UPS_FLAG_ONLINE); + ups_online_count++; + upsdebugx(2, "%s: [%s]: is now online " + "with status [%s] (online devices: %" PRIuSIZE ")", + __func__, ups->socketname, ups->status, ups_online_count); + } +} + +static void ups_is_offline(ups_device_t *ups) { + if (ups_has_flag(ups, UPS_FLAG_ONLINE)) { + ups_clear_flag(ups, UPS_FLAG_ONLINE); + ups_online_count--; + upsdebugx(2, "%s: [%s]: is now offline " + "with status [%s] (online devices: %" PRIuSIZE ")", + __func__, ups->socketname, ups->status, ups_online_count); + } +} + +static ups_device_t *get_primary_candidate() +{ + time_t now; + size_t i = 0; + size_t primaries = 0; + int best_priority = 100; + ups_device_t *best_choice = NULL; + + time(&now); + + for (i = 0; i < ups_count; ++i) { + int priority = PRIORITY_SKIPPED; + ups_device_t *ups = ups_list[i]; + double elapsed_ignore = (ups->force_ignore > 0) ? difftime(now, ups->force_ignore_time) : 0; + double elapsed_force = (ups->force_primary > 0) ? difftime(now, ups->force_primary_time) : 0; + + if (ups->force_primary > 0 && + (ups->force_primary_time == 0 || elapsed_force > ups->force_primary)) { + ups->force_primary = 0; + ups->force_primary_time = 0; + } + else if (ups->force_primary == 0 && ups->force_primary_time > 0) { + ups->force_primary_time = 0; + } + + if (ups->force_ignore > 0 && + (ups->force_ignore_time == 0 || elapsed_ignore > ups->force_ignore)) { + ups->force_ignore = 0; + ups->force_ignore_time = 0; + } + else if (ups->force_ignore == 0 && ups->force_ignore_time > 0) { + ups->force_ignore_time = 0; + } + + if (ups_has_flag(ups, UPS_FLAG_ALIVE | UPS_FLAG_DUMPED)) { + if (ups->force_ignore < 0) { + ups->priority = PRIORITY_SKIPPED; + + upsdebugx(4, "%s: [%s]: is permanently ignored and was not considered", + __func__, ups->socketname); + + continue; + } + else if (ups->force_ignore > 0 && elapsed_ignore <= ups->force_ignore) { + ups->priority = PRIORITY_SKIPPED; + + upsdebugx(4, "%s: [%s]: is currently ignored and not considered (%.0fs of %ds)", + __func__, ups->socketname, elapsed_ignore, ups->force_ignore); + + continue; + } + else if (ups->force_primary < 0) { + priority = PRIORITY_FORCED; + + upsdebugx(4, "%s: [%s]: is permanently forced to highest priority", + __func__, ups->socketname); + } + else if (ups->force_primary > 0 && elapsed_force <= ups->force_primary) { + priority = PRIORITY_FORCED; + + upsdebugx(4, "%s: [%s]: is currently forced to highest priority (%.0fs of %ds)", + __func__, ups->socketname, elapsed_force, ups->force_primary); + } + else if (ups_passes_status_filters(ups)) { + priority = PRIORITY_USERFILTERS; + } + else if (arg_strict_filtering) { + upsdebugx(4, "%s: [%s]: 'strict_filtering' is enabled, considering " + "only status filters, but not the default set of lower priorities", + __func__, ups->socketname); + } + else if (ups_has_flag(ups, UPS_FLAG_DATA_OK | UPS_FLAG_ONLINE)) { + priority = PRIORITY_GOOD; + } + else if (ups_has_flag(ups, UPS_FLAG_DATA_OK)) { + priority = PRIORITY_WEAK; + } + else { + priority = PRIORITY_LASTRESORT; + } + } + + if (priority >= 0) { + primaries++; + + if (priority < best_priority) { + best_choice = ups; + best_priority = priority; + } + + upsdebugx(4, "%s: [%s]: is a primary candidate with priority [%d]", + __func__, ups->socketname, priority); + } + + ups->priority = priority; + } + + ups_primary_count = primaries; + + return best_choice; +} + +static int ups_passes_status_filters(const ups_device_t *ups) +{ + size_t i = 0; + const char *status = NULL; + + if (!*ups->status) { + return 0; + } + + status = ups->status; + + if (arg_status_filters.have_any_count == 0 && + arg_status_filters.have_all_count == 0 && + arg_status_filters.nothave_any_count == 0 && + arg_status_filters.nothave_all_count == 0) { + + upsdebugx(4, "%s: [%s]: no status filters are set, disregarding filtering", + __func__, ups->socketname); + + return 0; + } + + for (i = 0; i < arg_status_filters.nothave_any_count; ++i) { + if (str_contains_token(status, arg_status_filters.nothave_any[i])) { + upsdebugx(4, "%s: [%s]: nothave_any: [%s] was found, excluded", + __func__, ups->socketname, arg_status_filters.nothave_any[i]); + + return 0; + } + } + + for (i = 0; i < arg_status_filters.have_all_count; ++i) { + if (!str_contains_token(status, arg_status_filters.have_all[i])) { + upsdebugx(4, "%s: [%s]: have_all: [%s] not found, excluded", + __func__, ups->socketname, arg_status_filters.have_all[i]); + + return 0; + } + } + + if (arg_status_filters.nothave_all_count > 0) { + int all_found = 1; + for (i = 0; i < arg_status_filters.nothave_all_count; ++i) { + if (!str_contains_token(status, arg_status_filters.nothave_all[i])) { + all_found = 0; + break; + } + } + if (all_found) { + upsdebugx(4, "%s: [%s]: nothave_all: all were found, excluded", + __func__, ups->socketname); + + return 0; + } + } + + if (arg_status_filters.have_any_count > 0) { + int any_found = 0; + for (i = 0; i < arg_status_filters.have_any_count; ++i) { + if (str_contains_token(status, arg_status_filters.have_any[i])) { + any_found = 1; + break; + } + } + if (!any_found) { + upsdebugx(4, "%s: [%s]: have_any: none were found, excluded", + __func__, ups->socketname); + + return 0; + } + } + + return 1; +} + +static void ups_promote_primary(ups_device_t *ups) +{ + if (primary_ups) { + ups_demote_primary(primary_ups); + } + + primary_ups = ups; + primary_ups->force_dstate_export = 1; + + ups_set_flag(ups, UPS_FLAG_PRIMARY); + + upslogx(LOG_NOTICE, "%s: [%s]: was promoted " + "to primary with status [%s] and priority [%d]", + __func__, primary_ups->socketname, primary_ups->status, primary_ups->priority); + + ups_export_dstate(primary_ups); +} + +static void ups_demote_primary(ups_device_t *ups) +{ + last_primary_ups = ups; + primary_ups = NULL; + + ups_clear_flag(last_primary_ups, UPS_FLAG_PRIMARY); + + upslogx(LOG_NOTICE, "%s: [%s]: is no longer " + "primary with (last known) status [%s] and priority [%d]", + __func__, last_primary_ups->socketname, last_primary_ups->status, last_primary_ups->priority); + + ups_clean_dstate(last_primary_ups); +} + +static void ups_export_dstate(ups_device_t *ups) +{ + size_t i = 0; + + if (ups->force_dstate_export) { + status_init(); + alarm_init(); + } + + for (i = 0; i < ups->cmd_count; ++i) { + ups_cmd_t *cmd = ups->cmd_list[i]; + + if (cmd->needs_export || ups->force_dstate_export) { + dstate_addcmd(cmd->value); + + upsdebugx(5, "%s: [%s]: exported command to dstate: [%s]", + __func__, ups->socketname, cmd->value); + + cmd->needs_export = 0; + } + } + + for (i = 0; i < ups->var_count; ++i) { + ups_var_t *var = ups->var_list[i]; + + if (var->needs_export || ups->force_dstate_export) { + size_t j = 0; + + if (!strcmp(var->key, "ups.alarm")) { + alarm_init(); + alarm_set(var->value); + alarm_commit(); + status_commit(); /* publish ALARM */ + upsdebugx(5, "%s: [%s]: exported UPS alarm to dstate: [%s] : [%s]", + __func__, ups->socketname, var->key, var->value); + } + else if (!strcmp(var->key, "ups.status")) { + status_init(); + status_set(var->value); + status_commit(); + upsdebugx(5, "%s: [%s]: exported UPS status to dstate: [%s] : [%s]", + __func__, ups->socketname, var->key, var->value); + } + else { + dstate_setinfo(var->key, "%s", var->value); + upsdebugx(5, "%s: [%s]: exported variable to dstate: [%s] : [%s]", + __func__, ups->socketname, var->key, var->value); + } + + if (var->flags) { + dstate_setflags(var->key, var->flags); + upsdebugx(5, "%s: [%s]: exported variable flags to dstate: [%s] : [%d]", + __func__, ups->socketname, var->key, var->flags); + } + + if (var->aux) { + dstate_setaux(var->key, var->aux); + upsdebugx(5, "%s: [%s]: exported variable aux to dstate: [%s] : [%ld]", + __func__, ups->socketname, var->key, var->aux); + } + + for (j = 0; j < var->enum_count; ++j) { + dstate_addenum(var->key, "%s", var->enum_list[j]); + upsdebugx(5, "%s: [%s]: exported variable enum to dstate: [%s] : [%s]", + __func__, ups->socketname, var->key, var->enum_list[j]); + } + + for (j = 0; j < var->range_count; ++j) { + dstate_addrange(var->key, var->range_list[j]->min, var->range_list[j]->max); + upsdebugx(5, "%s: [%s]: exported variable range to dstate: [%s] : min=[%d] : max=[%d]", + __func__, ups->socketname, var->key, var->range_list[j]->min, var->range_list[j]->max); + } + + var->needs_export = 0; + } + } + + if (ups->force_dstate_export) { + alarm_commit(); + status_commit(); + } + + ups->force_dstate_export = 0; +} + +static void ups_clean_dstate(ups_device_t *ups) +{ + size_t i = 0; + + status_init(); + alarm_init(); + + for (i = 0; i < ups->cmd_count; ++i) { + dstate_delcmd(ups->cmd_list[i]->value); + upsdebugx(5, "%s: [%s]: removed command from dstate: [%s]", + __func__, ups->socketname, ups->cmd_list[i]->value); + } + + for (i = 0; i < ups->var_count; ++i) { + dstate_delinfo(ups->var_list[i]->key); + upsdebugx(5, "%s: [%s]: removed variable from dstate: [%s]", + __func__, ups->socketname, ups->var_list[i]->key); + } + + alarm_commit(); + status_commit(); +} + +static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd) +{ + size_t i = 0; + + for (i = 0; i < ups->cmd_count; ++i) { + if (!strcmp(ups->cmd_list[i]->value, cmd)) { + return i; + } + } + + return -1; +} + +static int ups_add_cmd(ups_device_t *ups, const char *val) +{ + ups_cmd_t *new_cmd = NULL; + + if (ups_get_cmd_pos(ups, val) >= 0) { + return 0; + } + + if (ups->cmd_count >= ups->cmd_allocs) { + ups->cmd_list = xrealloc(ups->cmd_list, sizeof(*ups->cmd_list) * (ups->cmd_allocs + CMD_ALLOC_BATCH)); + memset(ups->cmd_list + ups->cmd_allocs, 0, sizeof(*ups->cmd_list) * CMD_ALLOC_BATCH); + ups->cmd_allocs = ups->cmd_allocs + CMD_ALLOC_BATCH; + } + + new_cmd = xcalloc(1, sizeof(**ups->cmd_list)); + new_cmd->value = xstrdup(val); + new_cmd->needs_export = 1; + + ups->cmd_list[ups->cmd_count] = new_cmd; + ups->cmd_count++; + + upsdebugx(5, "%s: [%s]: added to ups->cmd_list: [%s]", + __func__, ups->socketname, val); + + return 1; +} + +static int ups_del_cmd(ups_device_t *ups, const char *val) +{ + int cmd_pos = ups_get_cmd_pos(ups, val); + + if (cmd_pos >= 0) { + ups_cmd_t *cmd = ups->cmd_list[cmd_pos]; + size_t i = 0; + + if (primary_ups == ups) { + dstate_delcmd(val); + + upsdebugx(5, "%s: [%s]: removed command from dstate: [%s]", + __func__, ups->socketname, val); + } + + free(cmd->value); + free(cmd); + + for (i = cmd_pos; i < ups->cmd_count - 1; ++i) { + ups->cmd_list[i] = ups->cmd_list[i + 1]; + } + + ups->cmd_list[ups->cmd_count - 1] = NULL; + ups->cmd_count--; + + if (ups->cmd_count == 0) { + free(ups->cmd_list); + ups->cmd_list = NULL; + ups->cmd_allocs = 0; + } + else if (ups->cmd_count % CMD_ALLOC_BATCH == 0) { + ups->cmd_list = xrealloc(ups->cmd_list, sizeof(*ups->cmd_list) * ups->cmd_count); + ups->cmd_allocs = ups->cmd_count; + } + + upsdebugx(5, "%s: [%s]: removed from ups->cmd_list: [%s]", + __func__, ups->socketname, val); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->cmd_list: [%s]", + __func__, ups->socketname, val); + + return 0; +} + +static int ups_get_var_pos(const ups_device_t *ups, const char *key) +{ + size_t i = 0; + + for (i = 0; i < ups->var_count; ++i) { + if (!strcmp(ups->var_list[i]->key, key)) { + return i; + } + } + + return -1; +} + +static int ups_set_var(ups_device_t *ups, const char *key, const char *value) +{ + ups_var_t *new_var = NULL; + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + + if (strcmp(var->value, value)) { + free(var->value); + var->value = xstrdup(value); + var->needs_export = 1; + + upsdebugx(5, "%s: [%s]: updated in ups->var_list: [%s] : [%s]", + __func__, ups->socketname, key, value); + + return 1; + } else { + upsdebugx(6, "%s: [%s]: unchanged in ups->var_list: [%s] : [%s]", + __func__, ups->socketname, key, value); + + return 1; + } + } + + if (ups->var_count >= ups->var_allocs) { + ups->var_list = xrealloc(ups->var_list, sizeof(*ups->var_list) * (ups->var_allocs + VAR_ALLOC_BATCH)); + memset(ups->var_list + ups->var_allocs, 0, sizeof(*ups->var_list) * VAR_ALLOC_BATCH); + ups->var_allocs = ups->var_allocs + VAR_ALLOC_BATCH; + } + + new_var = xcalloc(1, sizeof(**ups->var_list)); + new_var->key = xstrdup(key); + new_var->value = xstrdup(value); + new_var->needs_export = 1; + + ups->var_list[ups->var_count] = new_var; + ups->var_count++; + + upsdebugx(5, "%s: [%s]: stored in ups->var_list: [%s] : [%s]", + __func__, ups->socketname, key, value); + + return 1; +} + +static int ups_del_var(ups_device_t *ups, const char *key) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + size_t i = 0; + + if (primary_ups == ups) { + if (!strcmp(key, "ups.alarm")) { + alarm_init(); + alarm_commit(); + status_commit(); /* clear ALARM */ + + upsdebugx(5, "%s: [%s]: cleared UPS alarm from dstate: [%s]", + __func__, ups->socketname, key); + } + if (!strcmp(key, "ups.status")) { + status_init(); + status_commit(); /* clear STATUS */ + + upsdebugx(5, "%s: [%s]: cleared UPS status from dstate: [%s]", + __func__, ups->socketname, key); + } + + dstate_delinfo(key); + + upsdebugx(5, "%s: [%s]: removed variable from dstate: [%s]", + __func__, ups->socketname, key); + } + + ups_free_var_state(var); + free(var); + + for (i = var_pos; i < ups->var_count - 1; ++i) { + ups->var_list[i] = ups->var_list[i + 1]; + } + + ups->var_list[ups->var_count - 1] = NULL; + ups->var_count--; + + if (ups->var_count == 0) { + free(ups->var_list); + ups->var_list = NULL; + ups->var_allocs = 0; + } + else if (ups->var_count % VAR_ALLOC_BATCH == 0) { + ups->var_list = xrealloc(ups->var_list, sizeof(*ups->var_list) * ups->var_count); + ups->var_allocs = ups->var_count; + } + + upsdebugx(5, "%s: [%s]: removed from ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_set_var_flags(ups_device_t *ups, const char *key, const int flags) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + + if (var->flags == flags) { + upsdebugx(6, "%s: [%s]: unchanged flags in ups->var_list: [%s] : [%d]", + __func__, ups->socketname, key, flags); + + return 0; + } + + var->flags = flags; + var->needs_export = 1; + + upsdebugx(5, "%s: [%s]: stored flags in ups->var_list: [%s] : [%d]", + __func__, ups->socketname, key, flags); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_set_var_aux(ups_device_t *ups, const char *key, const long aux) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + + if (var->aux == aux) { + upsdebugx(6, "%s: [%s]: unchanged aux in ups->var_list: [%s] : [%ld]", + __func__, ups->socketname, key, aux); + + return 0; + } + + var->aux = aux; + var->needs_export = 1; + + upsdebugx(5, "%s: [%s]: stored aux in ups->var_list: [%s] : [%ld]", + __func__, ups->socketname, key, aux); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_add_range(ups_device_t *ups, const char *key, const int min, const int max) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + var_range_t *new_range = NULL; + ups_var_t *var = ups->var_list[var_pos]; + size_t i = 0; + + for (i = 0; i < var->range_count; ++i) { + if (var->range_list[i]->min == min && var->range_list[i]->max == max) { + upsdebugx(6, "%s: [%s]: unchanged in ups->var_list->range_list: [%s] : min=[%d] : max=[%d]", + __func__, ups->socketname, key, min, max); + + return 0; + } + } + + if (var->range_count >= var->range_allocs) { + var->range_list = xrealloc(var->range_list, sizeof(*var->range_list) * (var->range_allocs + SUBVAR_ALLOC_BATCH)); + memset(var->range_list + var->range_allocs, 0, sizeof(*var->range_list) * SUBVAR_ALLOC_BATCH); + var->range_allocs = var->range_allocs + SUBVAR_ALLOC_BATCH; + } + + new_range = xcalloc(1, sizeof(**var->range_list)); + new_range->min = min; + new_range->max = max; + + var->range_list[var->range_count] = new_range; + var->range_count++; + var->needs_export = 1; + + upsdebugx(5, "%s: [%s]: added to ups->var_list->range_list: [%s] : min=[%d] : max=[%d]", + __func__, ups->socketname, key, min, max); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_del_range(ups_device_t *ups, const char *key, const int min, const int max) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + size_t i = 0; + + for (i = 0; i < var->range_count; ++i) { + if (var->range_list[i]->min == min && var->range_list[i]->max == max) { + size_t j = 0; + + if (primary_ups == ups) { + dstate_delrange(key, min, max); + + upsdebugx(5, "%s: [%s]: removed range from dstate: [%s] : min=[%d] : max=[%d]", + __func__, ups->socketname, key, min, max); + } + + free(var->range_list[i]); + + for (j = i; j < var->range_count - 1; ++j) { + var->range_list[j] = var->range_list[j + 1]; + } + + var->range_list[var->range_count - 1] = NULL; + var->range_count--; + + if (var->range_count == 0) { + free(var->range_list); + var->range_list = NULL; + var->range_allocs = 0; + } + else if (var->range_count % SUBVAR_ALLOC_BATCH == 0) { + var->range_list = xrealloc(var->range_list, sizeof(*var->range_list) * var->range_count); + var->range_allocs = var->range_count; + } + + upsdebugx(5, "%s: [%s]: deleted from ups->var_list->range_list: [%s] : min=[%d] : max=[%d]", + __func__, ups->socketname, key, min, max); + + return 1; + } + } + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_add_enum(ups_device_t *ups, const char *key, const char *val) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + size_t i = 0; + + for (i = 0; i < var->enum_count; ++i) { + if (!strcmp(var->enum_list[i], val)) { + return 0; + } + } + + if (var->enum_count >= var->enum_allocs) { + var->enum_list = xrealloc(var->enum_list, sizeof(*var->enum_list) * (var->enum_allocs + SUBVAR_ALLOC_BATCH)); + memset(var->enum_list + var->enum_allocs, 0, sizeof(*var->enum_list) * SUBVAR_ALLOC_BATCH); + var->enum_allocs = var->enum_allocs + SUBVAR_ALLOC_BATCH; + } + + var->enum_list[var->enum_count] = xstrdup(val); + + var->enum_count++; + var->needs_export = 1; + + upsdebugx(5, "%s: [%s]: added to ups->var_list->enum_list: [%s] : [%s]", + __func__, ups->socketname, key, val); + + return 1; + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static int ups_del_enum(ups_device_t *ups, const char *key, const char *val) +{ + int var_pos = ups_get_var_pos(ups, key); + + if (var_pos >= 0) { + ups_var_t *var = ups->var_list[var_pos]; + size_t i = 0; + + for (i = 0; i < var->enum_count; ++i) { + if (!strcmp(var->enum_list[i], val)) { + size_t j = 0; + + if (primary_ups == ups) { + dstate_delenum(key, val); + + upsdebugx(5, "%s: [%s]: removed enum from dstate: [%s] : [%s]", + __func__, ups->socketname, key, val); + } + + free(var->enum_list[i]); + + for (j = i; j < var->enum_count - 1; ++j) { + var->enum_list[j] = var->enum_list[j + 1]; + } + + var->enum_list[var->enum_count - 1] = NULL; + var->enum_count--; + + if (var->enum_count == 0) { + free(var->enum_list); + var->enum_list = NULL; + var->enum_allocs = 0; + } + else if (var->enum_count % SUBVAR_ALLOC_BATCH == 0) { + var->enum_list = xrealloc(var->enum_list, sizeof(*var->enum_list) * var->enum_count); + var->enum_allocs = var->enum_count; + } + + upsdebugx(5, "%s: [%s]: deleted from ups->var_list->enum_list: [%s] : [%s]", + __func__, ups->socketname, key, val); + + + return 1; + } + } + } + + upsdebugx(6, "%s: [%s]: not found in ups->var_list: [%s]", + __func__, ups->socketname, key); + + return 0; +} + +static void free_status_filters(void) +{ + size_t i = 0; + + if (arg_status_filters.have_any) { + for (i = 0; i < arg_status_filters.have_any_count; ++i) { + free(arg_status_filters.have_any[i]); + } + free(arg_status_filters.have_any); + arg_status_filters.have_any = NULL; + arg_status_filters.have_any_count = 0; + } + + if (arg_status_filters.have_all) { + for (i = 0; i < arg_status_filters.have_all_count; ++i) { + free(arg_status_filters.have_all[i]); + } + free(arg_status_filters.have_all); + arg_status_filters.have_all = NULL; + arg_status_filters.have_all_count = 0; + } + + if (arg_status_filters.nothave_any) { + for (i = 0; i < arg_status_filters.nothave_any_count; ++i) { + free(arg_status_filters.nothave_any[i]); + } + free(arg_status_filters.nothave_any); + arg_status_filters.nothave_any = NULL; + arg_status_filters.nothave_any_count = 0; + } + + if (arg_status_filters.nothave_all) { + for (i = 0; i < arg_status_filters.nothave_all_count; ++i) { + free(arg_status_filters.nothave_all[i]); + } + free(arg_status_filters.nothave_all); + arg_status_filters.nothave_all = NULL; + arg_status_filters.nothave_all_count = 0; + } +} + +static void ups_free_ups_state(ups_device_t *ups) +{ + size_t i = 0; + + if (ups->var_list) { + for (i = 0; i < ups->var_count; ++i) { + if (ups->var_list[i]) { + ups_free_var_state(ups->var_list[i]); + free(ups->var_list[i]); + ups->var_list[i] = NULL; + } + } + + free(ups->var_list); + ups->var_list = NULL; + ups->var_count = 0; + ups->var_allocs = 0; + } + + if (ups->cmd_list) { + for (i = 0; i < ups->cmd_count; ++i) { + if (ups->cmd_list[i]) { + if (ups->cmd_list[i]->value) { + free(ups->cmd_list[i]->value); + ups->cmd_list[i]->value = NULL; + } + free(ups->cmd_list[i]); + ups->cmd_list[i] = NULL; + } + } + + free(ups->cmd_list); + ups->cmd_list = NULL; + ups->cmd_count = 0; + ups->cmd_allocs = 0; + } + + if (ups->status) { + free(ups->status); + ups->status = NULL; + } +} + +static void ups_free_var_state(ups_var_t *var) +{ + size_t i = 0; + + if (var->key) { + free(var->key); + var->key = NULL; + } + + if (var->value) { + free(var->value); + var->value = NULL; + } + + if (var->enum_list) { + for (i = 0; i < var->enum_count; ++i) { + if (var->enum_list[i]) { + free(var->enum_list[i]); + var->enum_list[i] = NULL; + } + } + free(var->enum_list); + var->enum_list = NULL; + var->enum_count = 0; + var->enum_allocs = 0; + } + + if (var->range_list) { + for (i = 0; i < var->range_count; ++i) { + if (var->range_list[i]) { + free(var->range_list[i]); + var->range_list[i] = NULL; + } + } + free(var->range_list); + var->range_list = NULL; + var->range_count = 0; + var->range_allocs = 0; + } +} + +static const char *rewrite_driver_prefix(const char *in, char *out, size_t outlen) +{ + int required = -1; + + if (!strncmp(in, "driver.", 7)) { + required = snprintf(out, outlen, "upstream.%s", in); + + if ((size_t)required >= outlen) { + upslogx(LOG_WARNING, "%s: truncated variable name [%s] " + "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", + __func__, out, (size_t)required, outlen); + } + + return out; + } + + return in; +} + +static int split_socket_name(const char *input, char **driver, char **ups) +{ + size_t drv_len = 0; + size_t ups_len = 0; + const char *last_dash = strrchr(input, '-'); + + if (!input || !last_dash || last_dash == input || *(last_dash + 1) == '\0') { + *driver = NULL; + *ups = NULL; + + return 0; + } + + drv_len = last_dash - input; + ups_len = strlen(last_dash + 1); + + *driver = xmalloc(drv_len + 1); + *ups = xmalloc(ups_len + 1); + + snprintf(*driver, (drv_len + 1), "%.*s", (int)drv_len, input); + snprintf(*ups, (ups_len + 1), "%s", last_dash + 1); + + str_trim_space(*driver); + str_trim_space(*ups); + + return 1; +} + +static void csv_arg_to_array(const char *argname, const char *argcsv, char ***array, size_t *countvar) +{ + char *tmp = NULL; + char *token = NULL; + char *str = NULL; + + if (!argcsv) { + *array = NULL; + *countvar = 0; + + return; + } + + tmp = xstrdup(argcsv); + + token = strtok(tmp, ","); + while (token) { + str_trim_space(token); + + str = xstrdup(token); + + *array = xrealloc(*array, sizeof(**array) * (*countvar + 1)); + (*array)[*countvar] = str; + (*countvar)++; + + upsdebugx(1, "%s: added [%s] to [%s] from configuration", + __func__, str, argname); + + token = strtok(NULL, ","); + } + + free(tmp); + tmp = NULL; +} + +static inline void ups_set_flag(ups_device_t *ups, ups_flags_t flag) +{ + ups->flags |= flag; +} + +static inline void ups_clear_flag(ups_device_t *ups, ups_flags_t flag) +{ + ups->flags &= ~flag; +} + +static inline int ups_has_flag(const ups_device_t *ups, ups_flags_t flags) +{ + return (ups->flags & flags) == flags; +} From 5855e6e6e16b84a39d3b3df4ac57d96636677527 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 09:00:29 +0200 Subject: [PATCH 02/41] server/netget.c: rewrite upstream prefix for proxying drivers Signed-off-by: Sebastian Kuttnig --- server/netget.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/server/netget.c b/server/netget.c index 1fd4d2475a..de22d96ace 100644 --- a/server/netget.c +++ b/server/netget.c @@ -69,6 +69,7 @@ static void get_upsdesc(nut_ctype_t *client, const char *upsname) static void get_desc(nut_ctype_t *client, const char *upsname, const char *var) { const upstype_t *ups; + const char *varptr; const char *desc; ups = get_ups_ptr(upsname); @@ -81,7 +82,15 @@ static void get_desc(nut_ctype_t *client, const char *upsname, const char *var) if (!ups_available(ups, client)) return; - desc = desc_get_var(var); + /* Strip out upstream. for proxying (failover, clone...) lookups, + * but return the requested full variable info back to the client. */ + if (var && !strncmp(var, "upstream.", 9)) { + varptr = var + 9; + } else { + varptr = var; + } + + desc = desc_get_var(varptr); if (desc) sendback(client, "DESC %s %s \"%s\"\n", upsname, var, desc); @@ -92,6 +101,7 @@ static void get_desc(nut_ctype_t *client, const char *upsname, const char *var) static void get_cmddesc(nut_ctype_t *client, const char *upsname, const char *cmd) { const upstype_t *ups; + const char *cmdptr; const char *desc; ups = get_ups_ptr(upsname); @@ -104,7 +114,15 @@ static void get_cmddesc(nut_ctype_t *client, const char *upsname, const char *cm if (!ups_available(ups, client)) return; - desc = desc_get_cmd(cmd); + /* Strip out upstream. for proxying (failover, clone...) lookups, + * but return the requested full command info back to the client. */ + if (cmd && !strncmp(cmd, "upstream.", 9)) { + cmdptr = cmd + 9; + } else { + cmdptr = cmd; + } + + desc = desc_get_cmd(cmdptr); if (desc) sendback(client, "CMDDESC %s %s \"%s\"\n", upsname, cmd, desc); From fe08563a20394e074163878ed72d0e5368f7a5a9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 09:04:37 +0200 Subject: [PATCH 03/41] NEWS.adoc: introduce failover driver Signed-off-by: Sebastian Kuttnig --- NEWS.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 8cecb77884..13ef6f1987 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -136,6 +136,9 @@ https://github.com/networkupstools/nut/milestone/9 This seems to be a protocol developed by Cyber Energy for serial-port devices, subsequently used by different vendors in their own products or re-branded Cyber Energy creations. [#2940] + * Introduced a `failover` driver for monitoring multiple UPS driver sockets + and seamless switching out of UPS data in a failover situation, includes + support for end-to-end tracked instant commands and also variable updating. - NUT Monitor GUI: * Ported Python 3 version to Qt6, now shipped alongside Qt5 for systems From bf3b9e2bf7cb11251d67b9e1f6f8dcb478aa897c Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 09:58:49 +0200 Subject: [PATCH 04/41] drivers/failover.c, NEWS.adoc: fixes for compiler warnings, add PR number to news entry [#2962] Signed-off-by: Sebastian Kuttnig --- NEWS.adoc | 1 + drivers/failover.c | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 13ef6f1987..8280dc1f8f 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -139,6 +139,7 @@ https://github.com/networkupstools/nut/milestone/9 * Introduced a `failover` driver for monitoring multiple UPS driver sockets and seamless switching out of UPS data in a failover situation, includes support for end-to-end tracked instant commands and also variable updating. + [#2962] - NUT Monitor GUI: * Ported Python 3 version to Qt6, now shipped alongside Qt5 for systems diff --git a/drivers/failover.c b/drivers/failover.c index 83ccf8b003..72e2dd0b07 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -192,7 +192,7 @@ static void ups_is_dead(ups_device_t *ups); static void ups_is_online(ups_device_t *ups); static void ups_is_offline(ups_device_t *ups); -static ups_device_t *get_primary_candidate(); +static ups_device_t *get_primary_candidate(void); static int ups_passes_status_filters(const ups_device_t *ups); static void ups_promote_primary(ups_device_t *ups); static void ups_demote_primary(ups_device_t *ups); @@ -876,6 +876,17 @@ static void handle_no_primaries(void) dstate_dataok(); break; + + default: + /* Should never happen, as we validate the argument */ + if (!primaries_gone) { + upslogx(LOG_WARNING, "%s: 'fsdmode' has unknown value [%d]: " + "keeping last primary and declaring data stale immediately", + __func__, arg_fsdmode); + } + + dstate_datastale(); + break; } primaries_gone = 1; } else { @@ -1390,7 +1401,7 @@ static void ups_is_offline(ups_device_t *ups) { } } -static ups_device_t *get_primary_candidate() +static ups_device_t *get_primary_candidate(void) { time_t now; size_t i = 0; @@ -1424,7 +1435,7 @@ static ups_device_t *get_primary_candidate() ups->force_ignore_time = 0; } - if (ups_has_flag(ups, UPS_FLAG_ALIVE | UPS_FLAG_DUMPED)) { + if (ups_has_flag(ups, (ups_flags_t)(UPS_FLAG_ALIVE | UPS_FLAG_DUMPED))) { if (ups->force_ignore < 0) { ups->priority = PRIORITY_SKIPPED; @@ -1461,7 +1472,7 @@ static ups_device_t *get_primary_candidate() "only status filters, but not the default set of lower priorities", __func__, ups->socketname); } - else if (ups_has_flag(ups, UPS_FLAG_DATA_OK | UPS_FLAG_ONLINE)) { + else if (ups_has_flag(ups, (ups_flags_t)(UPS_FLAG_DATA_OK | UPS_FLAG_ONLINE))) { priority = PRIORITY_GOOD; } else if (ups_has_flag(ups, UPS_FLAG_DATA_OK)) { From 961fbc25eac841e8d0bcb94482492b7fe9922227 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 11:02:03 +0200 Subject: [PATCH 05/41] drivers/failover.c: add shutdown non-handling Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/failover.c b/drivers/failover.c index 72e2dd0b07..b39f544942 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -324,7 +324,13 @@ void upsdrv_updateinfo(void) void upsdrv_shutdown(void) { + upslogx(LOG_ERR, "%s: %s: Shutdown is not supported by this driver, " + "monitored upstream drivers will shutdown when called to do so", + progname, __func__); + if (handling_upsdrv_shutdown > 0) { + set_exit_flag(EF_EXIT_FAILURE); + } } void upsdrv_help(void) From 6b3315706d6df2841844918a41c333193d796698 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 11:39:51 +0200 Subject: [PATCH 06/41] drivers/failover.c: free parse_port_argument() tmp on premature exit [#2962] Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/failover.c b/drivers/failover.c index b39f544942..a2a9260880 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -708,6 +708,7 @@ static void parse_port_argument(void) if (!split_socket_name(new_ups->socketname, &new_ups->drivername, &new_ups->name)) { free(new_ups->socketname); free(new_ups); + free(tmp); fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument has an invalid format, " "[%s] is not a valid splittable socket name, please correct the argument", progname, __func__, token); From e5f3631e087391db94b6e4ebc3c5a5a0a6c58cf7 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 13:55:08 +0200 Subject: [PATCH 07/41] drivers/failover.c: clean dstate after fsdmode 0, remove now freed var from exit debug Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index a2a9260880..bb9c8362f0 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -197,7 +197,7 @@ static int ups_passes_status_filters(const ups_device_t *ups); static void ups_promote_primary(ups_device_t *ups); static void ups_demote_primary(ups_device_t *ups); static void ups_export_dstate(ups_device_t *ups); -static void ups_clean_dstate(ups_device_t *ups); +static void ups_clean_dstate(const ups_device_t *ups); static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd); static int ups_add_cmd(ups_device_t *ups, const char *val); @@ -301,6 +301,7 @@ void upsdrv_updateinfo(void) /* Special handling for fsdmode 0 where primary was never demoted */ upslogx(LOG_NOTICE, "%s: [%s] was declared to be a suitable primary (again)", __func__, primary_candidate->socketname); + ups_clean_dstate(primary_candidate); primary_candidate->force_dstate_export = 1; } primaries_gone = 0; @@ -709,9 +710,9 @@ static void parse_port_argument(void) free(new_ups->socketname); free(new_ups); free(tmp); - fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument has an invalid format, " - "[%s] is not a valid splittable socket name, please correct the argument", - progname, __func__, token); + fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument contains at least one " + "invalid and non-splittable socket name, please correct the argument", + progname, __func__); } else { upsdebugx(3, "%s: [%s] was parsed into UPS driver [%s] and UPS [%s]", __func__, new_ups->socketname, new_ups->drivername, new_ups->name); @@ -1702,7 +1703,7 @@ static void ups_export_dstate(ups_device_t *ups) ups->force_dstate_export = 0; } -static void ups_clean_dstate(ups_device_t *ups) +static void ups_clean_dstate(const ups_device_t *ups) { size_t i = 0; From 2fac5e1f78efbe0a0c4bb7084594b6fa7b14cde9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 14:44:59 +0200 Subject: [PATCH 08/41] drivers/failover.c: preserve which port value failed the argument parsing process Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index bb9c8362f0..3e524ad042 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -707,12 +707,16 @@ static void parse_port_argument(void) new_ups->socketname = xstrdup(token); if (!split_socket_name(new_ups->socketname, &new_ups->drivername, &new_ups->name)) { + char buf[SMALLBUF]; + snprintf(buf, sizeof(buf), "%s", token); /* for fatalx */ + free(new_ups->socketname); free(new_ups); free(tmp); - fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument contains at least one " - "invalid and non-splittable socket name, please correct the argument", - progname, __func__); + + fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument has an invalid format, " + "[%s] is not a valid splittable socket name, please correct the argument", + progname, __func__, buf); } else { upsdebugx(3, "%s: [%s] was parsed into UPS driver [%s] and UPS [%s]", __func__, new_ups->socketname, new_ups->drivername, new_ups->name); From f4d8ab7c428c1f2524837f92c2a4b21f2e5e4b64 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 19 May 2025 16:27:14 +0200 Subject: [PATCH 09/41] drivers/failover.c: make defensive freeing consistent throughout the file Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 56 ++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 3e524ad042..df02db6544 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -412,39 +412,43 @@ void upsdrv_cleanup(void) for (i = 0; i < ups_count; ++i) { ups_device_t *ups = ups_list[i]; - if (primary_ups == ups) { - primary_ups = NULL; - } + if (ups) { + if (primary_ups == ups) { + primary_ups = NULL; + } - if (last_primary_ups == ups) { - last_primary_ups = NULL; - } + if (last_primary_ups == ups) { + last_primary_ups = NULL; + } - ups_disconnect(ups); /* free conn + ctx */ + ups_disconnect(ups); /* free conn + ctx */ - ups_free_ups_state(ups); /* free status, vars, subvars + cmds */ + ups_free_ups_state(ups); /* free status, vars, subvars + cmds */ - if (ups->name) { - free(ups->name); - ups->name = NULL; - } + if (ups->name) { + free(ups->name); + ups->name = NULL; + } - if (ups->drivername) { - free(ups->drivername); - ups->drivername = NULL; - } + if (ups->drivername) { + free(ups->drivername); + ups->drivername = NULL; + } - if (ups->socketname) { - free(ups->socketname); - ups->socketname = NULL; - } + if (ups->socketname) { + free(ups->socketname); + ups->socketname = NULL; + } - free(ups); - ups_list[i] = NULL; + free(ups); + ups_list[i] = NULL; + } } - free(ups_list); - ups_list = NULL; + if (ups_list) { + free(ups_list); + ups_list = NULL; + } free_status_filters(); /* free status filters */ } @@ -2189,6 +2193,7 @@ static void free_status_filters(void) if (arg_status_filters.have_any) { for (i = 0; i < arg_status_filters.have_any_count; ++i) { free(arg_status_filters.have_any[i]); + arg_status_filters.have_any[i] = NULL; } free(arg_status_filters.have_any); arg_status_filters.have_any = NULL; @@ -2198,6 +2203,7 @@ static void free_status_filters(void) if (arg_status_filters.have_all) { for (i = 0; i < arg_status_filters.have_all_count; ++i) { free(arg_status_filters.have_all[i]); + arg_status_filters.have_all[i] = NULL; } free(arg_status_filters.have_all); arg_status_filters.have_all = NULL; @@ -2207,6 +2213,7 @@ static void free_status_filters(void) if (arg_status_filters.nothave_any) { for (i = 0; i < arg_status_filters.nothave_any_count; ++i) { free(arg_status_filters.nothave_any[i]); + arg_status_filters.nothave_any[i] = NULL; } free(arg_status_filters.nothave_any); arg_status_filters.nothave_any = NULL; @@ -2216,6 +2223,7 @@ static void free_status_filters(void) if (arg_status_filters.nothave_all) { for (i = 0; i < arg_status_filters.nothave_all_count; ++i) { free(arg_status_filters.nothave_all[i]); + arg_status_filters.nothave_all[i] = NULL; } free(arg_status_filters.nothave_all); arg_status_filters.nothave_all = NULL; From 2c742146c97e76e4b866a118e8b260459bc128e1 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Thu, 22 May 2025 08:25:27 +0200 Subject: [PATCH 10/41] drivers/failover.c: reword shutdown to be more clear Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index df02db6544..bff37a41da 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -325,8 +325,9 @@ void upsdrv_updateinfo(void) void upsdrv_shutdown(void) { - upslogx(LOG_ERR, "%s: %s: Shutdown is not supported by this driver, " - "monitored upstream drivers will shutdown when called to do so", + upslogx(LOG_ERR, "%s: %s: Shutdown is not supported by this proxying driver. " + "Upstream drivers may implement their own shutdown handling, which would be " + "called directly or by upsdrvctl to shut down any specific upstream driver.", progname, __func__); if (handling_upsdrv_shutdown > 0) { From 1ad9b265b59b208e2eebadab2164212aab733a1a Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:10:49 +0200 Subject: [PATCH 11/41] drivers/failover.c: use NUT_STRARG helper for null checks in various places Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index bff37a41da..2fed77923b 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -500,7 +500,7 @@ static int instcmd(const char *cmdname, const char *extra) if (!primary_ups) { upslogx(LOG_INSTCMD_FAILED, "%s: received [%s] [%s], but" "there is currently no elected primary able to handle it", - __func__, cmdname, extra ? extra : ""); + __func__, cmdname, NUT_STRARG(extra)); return STAT_INSTCMD_FAILED; } @@ -541,13 +541,13 @@ static int instcmd(const char *cmdname, const char *extra) if (cmdret >= 0) { upslogx(LOG_NOTICE, "%s: sent [%s] [%s], " "received response code: [%" PRIiSIZE "]", - __func__, cmdname, extra ? extra : "", cmdret); + __func__, cmdname, NUT_STRARG(extra), cmdret); return cmdret; } else { upslog_with_errno(LOG_INSTCMD_FAILED, "%s: sent [%s] [%s], " "received no response code due to socket failure", - __func__, cmdname, extra ? extra : ""); + __func__, cmdname, NUT_STRARG(extra)); return STAT_INSTCMD_FAILED; } @@ -555,7 +555,7 @@ static int instcmd(const char *cmdname, const char *extra) upslogx(LOG_INSTCMD_UNKNOWN, "%s: received [%s] [%s], " "but it is not among the primary's supported commands", - __func__, cmdname, extra ? extra : ""); + __func__, cmdname, NUT_STRARG(extra)); return STAT_INSTCMD_UNKNOWN; } From 683559ccc9b66990e729c5bd9e5dd92a875557db Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:14:47 +0200 Subject: [PATCH 12/41] drivers/failover.c: use enum for priorities Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 2fed77923b..ce12557278 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -44,13 +44,6 @@ #define DEFAULT_FSD_MODE 0 #define DEFAULT_STRICT_FILTERING 0 -#define PRIORITY_SKIPPED -1 -#define PRIORITY_FORCED 0 -#define PRIORITY_USERFILTERS 1 -#define PRIORITY_GOOD 2 -#define PRIORITY_WEAK 3 -#define PRIORITY_LASTRESORT 4 - upsdrv_info_t upsdrv_info = { DRIVER_NAME, DRIVER_VERSION, @@ -59,6 +52,15 @@ upsdrv_info_t upsdrv_info = { { NULL } }; +typedef enum { + PRIORITY_SKIPPED = -1, + PRIORITY_FORCED = 0, + PRIORITY_USERFILTERS = 1, + PRIORITY_GOOD = 2, + PRIORITY_WEAK = 3, + PRIORITY_LASTRESORT = 4 +} ups_priority_t; + typedef enum { UPS_FLAG_NONE = 0, UPS_FLAG_ALIVE = 1 << 0, @@ -135,7 +137,7 @@ typedef struct { time_t force_primary_time; ups_flags_t flags; - int priority; + ups_priority_t priority; int failure_count; int force_ignore; From fb3d251a8f24a3530bb30df2a3eb13bdf46bcf18 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:32:24 +0200 Subject: [PATCH 13/41] drivers/failover.{c,h}: introduce failover.h for defines, typedefs Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 110 +---------------------------------- drivers/failover.h | 139 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 109 deletions(-) create mode 100644 drivers/failover.h diff --git a/drivers/failover.c b/drivers/failover.c index ce12557278..bd2dca3c9a 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -20,6 +20,7 @@ #include "config.h" #include "main.h" +#include "failover.h" #include "nut_stdint.h" #include "parseconf.h" #include "timehead.h" @@ -28,22 +29,6 @@ #define DRIVER_NAME "UPS Failover Driver" #define DRIVER_VERSION "0.01" -#define VAR_ALLOC_BATCH 50 -#define SUBVAR_ALLOC_BATCH 10 -#define CMD_ALLOC_BATCH 20 -#define CONN_READ_TIMEOUT 3 -#define CONN_CMD_TIMEOUT 3 -#define ALARM_PROPAG_TIME 15 - -#define DEFAULT_INIT_TIMEOUT 30 -#define DEFAULT_DEAD_TIMEOUT 30 -#define DEFAULT_CONNECTION_COOLOFF 15 -#define DEFAULT_NO_PRIMARY_TIMEOUT 15 -#define DEFAULT_MAX_CONNECT_FAILS 5 -#define DEFAULT_RELOG_TIMEOUT 5 -#define DEFAULT_FSD_MODE 0 -#define DEFAULT_STRICT_FILTERING 0 - upsdrv_info_t upsdrv_info = { DRIVER_NAME, DRIVER_VERSION, @@ -52,99 +37,6 @@ upsdrv_info_t upsdrv_info = { { NULL } }; -typedef enum { - PRIORITY_SKIPPED = -1, - PRIORITY_FORCED = 0, - PRIORITY_USERFILTERS = 1, - PRIORITY_GOOD = 2, - PRIORITY_WEAK = 3, - PRIORITY_LASTRESORT = 4 -} ups_priority_t; - -typedef enum { - UPS_FLAG_NONE = 0, - UPS_FLAG_ALIVE = 1 << 0, - UPS_FLAG_DUMPED = 1 << 1, - UPS_FLAG_DATA_OK = 1 << 2, - UPS_FLAG_ONLINE = 1 << 3, - UPS_FLAG_PRIMARY = 1 << 4 -} ups_flags_t; - -typedef struct { - int min; - int max; -} var_range_t; - -typedef struct { - char *key; - char *value; - - char **enum_list; - var_range_t **range_list; - - size_t enum_count; - size_t enum_allocs; - size_t range_count; - size_t range_allocs; - - long aux; - - int flags; - int needs_export; -} ups_var_t; - -typedef struct { - char *value; - int needs_export; -} ups_cmd_t; - -typedef struct { - char **have_any; - size_t have_any_count; - - char **have_all; - size_t have_all_count; - - char **nothave_any; - size_t nothave_any_count; - - char **nothave_all; - size_t nothave_all_count; -} status_filters_t; - -typedef struct { - char *name; - char *drivername; - char *socketname; - - udq_pipe_conn_t *conn; - PCONF_CTX_t parse_ctx; - - ups_var_t **var_list; - ups_cmd_t **cmd_list; - - size_t var_count; - size_t var_allocs; - size_t cmd_count; - size_t cmd_allocs; - - char *status; - - time_t last_heard_time; - time_t last_pinged_time; - time_t last_failure_time; - time_t force_ignore_time; - time_t force_primary_time; - - ups_flags_t flags; - ups_priority_t priority; - int failure_count; - - int force_ignore; - int force_primary; - int force_dstate_export; -} ups_device_t; - static status_filters_t arg_status_filters; static int arg_init_timeout = DEFAULT_INIT_TIMEOUT; diff --git a/drivers/failover.h b/drivers/failover.h new file mode 100644 index 0000000000..a70b4a654f --- /dev/null +++ b/drivers/failover.h @@ -0,0 +1,139 @@ +/* failover.h - UPS Failover Driver (Header) + + Copyright (C) + 2025 - Sebastian Kuttnig + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#ifndef FAILOVER_H_SEEN +#define FAILOVER_H_SEEN 1 + +#include "config.h" +#include "main.h" +#include "parseconf.h" +#include "timehead.h" +#include "upsdrvquery.h" + +#define VAR_ALLOC_BATCH 50 +#define SUBVAR_ALLOC_BATCH 10 +#define CMD_ALLOC_BATCH 20 +#define CONN_READ_TIMEOUT 3 +#define CONN_CMD_TIMEOUT 3 +#define ALARM_PROPAG_TIME 15 + +#define DEFAULT_INIT_TIMEOUT 30 +#define DEFAULT_DEAD_TIMEOUT 30 +#define DEFAULT_CONNECTION_COOLOFF 15 +#define DEFAULT_NO_PRIMARY_TIMEOUT 15 +#define DEFAULT_MAX_CONNECT_FAILS 5 +#define DEFAULT_RELOG_TIMEOUT 5 +#define DEFAULT_FSD_MODE 0 +#define DEFAULT_STRICT_FILTERING 0 + +typedef enum { + PRIORITY_SKIPPED = -1, + PRIORITY_FORCED = 0, + PRIORITY_USERFILTERS = 1, + PRIORITY_GOOD = 2, + PRIORITY_WEAK = 3, + PRIORITY_LASTRESORT = 4 +} ups_priority_t; + +typedef enum { + UPS_FLAG_NONE = 0, + UPS_FLAG_ALIVE = 1 << 0, + UPS_FLAG_DUMPED = 1 << 1, + UPS_FLAG_DATA_OK = 1 << 2, + UPS_FLAG_ONLINE = 1 << 3, + UPS_FLAG_PRIMARY = 1 << 4 +} ups_flags_t; + +typedef struct { + int min; + int max; +} var_range_t; + +typedef struct { + char *key; + char *value; + + char **enum_list; + var_range_t **range_list; + + size_t enum_count; + size_t enum_allocs; + size_t range_count; + size_t range_allocs; + + long aux; + + int flags; + int needs_export; +} ups_var_t; + +typedef struct { + char *value; + int needs_export; +} ups_cmd_t; + +typedef struct { + char **have_any; + size_t have_any_count; + + char **have_all; + size_t have_all_count; + + char **nothave_any; + size_t nothave_any_count; + + char **nothave_all; + size_t nothave_all_count; +} status_filters_t; + +typedef struct { + char *name; + char *drivername; + char *socketname; + + udq_pipe_conn_t *conn; + PCONF_CTX_t parse_ctx; + + ups_var_t **var_list; + ups_cmd_t **cmd_list; + + size_t var_count; + size_t var_allocs; + size_t cmd_count; + size_t cmd_allocs; + + char *status; + + time_t last_heard_time; + time_t last_pinged_time; + time_t last_failure_time; + time_t force_ignore_time; + time_t force_primary_time; + + ups_flags_t flags; + ups_priority_t priority; + int failure_count; + + int force_ignore; + int force_primary; + int force_dstate_export; +} ups_device_t; + +#endif /* FAILOVER_H_SEEN */ From 18ac1742256f9b56fe8155bc34d2bc9cc9e51e2e Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:35:09 +0200 Subject: [PATCH 14/41] drivers/failover.c: do not fatalx on no connectable drivers, keep trying instead Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/failover.c b/drivers/failover.c index bd2dca3c9a..6abe27eaf9 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -154,7 +154,7 @@ void upsdrv_initinfo(void) } if (!ups_alive_count) { - fatalx(EXIT_FAILURE, "%s: %s: none of the tracked UPS drivers were connectable", + upslogx(LOG_WARNING, "%s: %s: none of the tracked UPS drivers were connectable", progname, __func__); } From be52d0b9ffa3a8cb527f44785448ce439f3d8676 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:38:14 +0200 Subject: [PATCH 15/41] drivers/failover.c: remove redundant _init() calls for status/alarm Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 6abe27eaf9..9d802bd87a 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -756,11 +756,10 @@ static void handle_no_primaries(void) __func__, ALARM_PROPAG_TIME); } - status_init(); - alarm_init(); + /* dstate is already clean at this point, hence no _init() calls */ alarm_set("No suitable primaries for failover"); alarm_commit(); - status_commit(); + status_commit(); /* publish ALARM */ if (elapsed < arg_noprimary_timeout + ALARM_PROPAG_TIME) { /* make sure ALARM propagates to all clients first... */ @@ -778,12 +777,11 @@ static void handle_no_primaries(void) __func__); } - status_init(); - alarm_init(); + /* dstate is already clean at this point, hence no _init() calls */ status_set("FSD"); alarm_set("No suitable primaries for failover"); alarm_commit(); - status_commit(); + status_commit(); /* publish ALARM + FSD */ dstate_dataok(); break; From f3b154167ae39db7e251b1a23ea5db242361f6bf Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 06:43:23 +0200 Subject: [PATCH 16/41] drivers/failover.c: show truncation content at end of log message Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 9d802bd87a..506d02c556 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -138,18 +138,18 @@ void upsdrv_initinfo(void) dstate_addcmd(buf); if ((size_t)required >= sizeof(buf)) { - upslogx(LOG_WARNING, "%s: truncated administrative command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, buf, (size_t)required, sizeof(buf)); + upslogx(LOG_WARNING, "%s: truncated administrative command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), buf); } required = snprintf(buf, sizeof(buf), "%s.force.primary", ups->socketname); dstate_addcmd(buf); if ((size_t)required >= sizeof(buf)) { - upslogx(LOG_WARNING, "%s: truncated administrative command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, buf, (size_t)required, sizeof(buf)); + upslogx(LOG_WARNING, "%s: truncated administrative command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), buf); } } @@ -421,9 +421,9 @@ static int instcmd(const char *cmdname, const char *extra) } if ((size_t)required >= sizeof(msgbuf)) { - upslogx(LOG_WARNING, "%s: truncated INSTCMD command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, msgbuf, (size_t)required, sizeof(msgbuf)); + upslogx(LOG_WARNING, "%s: truncated INSTCMD command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(msgbuf), msgbuf); } tv.tv_sec = CONN_CMD_TIMEOUT; @@ -484,9 +484,9 @@ static int setvar(const char *varname, const char *val) required = snprintf(msgbuf, sizeof(msgbuf), "SET %s \"%s\"\n", var, val); if ((size_t)required >= sizeof(msgbuf)) { - upslogx(LOG_WARNING, "%s: truncated SET command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, msgbuf, (size_t)required, sizeof(msgbuf)); + upslogx(LOG_WARNING, "%s: truncated SET command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(msgbuf), msgbuf); } tv.tv_sec = CONN_CMD_TIMEOUT; @@ -1034,9 +1034,9 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) required = snprintf(buf, sizeof(buf), "upstream.%s", arg[1]); if ((size_t)required >= sizeof(buf)) { - upslogx(LOG_WARNING, "%s: truncated DELCMD command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, buf, (size_t)required, sizeof(buf)); + upslogx(LOG_WARNING, "%s: truncated DELCMD command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), buf); } ups_del_cmd(ups, buf); @@ -1052,9 +1052,9 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) required = snprintf(buf, sizeof(buf), "upstream.%s", arg[1]); if ((size_t)required >= sizeof(buf)) { - upslogx(LOG_WARNING, "%s: truncated ADDCMD command [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, buf, (size_t)required, sizeof(buf)); + upslogx(LOG_WARNING, "%s: truncated ADDCMD command size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), buf); } ups_add_cmd(ups, buf); @@ -1213,9 +1213,9 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) msgbuf[len - 1] = '\0'; } if ((size_t)len >= sizeof(msgbuf)) { - upsdebugx(6, "%s: truncated DBG output [%s]" - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, msgbuf, (size_t)len, sizeof(msgbuf)); + upsdebugx(6, "%s: truncated DBG output size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)len, sizeof(msgbuf), msgbuf); } upsdebugx(6, "%s: [%s]: ignored protocol line with %" PRIuSIZE " keyword(s): %s", @@ -2216,9 +2216,9 @@ static const char *rewrite_driver_prefix(const char *in, char *out, size_t outle required = snprintf(out, outlen, "upstream.%s", in); if ((size_t)required >= outlen) { - upslogx(LOG_WARNING, "%s: truncated variable name [%s] " - "size [%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]", - __func__, out, (size_t)required, outlen); + upslogx(LOG_WARNING, "%s: truncated variable name size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, outlen, out); } return out; From 53359b7eb21b6b185b709b9c2599ede4360abbe0 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 07:01:53 +0200 Subject: [PATCH 17/41] drivers/failover.c: safeguard ups_promote_primary against NULL or double promotion Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/failover.c b/drivers/failover.c index 506d02c556..7b8fbbbc11 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -1487,8 +1487,18 @@ static int ups_passes_status_filters(const ups_device_t *ups) return 1; } +/* Promote a UPS driver that is not NULL and not already the current primary */ static void ups_promote_primary(ups_device_t *ups) { + if (!ups || primary_ups == ups) { + upslogx(LOG_WARNING, "%s: Unsupported function call, " + "argument was either NULL or a UPS driver already declared as primary. " + "Please notify the NUT developers (on GitHub) to check this driver's code.", + __func__); + + return; + } + if (primary_ups) { ups_demote_primary(primary_ups); } From 9f78e4cff3b48e16da45fd7f14e6f15926e7990d Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 08:30:57 +0200 Subject: [PATCH 18/41] docs/man/failover.txt: polish documentation and add rationale Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 97 ++++++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 0f26bc9f2f..6152526407 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -49,14 +49,9 @@ When no suitable primary is available, a configurable fallback state is entered: - Raise `ALARM` and declare the data as stale - Raise `ALARM` and set forced shutdown (`FSD`) -How the UPS are connected (be it corded, networked, ...) to the machine does not -matter, `failover` is also not reliant on linkman:upsd[8] itself running. In -principle, it could even be used on multiple drivers connected to the same UPS, -but do note that any missing data would not be multiplexed between the drivers. - -In summary, `failover` simplifies multi UPS driver setups by consolidating -monitoring and control into a single NUT-visible "device", reducing complexity -and ensuring seamless transitions in high-availability environments. +Different communication media can be used to connect to individual UPS drivers +(e.g., USB, Serial, Ethernet). `failover` communicates directly at the socket +level and therefore does not rely on linkman:upsd[8] being active. EXTRA ARGUMENTS --------------- @@ -98,49 +93,57 @@ after exceeding `maxconnfails`. Defaults to 15 seconds. Optional. Defines the behavior when no suitable primary UPS driver is found after `noprimarytime` has elapsed. Defaults to 0. -- `0`: *Do not demote the last primary, but mark its data stale.* This is +- `0`: *Do not demote the last primary, but mark its data as stale.* This is similar to how a regular UPS driver would behave when it loses its connection to the target UPS device. linkman:upsmon[8] will act on the last known (online or not) status, and decide itself whether that UPS should be considered critical. -- `1`: *Demote the primary, raise `ALARM` and mark the data stale after an +- `1`: *Demote the primary, raise `ALARM`, and mark the data as stale after an additional few seconds have elapsed (ensuring full propagation).* This will -force monitoring linkman:upsmon[8] to see a previously in an alarm state device -having lost its connection and consider the UPS driver critical, possibly -resulting in forced shutdown (`FSD`) by depletion of `MINSUPPLIES`. +cause linkman:upsmon[8] to detect that a device previously in an alarm state has +lost its connection, consider the UPS driver critical, and possibly trigger a +forced shutdown (`FSD`) due to depletion of `MINSUPPLIES`. -- `2`: *Demote the primary, raise `ALARM` and set immediate `FSD`.* This will -set `FSD` from the driver side and omit linkman:upsmon[8] to raise it itself. -This mode is for setups where immediate shutdown is warranted, regardless of -anything else, and getting `FSD` out to the clients as fast as just possible. +- `2`: *Demote the primary, raise `ALARM`, and immediately set `FSD`.* This will +set `FSD` from the driver side and preempt linkman:upsmon[8] from raising it +itself. This mode is for setups where immediate shutdown is warranted, +regardless of anything else, and getting `FSD` out to the clients as fast as +just possible. -*strictfiltering*='0|1':: Optional. If set to 1, only UPS matching the +*strictfiltering*='0|1':: Optional. If set to 1, only UPS drivers matching the configured status filters are considered for promotion to primary. If set to 0, the hard-coded default logic is also considered when no status filters match -(read more about this further down). Defaults to 0. +(read more about this in the section `PRIORITIES`). Defaults to 0. *status_have_any*='OL,CHRG,...':: Optional. If any of these comma-separated tokens are present in a UPS driver's -`ups.status`, it qualifies for promotion to primary. Defaults to unset. +`ups.status`, it passes this status filtering criteria. Defaults to unset. *status_have_all*='OL,CHRG,...':: Optional. All listed comma-separated tokens must be present in `ups.status` for -the UPS driver to be eligible for promotion to primary. Defaults to unset. +the UPS driver to pass this status filtering criteria. Defaults to unset. *status_nothave_any*='OB,OFF,...':: Optional. If any of these comma-separated tokens are present in `ups.status`, -the UPS driver is disqualified as a primary candidate. Defaults to unset. +the UPS driver does not pass this status filtering criteria. Defaults to unset. *status_nothave_all*='OB,LB,...':: Optional. If all of these comma-separated tokens are present in `ups.status`, -the UPS driver is disqualified as a primary candidate. Defaults to unset. +the UPS driver does not pass this status filtering criteria. Defaults to unset. + +NOTE: The `status_*` arguments are primarily intended to adjust the weighting of +UPS drivers, allowing some to be prioritized over others based on their status. +For example, a driver reporting `OL` might be preferred over one reporting +`ALARM OL`. While `strictfiltering` can be enabled, status filters are most +effective when used in combination with the default set of connectivity-based +`PRIORITIES`. For more details, see the respective section further below. IMPLEMENTATION -------------- The port argument in the linkman:ups.conf[5] should reference the local driver -sockets (or Windows named pipes) that the "real" UPS drivers are using. A most -basic defaults setup with multiple drivers could look like this: +sockets (or Windows named pipes) that the "real" UPS drivers are using. A basic +default setup with multiple drivers could look like this: ------ [realups] @@ -167,16 +170,16 @@ is to clearly separate the upstream commands from those of `failover` itself. For your convenience, additional administrative commands are exposed to directly influence and override the primary election process, e.g. for maintenance: -- `.force.ignore [seconds]` will prevent that UPS driver from ever -becoming primary within the given timeframe, or permanently in case of a -negative value. A value of 0 resets the override state back to disabled. +- `.force.ignore [seconds]` prevents the specified UPS driver from +being selected as primary for the given duration, or permanently if a negative +value is used. A value of `0` resets this override and re-enables selection. -- `.force.primary [seconds]` will force that UPS driver to the -highest priority within the given timeframe, or permanently in case of a -negative value. A value of 0 resets the override state back to disabled. +- `.force.primary [seconds]` forces the specified UPS driver to be +treated with the highest priority for the given duration, or permanently if a +negative value is used. A value of `0` resets this override. -If either command is executed without any argument, active overrides for that -UPS driver will be reset and returned to their default state of being disabled. +Calling either command without an argument has the same effect as passing `0`, +but only for that specific override — it does not affect the other. PRIORITIES ---------- @@ -208,6 +211,30 @@ NOTE: The base requirement for any election is the UPS socket being connectable and the UPS driver having published at least one full batch of data during its lifetime. UPS driver not fulfilling that requirement are always disqualified. +RATIONALE +--------- + +In complex power environments, presenting a single, consistent source of UPS +information to linkman:upsmon[8] is sometimes preferable to monitoring multiple +independent drivers directly. The `failover` driver serves as a bridge, allowing +linkman:upsmon[8] to make decisions based on the most suitable available data, +without having to interpret conflicting inputs or degraded sources. + +Originally designed for use cases such as dual-PSU systems or redundant +communication paths to a single UPS, `failover` also supports more advanced +setups — for example, when multiple UPSes feed a shared downstream load (via +STS/ATS switches), or when drivers vary in reliability. In these cases, the +driver can be combined with external logic or scripting to dynamically adjust +primary selection and facilitate graceful degradation. Such setups may also +benefit from further integration with the `clone` family of drivers, such as +linkman:clone[8] or linkman:clone-outlet[8], for greater granularity and +monitoring control down to the outlet level. + +Ultimately, `failover` enables more nuanced power monitoring and control than +binary online/offline logic alone, allowing administrators to respond to +degraded conditions early — before they escalate into critical events or require +linkman:upsmon[8] to take action. + AUTHOR ------ @@ -221,7 +248,9 @@ linkman:upsrw[1], linkman:ups.conf[5], linkman:upsc[8], linkman:upsmon[8], -linkman:nutupsdrv[8] +linkman:nutupsdrv[8], +linkman:clone[8], +linkman:clone-outlet[8] Internet Resources: ~~~~~~~~~~~~~~~~~~~ From 022342ca679638dfacab44f27b517513c4fd7d2f Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 08:36:48 +0200 Subject: [PATCH 19/41] drivers/failover.c: remove progname from non fatal log message Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 7b8fbbbc11..ac9cc23a56 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -154,8 +154,8 @@ void upsdrv_initinfo(void) } if (!ups_alive_count) { - upslogx(LOG_WARNING, "%s: %s: none of the tracked UPS drivers were connectable", - progname, __func__); + upslogx(LOG_WARNING, "%s: none of the tracked UPS drivers were connectable", + __func__); } status_init(); From a6fda90fb460aceee292f483e2d2f522ad84eacd Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 08:54:02 +0200 Subject: [PATCH 20/41] docs/man/failover.txt: make hyphens consistent Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 6152526407..58f269c690 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -4,7 +4,7 @@ FAILOVER(8) NAME ---- -failover - UPS Failover Driver +failover — UPS Failover Driver SYNOPSIS -------- From 22c84654c01148ff07475955972ba98f4ad02fcf Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 11:50:22 +0200 Subject: [PATCH 21/41] docs/man/failover.txt: add note to factor in network or lock-picking delays for inittime Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 58f269c690..3abf9d49d6 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -66,7 +66,9 @@ underlying UPS drivers to be tracked. Entries must follow the format *inittime*='seconds':: Optional. Sets a grace period after driver startup during which the absence of a -primary UPS is tolerated. This allows time for underlying drivers to initialize. +primary is tolerated. This allows time for underlying drivers to initialize. For +networked connections or drivers that require "lock-picking" their communication +protocol, consider increasing this value to accommodate potential longer delays. Defaults to 30 seconds. *deadtime*='seconds':: From 44cabf377cb12277e9256436859ff025ad253083 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 11:55:50 +0200 Subject: [PATCH 22/41] docs/man/failover.txt: add limitations Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 3abf9d49d6..1e0cf15c4c 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -237,6 +237,13 @@ binary online/offline logic alone, allowing administrators to respond to degraded conditions early — before they escalate into critical events or require linkman:upsmon[8] to take action. +LIMITATIONS +----------- + +When using `failover` for redundancy between multiple UPS drivers connected to +the same underlying UPS device, data is not multiplexed between the drivers. As +a result, some data points may be available in some drivers but not in others. + AUTHOR ------ From 5321a556c860498e971c4ed335e413521ce4de09 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Fri, 23 May 2025 14:20:28 +0200 Subject: [PATCH 23/41] docs/man/failover.txt: add 3rd party tool use case for rationale Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 1e0cf15c4c..8275f84ff1 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -232,7 +232,13 @@ benefit from further integration with the `clone` family of drivers, such as linkman:clone[8] or linkman:clone-outlet[8], for greater granularity and monitoring control down to the outlet level. -Ultimately, `failover` enables more nuanced power monitoring and control than +Additionally, in more niche scenarios, some third-party NUT integrations or +graphical interfaces may be limited to monitoring a single UPS device. In such +cases, `failover` can help by exposing only the most relevant or +highest-priority data source, allowing those tools to operate within their +constraints without missing critical information. + +Ultimately, this driver enables more nuanced power monitoring and control than binary online/offline logic alone, allowing administrators to respond to degraded conditions early — before they escalate into critical events or require linkman:upsmon[8] to take action. From 953d36896f5c34ebf1df352177f561c6cbb0cb85 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Sat, 24 May 2025 06:48:42 +0200 Subject: [PATCH 24/41] drivers/Makefile.am: add failover.h for dists Signed-off-by: Sebastian Kuttnig --- drivers/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 1610501f61..8c1c9adcc4 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -412,7 +412,7 @@ nutdrv_qx_SOURCES += $(NUTDRV_QX_SUBDRIVERS) dist_noinst_HEADERS = \ apc_modbus.h apc-mib.h apc-iem-mib.h apc-hid.h arduino-hid.h baytech-mib.h baytech-rpc3nc-mib.h bcmxcp.h bcmxcp_ser.h \ bcmxcp_io.h belkin.h belkin-hid.h bestpower-mib.h blazer.h cps-hid.h dstate.h \ - dummy-ups.h explore-hid.h gamatronic.h genericups.h \ + dummy-ups.h explore-hid.h failover.h gamatronic.h genericups.h \ generic_gpio_common.h generic_gpio_libgpiod.h \ hidparser.h hidtypes.h ietf-mib.h libhid.h libshut.h nut_libusb.h liebert-hid.h \ main.h mge-hid.h mge-mib.h mge-utalk.h \ From 12e7da48287adda5e498ca2ca0a02bdba7c875ed Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Sat, 24 May 2025 06:49:13 +0200 Subject: [PATCH 25/41] docs/man/failover.txt: fix incompatible characters Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 8275f84ff1..5955156bdc 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -4,7 +4,7 @@ FAILOVER(8) NAME ---- -failover — UPS Failover Driver +failover - UPS Failover Driver SYNOPSIS -------- @@ -181,7 +181,7 @@ treated with the highest priority for the given duration, or permanently if a negative value is used. A value of `0` resets this override. Calling either command without an argument has the same effect as passing `0`, -but only for that specific override — it does not affect the other. +but only for that specific override - it does not affect the other. PRIORITIES ---------- @@ -224,7 +224,7 @@ without having to interpret conflicting inputs or degraded sources. Originally designed for use cases such as dual-PSU systems or redundant communication paths to a single UPS, `failover` also supports more advanced -setups — for example, when multiple UPSes feed a shared downstream load (via +setups - for example, when multiple UPSes feed a shared downstream load (via STS/ATS switches), or when drivers vary in reliability. In these cases, the driver can be combined with external logic or scripting to dynamically adjust primary selection and facilitate graceful degradation. Such setups may also @@ -240,7 +240,7 @@ constraints without missing critical information. Ultimately, this driver enables more nuanced power monitoring and control than binary online/offline logic alone, allowing administrators to respond to -degraded conditions early — before they escalate into critical events or require +degraded conditions early - before they escalate into critical events or require linkman:upsmon[8] to take action. LIMITATIONS From 825edd25e58e8535b202260c27bda9a05649eb14 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 26 May 2025 12:17:49 +0200 Subject: [PATCH 26/41] drivers/failover.c: safer string to numeric conversions, improved argument handling Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 167 +++++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 66 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index ac9cc23a56..bb5ae5485d 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -112,7 +112,8 @@ static void ups_free_ups_state(ups_device_t *ups); static void ups_free_var_state(ups_var_t *var); static const char *rewrite_driver_prefix(const char *in, char *out, size_t outlen); static int split_socket_name(const char *input, char **driver, char **ups); -static void csv_arg_to_array(const char *argname, const char *argcsv, char ***array, size_t *countvar); +static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int defval, int min, int max); +static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar); static inline void ups_set_flag(ups_device_t *ups, ups_flags_t flag); static inline void ups_clear_flag(ups_device_t *ups, ups_flags_t flag); @@ -519,73 +520,33 @@ static int setvar(const char *varname, const char *val) static void handle_arguments(void) { - const char *val = NULL; - parse_port_argument(); parse_status_filters(); - val = getval("inittime"); - if (val) { - arg_init_timeout = atoi(val); - upsdebugx(1, "%s: set 'inittime' to [%d] from configuration", - __func__, arg_init_timeout); - } + str_arg_to_int("inittime", getval("inittime"), + &arg_init_timeout, DEFAULT_INIT_TIMEOUT, 0, INT_MAX); - val = getval("deadtime"); - if (val) { - arg_dead_timeout = atoi(val); - upsdebugx(1, "%s: set 'deadtime' to [%d] from configuration", - __func__, arg_dead_timeout); - } + str_arg_to_int("deadtime", getval("deadtime"), + &arg_dead_timeout, DEFAULT_DEAD_TIMEOUT, 0, INT_MAX); - val = getval("relogtime"); - if (val) { - arg_relog_timeout = atoi(val); - upsdebugx(1, "%s: set 'relogtime' to [%d] from configuration", - __func__, arg_relog_timeout); - } + str_arg_to_int("relogtime", getval("relogtime"), + &arg_relog_timeout, DEFAULT_RELOG_TIMEOUT, 0, INT_MAX); - val = getval("noprimarytime"); - if (val) { - arg_noprimary_timeout = atoi(val); - upsdebugx(1, "%s: set 'noprimarytime' to [%d] from configuration", - __func__, arg_noprimary_timeout); - } + str_arg_to_int("noprimarytime", getval("noprimarytime"), + &arg_noprimary_timeout, DEFAULT_NO_PRIMARY_TIMEOUT, 0, INT_MAX); - val = getval("maxconnfails"); - if (val) { - arg_maxconnfails = atoi(val); - upsdebugx(1, "%s: set 'maxconnfails' to [%d] from configuration", - __func__, arg_maxconnfails); - } + str_arg_to_int("maxconnfails", getval("maxconnfails"), + &arg_maxconnfails, DEFAULT_MAX_CONNECT_FAILS, 0, INT_MAX); - val = getval("coolofftime"); - if (val) { - arg_coolofftimeout = atoi(val); - upsdebugx(1, "%s: set 'coolofftime' to [%d] from configuration", - __func__, arg_coolofftimeout); - } + str_arg_to_int("coolofftime", getval("coolofftime"), + &arg_coolofftimeout, DEFAULT_CONNECTION_COOLOFF, 0, INT_MAX); - val = getval("fsdmode"); - if (val) { - arg_fsdmode = atoi(val); - if (arg_fsdmode >= 0 && arg_fsdmode <= 2) { - upsdebugx(1, "%s: set 'fsdmode' to [%d] from configuration", - __func__, arg_fsdmode); - } else { - upslogx(LOG_ERR, "%s: invalid 'fsdmode' of [%d] from configuration, " - "set to the default 'fsdmode' value of [%d] instead", - __func__, arg_fsdmode, DEFAULT_FSD_MODE); - arg_fsdmode = DEFAULT_FSD_MODE; - } - } + str_arg_to_int("fsdmode", getval("fsdmode"), + &arg_fsdmode, DEFAULT_FSD_MODE, 0, 2); + + str_arg_to_int("strictfiltering", getval("strictfiltering"), + &arg_strict_filtering, DEFAULT_STRICT_FILTERING, 0, 1); - val = getval("strictfiltering"); - if (val) { - arg_strict_filtering = atoi(val); - upsdebugx(1, "%s: set 'strictfiltering' to [%d] from configuration", - __func__, arg_strict_filtering); - } } static void parse_port_argument(void) @@ -594,6 +555,11 @@ static void parse_port_argument(void) char *token = NULL; const char *str = device_path; + if (!device_path) { + fatalx(EXIT_FAILURE, "%s: %s: device_path ('port' argument) is NULL", + progname, __func__); + } + tmp = xstrdup(str); token = strtok(tmp, ","); @@ -602,6 +568,12 @@ static void parse_port_argument(void) str_trim_space(token); + if (*token == '\0') { + token = strtok(NULL, ","); + + continue; + } + new_ups = xcalloc(1, sizeof(**ups_list)); new_ups->socketname = xstrdup(token); @@ -632,6 +604,7 @@ static void parse_port_argument(void) } free(tmp); + tmp = NULL; } static void parse_status_filters(void) @@ -1111,12 +1084,18 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) /* SETAUX */ if (!strcasecmp(arg[0], "SETAUX")) { + long auxval; + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + if (str_to_long(arg[2], &auxval, 10)) { upsdebugx(6, "%s: [%s]: got SETAUX [%s] from UPS driver", __func__, ups->socketname, arg[1]); - - ups_set_var_aux(ups, varptr, atol(arg[2])); + ups_set_var_aux(ups, varptr, auxval); + } else { + upsdebugx(5, "%s: [%s]: got non-numeric SETAUX [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + } return 1; } @@ -1177,24 +1156,38 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) /* DELRANGE */ if (!strcasecmp(arg[0], "DELRANGE")) { + int minval; + int maxval; + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + if (str_to_int(arg[2], &minval, 10) && str_to_int(arg[3], &maxval, 10)) { upsdebugx(6, "%s: [%s]: got DELRANGE [%s] from UPS driver", __func__, ups->socketname, arg[1]); - - ups_del_range(ups, varptr, atoi(arg[2]), atoi(arg[3])); + ups_del_range(ups, varptr, minval, maxval); + } else { + upsdebugx(5, "%s: [%s]: got non-numeric DELRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + } return 1; } /* ADDRANGE */ if (!strcasecmp(arg[0], "ADDRANGE")) { + int minval; + int maxval; + varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); + if (str_to_int(arg[2], &minval, 10) && str_to_int(arg[3], &maxval, 10)) { upsdebugx(6, "%s: [%s]: got ADDRANGE [%s] from UPS driver", __func__, ups->socketname, arg[1]); - - ups_add_range(ups, varptr, atoi(arg[2]), atoi(arg[3])); + ups_add_range(ups, varptr, minval, maxval); + } else { + upsdebugx(5, "%s: [%s]: got non-numeric ADDRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + } return 1; } @@ -2265,12 +2258,48 @@ static int split_socket_name(const char *input, char **driver, char **ups) return 1; } -static void csv_arg_to_array(const char *argname, const char *argcsv, char ***array, size_t *countvar) +static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int defval, int min, int max) +{ + if (!arg || !argval || !destvar) { + return 0; + } + + if (str_to_int(argval, destvar, 10)) { + if (!(min == -1 && max == -1) && (*destvar < min || *destvar > max)) { + upslogx(LOG_ERR, "%s: '%s' value [%d] out of range [%d..%d], " + "set to the default '%s' value of [%d] instead", + __func__, arg, *destvar, min, max, arg, defval); + + *destvar = defval; + + return 0; + } + + upsdebugx(1, "%s: set '%s' to [%d] from configuration", + __func__, arg, *destvar); + + return 1; + } + + upslogx(LOG_ERR, "%s: invalid '%s' of [%s] from configuration, " + "set to the default '%s' value of [%d] instead", + __func__, arg, argval, arg, defval); + + *destvar = defval; + + return 0; +} + +static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar) { char *tmp = NULL; char *token = NULL; char *str = NULL; + if (!arg || !array || !countvar) { + return; + } + if (!argcsv) { *array = NULL; *countvar = 0; @@ -2284,6 +2313,12 @@ static void csv_arg_to_array(const char *argname, const char *argcsv, char ***ar while (token) { str_trim_space(token); + if (*token == '\0') { + token = strtok(NULL, ","); + + continue; + } + str = xstrdup(token); *array = xrealloc(*array, sizeof(**array) * (*countvar + 1)); @@ -2291,7 +2326,7 @@ static void csv_arg_to_array(const char *argname, const char *argcsv, char ***ar (*countvar)++; upsdebugx(1, "%s: added [%s] to [%s] from configuration", - __func__, str, argname); + __func__, str, arg); token = strtok(NULL, ","); } From 2c68099b281091c045fc43e52e9ac8fa591de995 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 26 May 2025 14:16:58 +0200 Subject: [PATCH 27/41] drivers/failover.c: remove magic -1 from str_arg_to_int(), use INT_MIN/MAX instead Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/failover.c b/drivers/failover.c index bb5ae5485d..62c9e76a93 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -2265,7 +2265,7 @@ static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int } if (str_to_int(argval, destvar, 10)) { - if (!(min == -1 && max == -1) && (*destvar < min || *destvar > max)) { + if ((min != INT_MIN && *destvar < min) || (max != INT_MAX && *destvar > max)) { upslogx(LOG_ERR, "%s: '%s' value [%d] out of range [%d..%d], " "set to the default '%s' value of [%d] instead", __func__, arg, *destvar, min, max, arg, defval); From 95ea397ef982d518c04c7e6fe46b48696dc85fac Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 22 May 2025 21:16:32 +0200 Subject: [PATCH 28/41] scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in: add support for "drivers=..." media type for dependencies, and failover driver using that [#2962] Signed-off-by: Jim Klimov --- .../upsdrvsvcctl/nut-driver-enumerator.sh.in | 104 ++++++++++++++++-- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in index 2641bd2c61..e198996765 100644 --- a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in +++ b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in @@ -118,6 +118,9 @@ DEPSVC_NET_FULL_SYSTEMD="network-online.target systemd-resolved.service ifplugd. DEPREQ_NET_FULL_SYSTEMD="Wants" DEPSVC_NET_LOCAL_SYSTEMD="network.target" DEPREQ_NET_LOCAL_SYSTEMD="Wants" +# For dependency of one driver on anoter (clone*, dummy, failover): +DEPREQ_DRV_LOCAL_SYSTEMD="Wants" +SVCNAMESEP_SYSTEMD="@" SVCNAME_SYSTEMD="nut-driver" # Some or all of these FMRIs may be related to dynamically changing hardware @@ -135,6 +138,9 @@ DEPSVC_NET_FULL_SMF="svc:/network/physical svc:/milestone/name-services" DEPREQ_NET_FULL_SMF="optional_all" DEPSVC_NET_LOCAL_SMF="svc:/network/loopback:default" DEPREQ_NET_LOCAL_SMF="optional_all" +# For dependency of one driver on anoter (clone*, dummy, failover): +DEPREQ_DRV_LOCAL_SMF="require_any" +SVCNAMESEP_SMF=":" SVCNAME_SMF="svc:/system/power/nut-driver" [ -z "${NUT_DRIVER_ENUMERATOR_CONF-}" ] && \ @@ -636,17 +642,21 @@ upsconf_getDriverMedia() { printf '%s\n%s\n' "$CURR_DRV" "" ; return ;; esac ;; - *dummy*|*clone*) # May be networked (proxy to remote NUT) + *dummy*) # May be networked (proxy to remote NUT) CURR_PORT="`upsconf_getPort "$1"`" || CURR_PORT="" case "$CURR_PORT" in *@localhost|*@|*@127.0.0.1|*@::1) - printf '%s\n%s\n' "$CURR_DRV" "network-localhost" ; return ;; - *@*) + printf '%s\n%s\n' "$CURR_DRV" "network-localhost,drivers=`echo "${CURR_PORT}" | sed 's,@.*$,,'`" ; return ;; + *@"`hostname`"*) # Technically also local host, but via public host name (so networking must be up) + printf '%s\n%s\n' "$CURR_DRV" "network,drivers=`echo "${CURR_PORT}" | sed 's,@.*$,,'`" ; return ;; + *@*) # Might be local host via public IP address, but harder to detect portably printf '%s\n%s\n' "$CURR_DRV" "network" ; return ;; - *) + *) # Assume DEV/SEQ file: printf '%s\n%s\n' "$CURR_DRV" "" ; return ;; esac ;; + *clone*|failover) # Talk to another driver via local socket/pipe: + printf '%s\n%s\n' "$CURR_DRV" "drivers=${CURR_PORT}" ; return ;; # FIXME: other modbus? sysfs like INA219? GPIO? Other local devices? *) printf '%s\n%s\n' "$CURR_DRV" "" ; return ;; esac @@ -658,6 +668,22 @@ upsconf_getMedia() { return 0 } +upsconf_list_dev_drv_socket_checksum() { + # NOTE: Output column order matters, it is parsed e.g. to check + # for driver-on-driver dependencies when adding services + upslist_readFile_once && [ "${#UPSLIST_FILE}" != 0 ] \ + || { echo "No devices detected in '$UPSCONF'" >&2 ; return 1 ; } + # Use the section-driver-port subset + if [ x"${AVOID_REPARSE}" != xyes ] ; then + upslist_normalizeFile_once || return # Propagate errors upwards + fi + for _DEV in $UPSLIST_FILE ; do + _DRV="`upsconf_getDriver "${_DEV}"`" + _MD5="`upsconf_getSection_MD5 "${_DEV}"`" + printf '%s\t%s\t%s\t%s\n' "${_DEV}" "${_DRV}" "${_DRV}-${_DEV}" "${_MD5}" + done +} + upsconf_debug() { _DRV="`upsconf_getDriver "$1"`" _PRT="`upsconf_getPort "$1"`" @@ -720,20 +746,47 @@ smf_registerInstance() { DEPREQ="" _MED="`upsconf_getMedia "$DEVICE"`" case "${_MED}" in - usb) + usb|usb,*) DEPSVC="$DEPSVC_USB_SMF" DEPREQ="$DEPREQ_USB_SMF" ;; - network-localhost) + network-localhost|network-localhost,*) DEPSVC="$DEPSVC_NET_LOCAL_SMF" DEPREQ="$DEPREQ_NET_LOCAL_SMF" ;; - network) + network|network,*) DEPSVC="$DEPSVC_NET_FULL_SMF" DEPREQ="$DEPREQ_NET_FULL_SMF" ;; - serial) ;; + serial|serial,*) ;; + drivers=*) ;; # Handled just below '') ;; + # FIXME: modbus? sysfs like INA219? GPIO? Other local devices? *) echo "WARNING: Unexpected NUT media type ignored: '${_MED}'" >&2 ;; esac + case "${_MED}" in + drivers=*|*,drivers=*) + OTHERLIST="`upsconf_list_dev_drv_socket_checksum`" || OTHERLIST="" + for DEPDRV in `echo "${_MED}" | sed -e 's/^\(.*,d\|d\)rivers=//' -e 's/,/ /g'` ; do + case "${DEPDRV}" in + *-*) # May be "drivername-upsname", where either sub-string + # may have dashes inside too; try to find the right one: + OTHERDEV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $1 }'`" && \ + OTHERDRV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $2 }'`" && \ + [ x"${DEPDRV}" = x"${OTHERDRV}-${OTHERDEV}" ] && \ + [ x"${OTHERDEV}" != x"${DEVICE}" ] \ + || { OTHERDEV="" ; OTHERDRV="" ; } + ;; + *) OTHERDEV="${DEPDRV}" ;; + esac + if [ x"${OTHERDEV}" = x ] || ! (echo "$OTHERLIST" | grep -E "^${OTHERDEV}\t") >/dev/null ; then + echo "WARNING: Device ${DEVICE} depends on another ${DEPDRV} but we did not find a config section for it" >&2 + else + DEPSVC="$DEPSVC ${SVCNAME_SMF}${SVCNAMESEP_SMF}`smf_validInstanceName ${OTHERDEV}`" + fi + done + if [ x"`echo "$DEPSVC" | tr -d ' '`" != x ] ; then DEPREQ="$DEPREQ_DRV_FULL_SMF" ; fi + ;; + esac + TARGET_FMRI="nut-driver:$SVCINST" if [ -n "$DEPSVC" ]; then [ -n "$DEPREQ" ] || DEPREQ="optional_all" @@ -944,22 +997,49 @@ systemd_registerInstance() { DEPREQ="" _MED="`upsconf_getMedia "$DEVICE"`" case "${_MED}" in - usb) + usb|usb,*) DEPSVC="$DEPSVC_USB_SYSTEMD" DEPREQ="$DEPREQ_USB_SYSTEMD" ;; - network-localhost) + network-localhost|network-localhost,*) DEPSVC="$DEPSVC_NET_LOCAL_SYSTEMD" DEPREQ="$DEPREQ_NET_LOCAL_SYSTEMD" ;; - network) + network|network,*) DEPSVC="$DEPSVC_NET_FULL_SYSTEMD" DEPREQ="$DEPREQ_NET_FULL_SYSTEMD" ;; - serial) + serial|serial,*) DEPSVC="$DEPSVC_SERIAL_SYSTEMD" DEPREQ="$DEPREQ_SERIAL_SYSTEMD" ;; + drivers=*) ;; # Handled just below '') ;; # FIXME: modbus? sysfs like INA219? GPIO? Other local devices? *) echo "WARNING: Unexpected NUT media type ignored: '${_MED}'" >&2 ;; esac + + case "${_MED}" in + drivers=*|*,drivers=*) + OTHERLIST="`upsconf_list_dev_drv_socket_checksum`" || OTHERLIST="" + for DEPDRV in `echo "${_MED}" | sed -e 's/^\(.*,d\|d\)rivers=//' -e 's/,/ /g'` ; do + case "${DEPDRV}" in + *-*) # May be "drivername-upsname", where either sub-string + # may have dashes inside too; try to find the right one: + OTHERDEV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $1 }'`" && \ + OTHERDRV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $2 }'`" && \ + [ x"${DEPDRV}" = x"${OTHERDRV}-${OTHERDEV}" ] && \ + [ x"${OTHERDEV}" != x"${DEVICE}" ] \ + || { OTHERDEV="" ; OTHERDRV="" ; } + ;; + *) OTHERDEV="${DEPDRV}" ;; + esac + if [ x"${OTHERDEV}" = x ] || ! (echo "$OTHERLIST" | grep -E "^${OTHERDEV}\t") >/dev/null ; then + echo "WARNING: Device ${DEVICE} depends on another ${DEPDRV} but we did not find a config section for it" >&2 + else + DEPSVC="$DEPSVC ${SVCNAME_SYSTEMD}${SVCNAMESEP_SYSTEMD}`systemd_validInstanceName ${OTHERDEV}`" + fi + done + if [ x"`echo "$DEPSVC" | tr -d ' '`" != x ] ; then DEPREQ="$DEPREQ_DRV_FULL_SYSTEMD" ; fi + ;; + esac + if [ -n "$DEPSVC" ]; then [ -n "$DEPREQ" ] || DEPREQ="#Wants" echo "Adding '$DEPREQ'+After dependency for '$SVCINST' on '$DEPSVC'..." From 3625b15752c3c566c7939ff8215bb89912712e5d Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 26 May 2025 14:54:40 +0200 Subject: [PATCH 29/41] drivers/failover.c: make csv_arg_to_array() more reusable Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 62c9e76a93..93568f1d21 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -113,7 +113,7 @@ static void ups_free_var_state(ups_var_t *var); static const char *rewrite_driver_prefix(const char *in, char *out, size_t outlen); static int split_socket_name(const char *input, char **driver, char **ups); static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int defval, int min, int max); -static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar); +static ssize_t csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar); static inline void ups_set_flag(ups_device_t *ups, ups_flags_t flag); static inline void ups_clear_flag(ups_device_t *ups, ups_flags_t flag); @@ -2290,21 +2290,19 @@ static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int return 0; } -static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar) +static ssize_t csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar) { char *tmp = NULL; char *token = NULL; char *str = NULL; + ssize_t count = 0; - if (!arg || !array || !countvar) { - return; + if (!arg || !argcsv || !array || !countvar) { + return -1; } - if (!argcsv) { - *array = NULL; - *countvar = 0; - - return; + if (*argcsv == '\0') { + return 0; } tmp = xstrdup(argcsv); @@ -2325,6 +2323,8 @@ static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, (*array)[*countvar] = str; (*countvar)++; + count++; + upsdebugx(1, "%s: added [%s] to [%s] from configuration", __func__, str, arg); @@ -2333,6 +2333,8 @@ static void csv_arg_to_array(const char *arg, const char *argcsv, char ***array, free(tmp); tmp = NULL; + + return count; } static inline void ups_set_flag(ups_device_t *ups, ups_flags_t flag) From 36204e8313383facf79e302be1c512e2b8641944 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 26 May 2025 15:20:00 +0200 Subject: [PATCH 30/41] scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in: report if other device uses unexpected driver [#2962] Signed-off-by: Jim Klimov --- .../upsdrvsvcctl/nut-driver-enumerator.sh.in | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in index e198996765..838181fb5f 100644 --- a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in +++ b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in @@ -773,7 +773,18 @@ smf_registerInstance() { OTHERDRV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $2 }'`" && \ [ x"${DEPDRV}" = x"${OTHERDRV}-${OTHERDEV}" ] && \ [ x"${OTHERDEV}" != x"${DEVICE}" ] \ - || { OTHERDEV="" ; OTHERDRV="" ; } + || { + for OTHERDEV in $UPSLIST_FILE ; do + case "${DEPDRV}" in + *-"${OTHERDEV}") + OTHERDRV="'`echo "${OTHERLIST}" | awk '($1 == "'"${OTHERDEV}"'") { print $2 }'`'" || OTHERDRV="" + echo "WARNING: Device ${DEVICE} depends on another as '${DEPDRV}', but the other possible device '${OTHERDEV}' uses an unexpected driver: ${OTHERDRV}" >&2 + ;; + esac + done + OTHERDEV="" + OTHERDRV="" + } ;; *) OTHERDEV="${DEPDRV}" ;; esac @@ -1026,7 +1037,18 @@ systemd_registerInstance() { OTHERDRV="`echo "${OTHERLIST}" | awk '($3 == "'"${DEPDRV}"'") { print $2 }'`" && \ [ x"${DEPDRV}" = x"${OTHERDRV}-${OTHERDEV}" ] && \ [ x"${OTHERDEV}" != x"${DEVICE}" ] \ - || { OTHERDEV="" ; OTHERDRV="" ; } + || { + for OTHERDEV in $UPSLIST_FILE ; do + case "${DEPDRV}" in + *-"${OTHERDEV}") + OTHERDRV="'`echo "${OTHERLIST}" | awk '($1 == "'"${OTHERDEV}"'") { print $2 }'`'" || OTHERDRV="" + echo "WARNING: Device ${DEVICE} depends on another as '${DEPDRV}', but the other possible device '${OTHERDEV}' uses an unexpected driver: ${OTHERDRV}" >&2 + ;; + esac + done + OTHERDEV="" + OTHERDRV="" + } ;; *) OTHERDEV="${DEPDRV}" ;; esac From 962bac44055fcab8b5bd86585ec8ae1ff194ebc8 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Mon, 26 May 2025 17:55:49 +0200 Subject: [PATCH 31/41] drivers/failover.c: use str_to_int() also in instcmd() Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 93568f1d21..b9c06de9c0 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -364,11 +364,20 @@ static int instcmd(const char *cmdname, const char *extra) if (!strcmp(subcmd, ".force.ignore")) { time_t now; + int ignoreval = 0; + + if (extra && !str_to_int(extra, &ignoreval, 10)) { + upslogx(LOG_INSTCMD_CONVERSION_FAILED, "%s: " + "conversion failed setting [force_ignore] to [%s] on [%s]", + __func__, extra, ups->socketname); + + return STAT_INSTCMD_CONVERSION_FAILED; + } time(&now); - ups->force_ignore = extra ? atoi(extra) : 0; - ups->force_ignore_time = ups->force_ignore ? now : 0; + ups->force_ignore = ignoreval; + ups->force_ignore_time = ignoreval ? now : 0; upslogx(LOG_NOTICE, "%s: set [force_ignore] to [%d] on [%s]", __func__, ups->force_ignore, ups->socketname); @@ -378,11 +387,20 @@ static int instcmd(const char *cmdname, const char *extra) if (!strcmp(subcmd, ".force.primary")) { time_t now; + int primaryval = 0; + + if (extra && !str_to_int(extra, &primaryval, 10)) { + upslogx(LOG_INSTCMD_CONVERSION_FAILED, "%s: " + "conversion failed setting [force_primary] to [%s] on [%s]", + __func__, extra, ups->socketname); + + return STAT_INSTCMD_CONVERSION_FAILED; + } time(&now); - ups->force_primary = extra ? atoi(extra) : 0; - ups->force_primary_time = ups->force_primary ? now : 0; + ups->force_primary = primaryval; + ups->force_primary_time = primaryval ? now : 0; upslogx(LOG_NOTICE, "%s: set [force_primary] to [%d] on [%s]", __func__, ups->force_primary, ups->socketname); From 784ce12d053bad32c1337599d10e41d9cbda3e90 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 06:54:00 +0200 Subject: [PATCH 32/41] drivers/failover.{c,h}, docs/man/failover.txt: use _sockfn() for one-shot connections This change removes the dependency on splitting socket names into UPS and driver names. We now rely on the user to provide valid sockets: if it connects, it connects. This enables the use of full paths in the port argument also, improving flexibility and enhancing scriptability. Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 6 ++-- drivers/failover.c | 67 +++---------------------------------------- drivers/failover.h | 2 -- 3 files changed, 7 insertions(+), 68 deletions(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 5955156bdc..7173bce6fc 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -59,10 +59,10 @@ EXTRA ARGUMENTS This driver supports the following settings: *port*='drivername-devicename,drivername2-devicename2,...':: -Required. Specifies the local socket names (or Windows named pipes) of the -underlying UPS drivers to be tracked. Entries must follow the format +Required. Specifies the local sockets (or Windows named pipes) of the underlying +UPS drivers to be tracked. Entries must either be a path or follow the format `drivername-devicename`, as used by NUT's internal socket naming convention -(e.g. `usbhid-ups-ups1`). Multiple entries are comma-separated with no spaces. +(e.g. `usbhid-ups-myups`). Multiple entries are comma-separated with no spaces. *inittime*='seconds':: Optional. Sets a grace period after driver startup during which the absence of a diff --git a/drivers/failover.c b/drivers/failover.c index b9c06de9c0..04117ae926 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -111,7 +111,6 @@ static void free_status_filters(void); static void ups_free_ups_state(ups_device_t *ups); static void ups_free_var_state(ups_var_t *var); static const char *rewrite_driver_prefix(const char *in, char *out, size_t outlen); -static int split_socket_name(const char *input, char **driver, char **ups); static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int defval, int min, int max); static ssize_t csv_arg_to_array(const char *arg, const char *argcsv, char ***array, size_t *countvar); @@ -321,16 +320,6 @@ void upsdrv_cleanup(void) ups_free_ups_state(ups); /* free status, vars, subvars + cmds */ - if (ups->name) { - free(ups->name); - ups->name = NULL; - } - - if (ups->drivername) { - free(ups->drivername); - ups->drivername = NULL; - } - if (ups->socketname) { free(ups->socketname); ups->socketname = NULL; @@ -448,7 +437,7 @@ static int instcmd(const char *cmdname, const char *extra) tv.tv_sec = CONN_CMD_TIMEOUT; tv.tv_usec = 0; - cmdret = upsdrvquery_oneshot(primary_ups->drivername, primary_ups->name, + cmdret = upsdrvquery_oneshot_sockfn(primary_ups->socketname, msgbuf, NULL, 0, &tv); if (cmdret >= 0) { @@ -511,7 +500,7 @@ static int setvar(const char *varname, const char *val) tv.tv_sec = CONN_CMD_TIMEOUT; tv.tv_usec = 0; - cmdret = upsdrvquery_oneshot(primary_ups->drivername, primary_ups->name, + cmdret = upsdrvquery_oneshot_sockfn(primary_ups->socketname, msgbuf, NULL, 0, &tv); if (cmdret >= 0) { @@ -595,22 +584,6 @@ static void parse_port_argument(void) new_ups = xcalloc(1, sizeof(**ups_list)); new_ups->socketname = xstrdup(token); - if (!split_socket_name(new_ups->socketname, &new_ups->drivername, &new_ups->name)) { - char buf[SMALLBUF]; - snprintf(buf, sizeof(buf), "%s", token); /* for fatalx */ - - free(new_ups->socketname); - free(new_ups); - free(tmp); - - fatalx(EXIT_FAILURE, "%s: %s: the 'port' argument has an invalid format, " - "[%s] is not a valid splittable socket name, please correct the argument", - progname, __func__, buf); - } else { - upsdebugx(3, "%s: [%s] was parsed into UPS driver [%s] and UPS [%s]", - __func__, new_ups->socketname, new_ups->drivername, new_ups->name); - } - ups_list = xrealloc(ups_list, sizeof(*ups_list) * (ups_count + 1)); ups_list[ups_count] = new_ups; ups_count++; @@ -686,16 +659,12 @@ static void export_driver_state(void) dstate_setinfo("driver.stats.total_drivers", "%" PRIuSIZE, ups_count); if (primary_ups) { - dstate_setinfo("driver.primary.upsname", "%s", primary_ups->name); - dstate_setinfo("driver.primary.drvname", "%s", primary_ups->drivername); - dstate_setinfo("driver.primary.sockname", "%s", primary_ups->socketname); + dstate_setinfo("driver.primary.socketname", "%s", primary_ups->socketname); dstate_setinfo("driver.primary.priority", "%d", primary_ups->priority); dstate_setinfo("driver.primary.stats.cmds", "%" PRIuSIZE, primary_ups->cmd_count); dstate_setinfo("driver.primary.stats.vars", "%" PRIuSIZE, primary_ups->var_count); } else { - dstate_delinfo("driver.primary.upsname"); - dstate_delinfo("driver.primary.drvname"); - dstate_delinfo("driver.primary.sockname"); + dstate_delinfo("driver.primary.socketname"); dstate_delinfo("driver.primary.priority"); dstate_delinfo("driver.primary.stats.cmds"); dstate_delinfo("driver.primary.stats.vars"); @@ -2248,34 +2217,6 @@ static const char *rewrite_driver_prefix(const char *in, char *out, size_t outle return in; } -static int split_socket_name(const char *input, char **driver, char **ups) -{ - size_t drv_len = 0; - size_t ups_len = 0; - const char *last_dash = strrchr(input, '-'); - - if (!input || !last_dash || last_dash == input || *(last_dash + 1) == '\0') { - *driver = NULL; - *ups = NULL; - - return 0; - } - - drv_len = last_dash - input; - ups_len = strlen(last_dash + 1); - - *driver = xmalloc(drv_len + 1); - *ups = xmalloc(ups_len + 1); - - snprintf(*driver, (drv_len + 1), "%.*s", (int)drv_len, input); - snprintf(*ups, (ups_len + 1), "%s", last_dash + 1); - - str_trim_space(*driver); - str_trim_space(*ups); - - return 1; -} - static int str_arg_to_int(const char *arg, const char *argval, int *destvar, int defval, int min, int max) { if (!arg || !argval || !destvar) { diff --git a/drivers/failover.h b/drivers/failover.h index a70b4a654f..78862c2e04 100644 --- a/drivers/failover.h +++ b/drivers/failover.h @@ -104,8 +104,6 @@ typedef struct { } status_filters_t; typedef struct { - char *name; - char *drivername; char *socketname; udq_pipe_conn_t *conn; From ff0865973794cc2a7b59ca671a9747ee6960deea Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 07:19:17 +0200 Subject: [PATCH 33/41] tests/nut-driver-enumerator-test.sh: reflect recent enumerator changes in test expectations [#2962] Signed-off-by: Sebastian Kuttnig --- tests/nut-driver-enumerator-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/nut-driver-enumerator-test.sh b/tests/nut-driver-enumerator-test.sh index dd91a58e0f..13b401c0af 100755 --- a/tests/nut-driver-enumerator-test.sh +++ b/tests/nut-driver-enumerator-test.sh @@ -211,7 +211,7 @@ testcase_upslist_debug() { run_testcase "List decided MEDIA and config checksums for all devices" 0 \ "INST: 68b329da9893e34099c7d8ad5cb9c940~[]: DRV='' PORT='' MEDIA='' SECTIONMD5='9a1f372a850f1ee3ab1fc08b185783e0' INST: 010cf0aed6dd49865bb49b70267946f5~[dummy-proxy]: DRV='dummy-ups ' PORT='remoteUPS@RemoteHost.local' MEDIA='network' SECTIONMD5='aff543fc07d7fbf83e81001b181c8b97' -INST: 1ea79c6eea3681ba73cc695f3253e605~[dummy-proxy-localhost]: DRV='dummy-ups ' PORT='localUPS@127.0.0.1' MEDIA='network-localhost' SECTIONMD5='73e6b7e3e3b73558dc15253d8cca51b2' +INST: 1ea79c6eea3681ba73cc695f3253e605~[dummy-proxy-localhost]: DRV='dummy-ups ' PORT='localUPS@127.0.0.1' MEDIA='network-localhost,drivers=localUPS' SECTIONMD5='73e6b7e3e3b73558dc15253d8cca51b2' INST: 76b645e28b0b53122b4428f4ab9eb4b9~[dummy1]: DRV='dummy-ups' PORT='file1.dev' MEDIA='' SECTIONMD5='9e0a326b67e00d455494f8b4258a01f1' INST: a293d65e62e89d6cc3ac6cb88bc312b8~[epdu-2]: DRV='netxml-ups' PORT='http://172.16.1.2' MEDIA='network' SECTIONMD5='0d9a0147dcf87c7c720e341170f69ed4' INST: 9a5561464ff8c78dd7cb544740ce2adc~[epdu-2-snmp]: DRV='snmp-ups' PORT='172.16.1.2' MEDIA='network' SECTIONMD5='2631b6c21140cea0dd30bb88b942ce3f' From 54c604bcd9b1261dcd891b9d91305e640f2aa068 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 28 May 2025 08:59:52 +0200 Subject: [PATCH 34/41] NEWS.adoc: mention NDE change to track inter-driver dependency [#2962] Signed-off-by: Jim Klimov --- NEWS.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 86c22b16f4..01cd36b907 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -165,6 +165,12 @@ https://github.com/networkupstools/nut/milestone/9 support for end-to-end tracked instant commands and also variable updating. [#2962] + - The `nut-driver-enumerator.sh` script (NDE) now internally tracks dependency + of one driver on another one that should be locally running to serve the + "original" data points (`clone`, `clone-outlet`, `dummy-ups`, `failover`). + It should create soft dependencies between respective service instances + to order their start-up sequence. [#2962] + - NUT Monitor GUI: * Ported Python 3 version to Qt6, now shipped alongside Qt5 for systems with either or both, maximizing compatibility with old and new setups. From 89a0db0f49eb085cb4734ba68f302aad143d7031 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 28 May 2025 09:11:33 +0200 Subject: [PATCH 35/41] docs/man/nut-driver-enumerator.txt: update intro, mention driver-on-drver dependencies [#2962] Signed-off-by: Jim Klimov --- docs/man/nut-driver-enumerator.txt | 37 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/man/nut-driver-enumerator.txt b/docs/man/nut-driver-enumerator.txt index cedc52f485..5e02cbb55c 100644 --- a/docs/man/nut-driver-enumerator.txt +++ b/docs/man/nut-driver-enumerator.txt @@ -19,16 +19,24 @@ SYNOPSIS DESCRIPTION ----------- -*nut-driver-enumerator.sh* implements the set-up and querying of the -mapping between NUT driver configuration sections for each individual -monitored device, and the operating system service management framework -service instances into which such drivers are wrapped for independent -execution and management (on platforms where NUT currently supports -this integration -- currently this covers Linux distributions with -systemd and systems derived from Solaris 10 codebase, including -proprietary Sun/Oracle Solaris and numerous open-source illumos -distributions with SMF). It may be not installed in packaging for -other operating systems. +The *nut-driver-enumerator.sh* (also known as "NDE") script implements the +set-up and querying of the mapping between NUT driver configuration sections +for each individual monitored device, and the service instances of an +operating system service management framework (on platforms where NUT already +supports this integration -- currently this covers Linux distributions with +systemd and systems derived from Solaris 10 codebase, including proprietary +Sun/Oracle Solaris and numerous open-source illumos distributions with SMF), +into which such drivers are wrapped for independent execution and management. +It may be not installed in packaging for other operating systems. + +With each NUT driver represented as a separate service instance, dependencies +can be defined (e.g. networked drivers must start after the network ability +appears in the OS, but USB/Serial drivers should not wait for that), and they +can fail or be brought into maintenance independently (unlike a monolithic +service based on linkman:upsdrvctl[8] requiring everything configured to be +started). For a few special drivers like linkman:dummy-ups[8], linkman:clone[8], +linkman:clone-outlet[8], and linkman:failover[8] this may also involve a +dependency between service instances of different NUT drivers themselves. This script provides a uniform interface for further NUT tools such as linkman:upsdrvsvcctl[8] to implement their logic as @@ -42,6 +50,15 @@ hides is the difference of rules for valid service instance names in various frameworks, as well as system tools and naming patterns involved. +Depending on the platform, the script may also be wrapped by different service +unit types to run automatically (e.g. upon system start-up, or regularly to +pick up changes of linkman:ups.conf[5] soon after it is edited, or integrated +with a file system monitor to be triggered when the configuration is changed). +Some of these modes make sense for use-cases with a rarely (if ever) changing +population of power devices, e.g. a home or small-office UPS monitored same +way for years at a time; others can help automate a data-center monitoring +system where device deployments (or discovery) can be much more dynamic. + COMMANDS -------- From ccaac91748527c03ae294356e9558dae3b572481 Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 11:25:43 +0200 Subject: [PATCH 36/41] drivers/failover.{c,h}: introduce checkruntime argument Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 94 ++++++++++++++++++++++++++++++++++++++++++---- drivers/failover.h | 1 + 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 04117ae926..ac3b6d1851 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -47,6 +47,7 @@ static int arg_maxconnfails = DEFAULT_MAX_CONNECT_FAILS; static int arg_coolofftimeout = DEFAULT_CONNECTION_COOLOFF; static int arg_fsdmode = DEFAULT_FSD_MODE; static int arg_strict_filtering = DEFAULT_STRICT_FILTERING; +static int arg_check_runtime = DEFAULT_CHECK_RUNTIME; static int init_time_elapsed; static int primaries_gone; @@ -97,6 +98,8 @@ static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd); static int ups_add_cmd(ups_device_t *ups, const char *val); static int ups_del_cmd(ups_device_t *ups, const char *val); +static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low); +static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode); static int ups_get_var_pos(const ups_device_t *ups, const char *key); static int ups_set_var(ups_device_t *ups, const char *key, const char *value); static int ups_del_var(ups_device_t *ups, const char *key); @@ -286,6 +289,12 @@ void upsdrv_makevartable(void) arg_strict_filtering); addvar(VAR_VALUE, "strictfiltering", buf); + snprintf(buf, sizeof(buf), + "Sets if runtime remaining variables should resolve ties for non-OL priorities " + "3 and lower (0: disabled, 1: runtime, 2: runtime low, 3: both) (default: %d)", + arg_check_runtime); + addvar(VAR_VALUE, "checkruntime", buf); + addvar(VAR_VALUE, "status_have_any", "Comma separated list of status tokens, any present qualifies " "the UPS driver for primary (default: unset)"); @@ -554,6 +563,8 @@ static void handle_arguments(void) str_arg_to_int("strictfiltering", getval("strictfiltering"), &arg_strict_filtering, DEFAULT_STRICT_FILTERING, 0, 1); + str_arg_to_int("checkruntime", getval("checkruntime"), + &arg_check_runtime, DEFAULT_CHECK_RUNTIME, 0, 3); } static void parse_port_argument(void) @@ -1076,8 +1087,8 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); if (str_to_long(arg[2], &auxval, 10)) { - upsdebugx(6, "%s: [%s]: got SETAUX [%s] from UPS driver", - __func__, ups->socketname, arg[1]); + upsdebugx(6, "%s: [%s]: got SETAUX [%s] from UPS driver", + __func__, ups->socketname, arg[1]); ups_set_var_aux(ups, varptr, auxval); } else { upsdebugx(5, "%s: [%s]: got non-numeric SETAUX [%s] from UPS driver", @@ -1149,8 +1160,8 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); if (str_to_int(arg[2], &minval, 10) && str_to_int(arg[3], &maxval, 10)) { - upsdebugx(6, "%s: [%s]: got DELRANGE [%s] from UPS driver", - __func__, ups->socketname, arg[1]); + upsdebugx(6, "%s: [%s]: got DELRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); ups_del_range(ups, varptr, minval, maxval); } else { upsdebugx(5, "%s: [%s]: got non-numeric DELRANGE [%s] from UPS driver", @@ -1168,8 +1179,8 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) varptr = rewrite_driver_prefix(arg[1], buf, sizeof(buf)); if (str_to_int(arg[2], &minval, 10) && str_to_int(arg[3], &maxval, 10)) { - upsdebugx(6, "%s: [%s]: got ADDRANGE [%s] from UPS driver", - __func__, ups->socketname, arg[1]); + upsdebugx(6, "%s: [%s]: got ADDRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); ups_add_range(ups, varptr, minval, maxval); } else { upsdebugx(5, "%s: [%s]: got non-numeric ADDRANGE [%s] from UPS driver", @@ -1296,6 +1307,8 @@ static ups_device_t *get_primary_candidate(void) size_t i = 0; size_t primaries = 0; int best_priority = 100; + int best_runtime = -1; + int best_runtime_low = -1; ups_device_t *best_choice = NULL; time(&now); @@ -1373,15 +1386,32 @@ static ups_device_t *get_primary_candidate(void) } if (priority >= 0) { + int rt = -1; + int rt_low = -1; + primaries++; + if (arg_check_runtime && priority >= PRIORITY_WEAK) { + ups_get_runtimes(ups, &rt, &rt_low); + } + if (priority < best_priority) { best_choice = ups; best_priority = priority; + best_runtime = rt; + best_runtime_low = rt_low; + } + else if (priority == best_priority && arg_check_runtime && priority >= PRIORITY_WEAK) { + /* All devices are not fully online and runtime checking is enabled, compare values: */ + if (has_better_runtime(rt, rt_low, best_runtime, best_runtime_low, arg_check_runtime)) { + best_choice = ups; + best_runtime = rt; + best_runtime_low = rt_low; + } } - upsdebugx(4, "%s: [%s]: is a primary candidate with priority [%d]", - __func__, ups->socketname, priority); + upsdebugx(4, "%s: [%s]: is candidate (priority [%d], runtime [%d]/[%d])", + __func__, ups->socketname, priority, rt, rt_low); } ups->priority = priority; @@ -1389,6 +1419,11 @@ static ups_device_t *get_primary_candidate(void) ups_primary_count = primaries; + if (best_choice) { + upsdebugx(4, "%s: [%s]: was selected (priority [%d], runtime [%d]/[%d])", + __func__, best_choice->socketname, best_priority, best_runtime, best_runtime_low); + } + return best_choice; } @@ -1704,6 +1739,49 @@ static int ups_del_cmd(ups_device_t *ups, const char *val) return 0; } +static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low) +{ + int tmp = -1; + int pos_rt = -1; + int pos_rt_low = -1; + + if (!ups || !runtime || !runtime_low) { + return; + } + + pos_rt = ups_get_var_pos(ups, "battery.runtime"); + pos_rt_low = ups_get_var_pos(ups, "battery.runtime.low"); + + *runtime = -1; + *runtime_low = -1; + + if (pos_rt >= 0 && str_to_int(ups->var_list[pos_rt]->value, &tmp, 10)) { + *runtime = tmp; + } + + if (pos_rt_low >= 0 && str_to_int(ups->var_list[pos_rt_low]->value, &tmp, 10)) { + *runtime_low = tmp; + } +} + +static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode) +{ + switch (mode) { + case 1: + /* compare runtime */ + return rt > best_rt; + case 2: + /* compare runtime low */ + return rt_low > best_rt_low; + case 3: + /* compare runtime + runtime low */ + return (rt > best_rt && rt_low > best_rt_low); + default: + /* invalid mode */ + return 0; + } +} + static int ups_get_var_pos(const ups_device_t *ups, const char *key) { size_t i = 0; diff --git a/drivers/failover.h b/drivers/failover.h index 78862c2e04..67d577b358 100644 --- a/drivers/failover.h +++ b/drivers/failover.h @@ -40,6 +40,7 @@ #define DEFAULT_NO_PRIMARY_TIMEOUT 15 #define DEFAULT_MAX_CONNECT_FAILS 5 #define DEFAULT_RELOG_TIMEOUT 5 +#define DEFAULT_CHECK_RUNTIME 1 #define DEFAULT_FSD_MODE 0 #define DEFAULT_STRICT_FILTERING 0 From c498dd37f343734fdb2d658e88ff269eca027e0d Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 11:26:27 +0200 Subject: [PATCH 37/41] docs/man/failover.txt, docs/nut.dict: introduce checkruntime argument Signed-off-by: Sebastian Kuttnig --- docs/man/failover.txt | 23 +++++++++++++++++++++++ docs/nut.dict | 3 ++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/man/failover.txt b/docs/man/failover.txt index 7173bce6fc..537bb5973d 100644 --- a/docs/man/failover.txt +++ b/docs/man/failover.txt @@ -112,6 +112,24 @@ itself. This mode is for setups where immediate shutdown is warranted, regardless of anything else, and getting `FSD` out to the clients as fast as just possible. +*checkruntime*='0|1|2|3':: +Optional. Controls how `battery.runtime` values are used to break ties between +non-fully-online UPS devices **at priority 3 or lower**. Has no effect on +initial priority selection or when `strictfiltering` is enabled. Defaults to 1. + +- `0`: *Disabled.* No runtime comparison is done. The first candidate with the +best priority is selected according to the order of the port argument. + +- `1`: *Compare `battery.runtime`.* The UPS with the higher value is preferred. +If the value is missing or invalid, the UPS cannot win the tie-break. + +- `2`: *Compare `battery.runtime.low`.* The UPS with the higher value is +preferred. If the value is missing or invalid, the UPS cannot win the tie-break. + +- `3`: *Compare both variables strictly.* The UPS is preferred only if it has +both a higher `battery.runtime` and `battery.runtime.low` value. If either is +missing or invalid, the UPS cannot win the tie-break. + *strictfiltering*='0|1':: Optional. If set to 1, only UPS drivers matching the configured status filters are considered for promotion to primary. If set to 0, the hard-coded default logic is also considered when no status filters match @@ -250,6 +268,11 @@ When using `failover` for redundancy between multiple UPS drivers connected to the same underlying UPS device, data is not multiplexed between the drivers. As a result, some data points may be available in some drivers but not in others. +For `checkruntime` considerations, the unit of both `battery.runtime` and +`battery.runtime.low` is assumed to be **seconds**. UPS drivers that report +these values using different units are considered non-compliant with the NUT +variable standards and should be reported to the NUT developers as faulty. + AUTHOR ------ diff --git a/docs/nut.dict b/docs/nut.dict index c62feae55c..bcb30f19e2 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3517 utf-8 +personal_ws-1.1 en 3518 utf-8 AAC AAS ABI @@ -1756,6 +1756,7 @@ cgroup cgroupsv chargetime charset +checkruntime checksum checksums chgrp From d8f171c18c9ec59bb13400c2a12da1e31c0cc1aa Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 12:03:07 +0200 Subject: [PATCH 38/41] drivers/failover.c: minor improvements to order and debug levels Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 60 +++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index ac3b6d1851..c99d66630e 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -98,8 +98,8 @@ static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd); static int ups_add_cmd(ups_device_t *ups, const char *val); static int ups_del_cmd(ups_device_t *ups, const char *val); -static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low); static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode); +static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low); static int ups_get_var_pos(const ups_device_t *ups, const char *key); static int ups_set_var(ups_device_t *ups, const char *key, const char *value); static int ups_del_var(ups_device_t *ups, const char *key); @@ -283,18 +283,18 @@ void upsdrv_makevartable(void) arg_fsdmode); addvar(VAR_VALUE, "fsdmode", buf); - snprintf(buf, sizeof(buf), - "Sets if only the given status filters should be considered for " - "UPS driver to be electable as primary (default: %d)", - arg_strict_filtering); - addvar(VAR_VALUE, "strictfiltering", buf); - snprintf(buf, sizeof(buf), "Sets if runtime remaining variables should resolve ties for non-OL priorities " "3 and lower (0: disabled, 1: runtime, 2: runtime low, 3: both) (default: %d)", arg_check_runtime); addvar(VAR_VALUE, "checkruntime", buf); + snprintf(buf, sizeof(buf), + "Sets if only the given status filters should be considered for " + "UPS driver to be electable as primary (default: %d)", + arg_strict_filtering); + addvar(VAR_VALUE, "strictfiltering", buf); + addvar(VAR_VALUE, "status_have_any", "Comma separated list of status tokens, any present qualifies " "the UPS driver for primary (default: unset)"); @@ -560,11 +560,11 @@ static void handle_arguments(void) str_arg_to_int("fsdmode", getval("fsdmode"), &arg_fsdmode, DEFAULT_FSD_MODE, 0, 2); - str_arg_to_int("strictfiltering", getval("strictfiltering"), - &arg_strict_filtering, DEFAULT_STRICT_FILTERING, 0, 1); - str_arg_to_int("checkruntime", getval("checkruntime"), &arg_check_runtime, DEFAULT_CHECK_RUNTIME, 0, 3); + + str_arg_to_int("strictfiltering", getval("strictfiltering"), + &arg_strict_filtering, DEFAULT_STRICT_FILTERING, 0, 1); } static void parse_port_argument(void) @@ -681,7 +681,7 @@ static void export_driver_state(void) dstate_delinfo("driver.primary.stats.vars"); } - upsdebugx(4, "%s: exported internal driver state to dstate", + upsdebugx(5, "%s: exported internal driver state to dstate", __func__); } @@ -1443,7 +1443,7 @@ static int ups_passes_status_filters(const ups_device_t *ups) arg_status_filters.nothave_any_count == 0 && arg_status_filters.nothave_all_count == 0) { - upsdebugx(4, "%s: [%s]: no status filters are set, disregarding filtering", + upsdebugx(5, "%s: [%s]: no status filters are set, disregarding filtering", __func__, ups->socketname); return 0; @@ -1739,6 +1739,24 @@ static int ups_del_cmd(ups_device_t *ups, const char *val) return 0; } +static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode) +{ + switch (mode) { + case 1: + /* compare runtime */ + return rt > best_rt; + case 2: + /* compare runtime low */ + return rt_low > best_rt_low; + case 3: + /* compare runtime + runtime low */ + return (rt > best_rt && rt_low > best_rt_low); + default: + /* invalid mode */ + return 0; + } +} + static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low) { int tmp = -1; @@ -1764,24 +1782,6 @@ static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime } } -static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode) -{ - switch (mode) { - case 1: - /* compare runtime */ - return rt > best_rt; - case 2: - /* compare runtime low */ - return rt_low > best_rt_low; - case 3: - /* compare runtime + runtime low */ - return (rt > best_rt && rt_low > best_rt_low); - default: - /* invalid mode */ - return 0; - } -} - static int ups_get_var_pos(const ups_device_t *ups, const char *key) { size_t i = 0; From 9a6f56cbfdaf7124b93f495fccb09b8b10eb1f5d Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 15:00:41 +0200 Subject: [PATCH 39/41] drivers/failover.{c,h}: store runtimes in UPS struct These changes avoid constant lookups in the variable table for better performance. Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 92 +++++++++++++++++++--------------------------- drivers/failover.h | 5 ++- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index c99d66630e..5b2255c93a 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -89,6 +89,7 @@ static void ups_is_offline(ups_device_t *ups); static ups_device_t *get_primary_candidate(void); static int ups_passes_status_filters(const ups_device_t *ups); +static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode); static void ups_promote_primary(ups_device_t *ups); static void ups_demote_primary(ups_device_t *ups); static void ups_export_dstate(ups_device_t *ups); @@ -98,8 +99,6 @@ static int ups_get_cmd_pos(const ups_device_t *ups, const char *cmd); static int ups_add_cmd(ups_device_t *ups, const char *val); static int ups_del_cmd(ups_device_t *ups, const char *val); -static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode); -static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low); static int ups_get_var_pos(const ups_device_t *ups, const char *key); static int ups_set_var(ups_device_t *ups, const char *key, const char *value); static int ups_del_var(ups_device_t *ups, const char *key); @@ -844,6 +843,9 @@ static int ups_connect(ups_device_t *ups) ups_free_ups_state(ups); /* free any previous state */ ups->force_dstate_export = 1; + ups->runtime = -1; + ups->runtime_low = -1; + ups_is_alive(ups); time(&ups->last_heard_time); @@ -1143,6 +1145,18 @@ static int ups_parse_protocol(ups_device_t *ups, size_t numargs, char **arg) } } + if (!strcmp(arg[1], "battery.runtime")) { + if (!str_to_int(arg[2], &ups->runtime, 10)) { + ups->runtime = -1; + } + } + + if (!strcmp(arg[1], "battery.runtime.low")) { + if (!str_to_int(arg[2], &ups->runtime_low, 10)) { + ups->runtime_low = -1; + } + } + ups_set_var(ups, varptr, arg[2]); return 1; @@ -1386,15 +1400,11 @@ static ups_device_t *get_primary_candidate(void) } if (priority >= 0) { - int rt = -1; - int rt_low = -1; + int rt = ups->runtime; + int rt_low = ups->runtime_low; primaries++; - if (arg_check_runtime && priority >= PRIORITY_WEAK) { - ups_get_runtimes(ups, &rt, &rt_low); - } - if (priority < best_priority) { best_choice = ups; best_priority = priority; @@ -1410,7 +1420,7 @@ static ups_device_t *get_primary_candidate(void) } } - upsdebugx(4, "%s: [%s]: is candidate (priority [%d], runtime [%d]/[%d])", + upsdebugx(4, "%s: [%s]: is a candidate (priority [%d], runtime [%d]/[%d])", __func__, ups->socketname, priority, rt, rt_low); } @@ -1420,7 +1430,7 @@ static ups_device_t *get_primary_candidate(void) ups_primary_count = primaries; if (best_choice) { - upsdebugx(4, "%s: [%s]: was selected (priority [%d], runtime [%d]/[%d])", + upsdebugx(4, "%s: [%s]: is best candidate (priority [%d], runtime [%d]/[%d])", __func__, best_choice->socketname, best_priority, best_runtime, best_runtime_low); } @@ -1502,7 +1512,24 @@ static int ups_passes_status_filters(const ups_device_t *ups) return 1; } -/* Promote a UPS driver that is not NULL and not already the current primary */ +static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode) +{ + switch (mode) { + case 1: + /* compare runtime */ + return rt > best_rt; + case 2: + /* compare runtime low */ + return rt_low > best_rt_low; + case 3: + /* compare runtime + runtime low */ + return (rt > best_rt && rt_low > best_rt_low); + default: + /* invalid mode */ + return 0; + } +} + static void ups_promote_primary(ups_device_t *ups) { if (!ups || primary_ups == ups) { @@ -1739,49 +1766,6 @@ static int ups_del_cmd(ups_device_t *ups, const char *val) return 0; } -static int has_better_runtime(int rt, int rt_low, int best_rt, int best_rt_low, int mode) -{ - switch (mode) { - case 1: - /* compare runtime */ - return rt > best_rt; - case 2: - /* compare runtime low */ - return rt_low > best_rt_low; - case 3: - /* compare runtime + runtime low */ - return (rt > best_rt && rt_low > best_rt_low); - default: - /* invalid mode */ - return 0; - } -} - -static void ups_get_runtimes(const ups_device_t *ups, int *runtime, int *runtime_low) -{ - int tmp = -1; - int pos_rt = -1; - int pos_rt_low = -1; - - if (!ups || !runtime || !runtime_low) { - return; - } - - pos_rt = ups_get_var_pos(ups, "battery.runtime"); - pos_rt_low = ups_get_var_pos(ups, "battery.runtime.low"); - - *runtime = -1; - *runtime_low = -1; - - if (pos_rt >= 0 && str_to_int(ups->var_list[pos_rt]->value, &tmp, 10)) { - *runtime = tmp; - } - - if (pos_rt_low >= 0 && str_to_int(ups->var_list[pos_rt_low]->value, &tmp, 10)) { - *runtime_low = tmp; - } -} - static int ups_get_var_pos(const ups_device_t *ups, const char *key) { size_t i = 0; diff --git a/drivers/failover.h b/drivers/failover.h index 67d577b358..fba603af2b 100644 --- a/drivers/failover.h +++ b/drivers/failover.h @@ -128,11 +128,14 @@ typedef struct { ups_flags_t flags; ups_priority_t priority; - int failure_count; + int runtime; + int runtime_low; int force_ignore; int force_primary; int force_dstate_export; + + int failure_count; } ups_device_t; #endif /* FAILOVER_H_SEEN */ From 0937f25a4abdff45df23ab9634230d4febdeae5e Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 15:12:48 +0200 Subject: [PATCH 40/41] drivers/failover.c: improve guarding of ups->status against NULL dereference Adds checks to ensure ups->status is not NULL before use, preventing a possible NULL pointer dereference. Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index 5b2255c93a..e7b445dc98 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -1284,14 +1284,14 @@ static void ups_is_dead(ups_device_t *ups) { ups_alive_count--; upsdebugx(2, "%s: [%s]: is now dead " "with (last known) status [%s] (alive devices: %" PRIuSIZE ")", - __func__, ups->socketname, ups->status, ups_alive_count); + __func__, ups->socketname, NUT_STRARG(ups->status), ups_alive_count); } if (ups_has_flag(ups, UPS_FLAG_ONLINE)) { ups_online_count--; upsdebugx(3, "%s: [%s]: was online with (last known) " "status [%s] and is now dead (online devices: %" PRIuSIZE ")", - __func__, ups->socketname, ups->status, ups_online_count); + __func__, ups->socketname, NUT_STRARG(ups->status), ups_online_count); } } @@ -1301,7 +1301,7 @@ static void ups_is_online(ups_device_t *ups) { ups_online_count++; upsdebugx(2, "%s: [%s]: is now online " "with status [%s] (online devices: %" PRIuSIZE ")", - __func__, ups->socketname, ups->status, ups_online_count); + __func__, ups->socketname, NUT_STRARG(ups->status), ups_online_count); } } @@ -1311,7 +1311,7 @@ static void ups_is_offline(ups_device_t *ups) { ups_online_count--; upsdebugx(2, "%s: [%s]: is now offline " "with status [%s] (online devices: %" PRIuSIZE ")", - __func__, ups->socketname, ups->status, ups_online_count); + __func__, ups->socketname, NUT_STRARG(ups->status), ups_online_count); } } @@ -1440,14 +1440,14 @@ static ups_device_t *get_primary_candidate(void) static int ups_passes_status_filters(const ups_device_t *ups) { size_t i = 0; - const char *status = NULL; - if (!*ups->status) { + if (!ups->status || *ups->status == '\0') { + upsdebugx(4, "%s: [%s]: no status is available, disregarding filtering", + __func__, ups->socketname); + return 0; } - status = ups->status; - if (arg_status_filters.have_any_count == 0 && arg_status_filters.have_all_count == 0 && arg_status_filters.nothave_any_count == 0 && @@ -1460,7 +1460,7 @@ static int ups_passes_status_filters(const ups_device_t *ups) } for (i = 0; i < arg_status_filters.nothave_any_count; ++i) { - if (str_contains_token(status, arg_status_filters.nothave_any[i])) { + if (str_contains_token(ups->status, arg_status_filters.nothave_any[i])) { upsdebugx(4, "%s: [%s]: nothave_any: [%s] was found, excluded", __func__, ups->socketname, arg_status_filters.nothave_any[i]); @@ -1469,7 +1469,7 @@ static int ups_passes_status_filters(const ups_device_t *ups) } for (i = 0; i < arg_status_filters.have_all_count; ++i) { - if (!str_contains_token(status, arg_status_filters.have_all[i])) { + if (!str_contains_token(ups->status, arg_status_filters.have_all[i])) { upsdebugx(4, "%s: [%s]: have_all: [%s] not found, excluded", __func__, ups->socketname, arg_status_filters.have_all[i]); @@ -1480,7 +1480,7 @@ static int ups_passes_status_filters(const ups_device_t *ups) if (arg_status_filters.nothave_all_count > 0) { int all_found = 1; for (i = 0; i < arg_status_filters.nothave_all_count; ++i) { - if (!str_contains_token(status, arg_status_filters.nothave_all[i])) { + if (!str_contains_token(ups->status, arg_status_filters.nothave_all[i])) { all_found = 0; break; } @@ -1496,7 +1496,7 @@ static int ups_passes_status_filters(const ups_device_t *ups) if (arg_status_filters.have_any_count > 0) { int any_found = 0; for (i = 0; i < arg_status_filters.have_any_count; ++i) { - if (str_contains_token(status, arg_status_filters.have_any[i])) { + if (str_contains_token(ups->status, arg_status_filters.have_any[i])) { any_found = 1; break; } @@ -1552,7 +1552,8 @@ static void ups_promote_primary(ups_device_t *ups) upslogx(LOG_NOTICE, "%s: [%s]: was promoted " "to primary with status [%s] and priority [%d]", - __func__, primary_ups->socketname, primary_ups->status, primary_ups->priority); + __func__, primary_ups->socketname, + NUT_STRARG(primary_ups->status), primary_ups->priority); ups_export_dstate(primary_ups); } @@ -1566,7 +1567,8 @@ static void ups_demote_primary(ups_device_t *ups) upslogx(LOG_NOTICE, "%s: [%s]: is no longer " "primary with (last known) status [%s] and priority [%d]", - __func__, last_primary_ups->socketname, last_primary_ups->status, last_primary_ups->priority); + __func__, last_primary_ups->socketname, + NUT_STRARG(last_primary_ups->status), last_primary_ups->priority); ups_clean_dstate(last_primary_ups); } From fb3cac709b54395d887f9f7907b8b86d18a6621b Mon Sep 17 00:00:00 2001 From: Sebastian Kuttnig Date: Wed, 28 May 2025 15:28:42 +0200 Subject: [PATCH 41/41] drivers/failover.{c,h}: make UPS priorities more readable in code Signed-off-by: Sebastian Kuttnig --- drivers/failover.c | 10 +++++----- drivers/failover.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/drivers/failover.c b/drivers/failover.c index e7b445dc98..f3acfa50e6 100644 --- a/drivers/failover.c +++ b/drivers/failover.c @@ -1381,7 +1381,7 @@ static ups_device_t *get_primary_candidate(void) __func__, ups->socketname, elapsed_force, ups->force_primary); } else if (ups_passes_status_filters(ups)) { - priority = PRIORITY_USERFILTERS; + priority = PRIORITY_STATUSFILTERS; } else if (arg_strict_filtering) { upsdebugx(4, "%s: [%s]: 'strict_filtering' is enabled, considering " @@ -1389,13 +1389,13 @@ static ups_device_t *get_primary_candidate(void) __func__, ups->socketname); } else if (ups_has_flag(ups, (ups_flags_t)(UPS_FLAG_DATA_OK | UPS_FLAG_ONLINE))) { - priority = PRIORITY_GOOD; + priority = PRIORITY_ONLINE; } else if (ups_has_flag(ups, UPS_FLAG_DATA_OK)) { - priority = PRIORITY_WEAK; + priority = PRIORITY_BATTERY; } else { - priority = PRIORITY_LASTRESORT; + priority = PRIORITY_STALE; } } @@ -1411,7 +1411,7 @@ static ups_device_t *get_primary_candidate(void) best_runtime = rt; best_runtime_low = rt_low; } - else if (priority == best_priority && arg_check_runtime && priority >= PRIORITY_WEAK) { + else if (priority == best_priority && arg_check_runtime && priority >= PRIORITY_BATTERY) { /* All devices are not fully online and runtime checking is enabled, compare values: */ if (has_better_runtime(rt, rt_low, best_runtime, best_runtime_low, arg_check_runtime)) { best_choice = ups; diff --git a/drivers/failover.h b/drivers/failover.h index fba603af2b..4c89f1fc1e 100644 --- a/drivers/failover.h +++ b/drivers/failover.h @@ -47,10 +47,10 @@ typedef enum { PRIORITY_SKIPPED = -1, PRIORITY_FORCED = 0, - PRIORITY_USERFILTERS = 1, - PRIORITY_GOOD = 2, - PRIORITY_WEAK = 3, - PRIORITY_LASTRESORT = 4 + PRIORITY_STATUSFILTERS = 1, + PRIORITY_ONLINE = 2, + PRIORITY_BATTERY = 3, + PRIORITY_STALE = 4 } ups_priority_t; typedef enum {