diff --git a/NEWS.adoc b/NEWS.adoc index c45ea316c7..01cd36b907 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -160,6 +160,16 @@ 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. + [#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 diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index d7453ba712..c41f7ea4b0 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -857,6 +857,7 @@ SRC_SERIAL_PAGES = \ clone.txt \ clone-outlet.txt \ dummy-ups.txt \ + failover.txt \ etapro.txt \ everups.txt \ gamatronic.txt \ @@ -909,6 +910,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) \ @@ -976,6 +978,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..537bb5973d --- /dev/null +++ b/docs/man/failover.txt @@ -0,0 +1,296 @@ +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`) + +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 +--------------- + +This driver supports the following settings: + +*port*='drivername-devicename,drivername2-devicename2,...':: +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-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 +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':: +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 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 as stale after an +additional few seconds have elapsed (ensuring full propagation).* This will +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 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. + +*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 +(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 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 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 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 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 basic +default 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]` 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]` 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. + +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 +---------- + +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. + +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. + +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. + +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. + +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 +------ + +Sebastian Kuttnig + +SEE ALSO +-------- + +linkman:upscmd[1], +linkman:upsrw[1], +linkman:ups.conf[5], +linkman:upsc[8], +linkman:upsmon[8], +linkman:nutupsdrv[8], +linkman:clone[8], +linkman:clone-outlet[8] + +Internet Resources: +~~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ 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 -------- diff --git a/docs/nut.dict b/docs/nut.dict index fa853d674f..bcb30f19e2 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3503 utf-8 +personal_ws-1.1 en 3518 utf-8 AAC AAS ABI @@ -371,6 +371,7 @@ Exar ExecCGI ExecStart ExecStartPre +FAILOVER FBCA FD FDE @@ -1755,6 +1756,7 @@ cgroup cgroupsv chargetime charset +checkruntime checksum checksums chgrp @@ -1810,6 +1812,8 @@ confpath conn const contrib +cooldown +coolofftime copyrightable coreutils cout @@ -1879,6 +1883,7 @@ ddk ddl de deUNV +deadtime debian debootstrap debouncing @@ -2038,6 +2043,7 @@ faa fabula facto failmode +failover fallthrough fasttrack fatalx @@ -2079,6 +2085,7 @@ frob frontends fs fsd +fsdmode fsr fstab ftdi @@ -2230,6 +2237,7 @@ includePath includedir inductor inet +influenceable infos infoval inh @@ -2240,6 +2248,7 @@ initializer initializers initinfo initscripts +inittime initups inline inlined @@ -2481,6 +2490,7 @@ manpage manpages masterguard matcher +maxconnfails maxd maxlength maxreport @@ -2635,12 +2645,14 @@ nombattvolt noncommercially noout nooutstats +noprimarytime norating noro noscanlangid nosnap nosuid notAfter +nothave notifyflags notifyme notifymsg @@ -2830,6 +2842,7 @@ proc productid prog progname +proxied prtconf ps psu @@ -2891,6 +2904,7 @@ regtype relatime releasekeyring relicensing +relogtime remoteip renderer renderers @@ -3108,6 +3122,7 @@ strcpy strdup strerror strftime +strictfiltering stringify strlen strncpy diff --git a/drivers/Makefile.am b/drivers/Makefile.am index b02cfd049f..8c1c9adcc4 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) @@ -409,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 \ diff --git a/drivers/failover.c b/drivers/failover.c new file mode 100644 index 0000000000..f3acfa50e6 --- /dev/null +++ b/drivers/failover.c @@ -0,0 +1,2376 @@ +/* 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 "failover.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" + +upsdrv_info_t upsdrv_info = { + DRIVER_NAME, + DRIVER_VERSION, + "Sebastian Kuttnig ", + DRV_EXPERIMENTAL, + { NULL } +}; + +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 arg_check_runtime = DEFAULT_CHECK_RUNTIME; + +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(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); +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); +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 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); + +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 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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), buf); + } + } + + if (!ups_alive_count) { + upslogx(LOG_WARNING, "%s: none of the tracked UPS drivers were connectable", + __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); + ups_clean_dstate(primary_candidate); + 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) +{ + 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) { + set_exit_flag(EF_EXIT_FAILURE); + } +} + +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 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)"); + 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 (ups) { + 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->socketname) { + free(ups->socketname); + ups->socketname = NULL; + } + + free(ups); + ups_list[i] = NULL; + } + } + + if (ups_list) { + 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; + 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 = 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); + + return STAT_INSTCMD_HANDLED; + } + + 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 = 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); + + 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, NUT_STRARG(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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(msgbuf), msgbuf); + } + + tv.tv_sec = CONN_CMD_TIMEOUT; + tv.tv_usec = 0; + + cmdret = upsdrvquery_oneshot_sockfn(primary_ups->socketname, + msgbuf, NULL, 0, &tv); + + if (cmdret >= 0) { + upslogx(LOG_NOTICE, "%s: sent [%s] [%s], " + "received response code: [%" PRIiSIZE "]", + __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, NUT_STRARG(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, NUT_STRARG(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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(msgbuf), msgbuf); + } + + tv.tv_sec = CONN_CMD_TIMEOUT; + tv.tv_usec = 0; + + cmdret = upsdrvquery_oneshot_sockfn(primary_ups->socketname, + 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) +{ + parse_port_argument(); + parse_status_filters(); + + str_arg_to_int("inittime", getval("inittime"), + &arg_init_timeout, DEFAULT_INIT_TIMEOUT, 0, INT_MAX); + + str_arg_to_int("deadtime", getval("deadtime"), + &arg_dead_timeout, DEFAULT_DEAD_TIMEOUT, 0, INT_MAX); + + str_arg_to_int("relogtime", getval("relogtime"), + &arg_relog_timeout, DEFAULT_RELOG_TIMEOUT, 0, INT_MAX); + + str_arg_to_int("noprimarytime", getval("noprimarytime"), + &arg_noprimary_timeout, DEFAULT_NO_PRIMARY_TIMEOUT, 0, INT_MAX); + + str_arg_to_int("maxconnfails", getval("maxconnfails"), + &arg_maxconnfails, DEFAULT_MAX_CONNECT_FAILS, 0, INT_MAX); + + str_arg_to_int("coolofftime", getval("coolofftime"), + &arg_coolofftimeout, DEFAULT_CONNECTION_COOLOFF, 0, INT_MAX); + + str_arg_to_int("fsdmode", getval("fsdmode"), + &arg_fsdmode, DEFAULT_FSD_MODE, 0, 2); + + 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) +{ + char *tmp = NULL; + 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, ","); + while (token) { + ups_device_t *new_ups = NULL; + + str_trim_space(token); + + if (*token == '\0') { + token = strtok(NULL, ","); + + continue; + } + + new_ups = xcalloc(1, sizeof(**ups_list)); + new_ups->socketname = xstrdup(token); + + 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); + tmp = NULL; +} + +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.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.socketname"); + dstate_delinfo("driver.primary.priority"); + dstate_delinfo("driver.primary.stats.cmds"); + dstate_delinfo("driver.primary.stats.vars"); + } + + upsdebugx(5, "%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); + } + + /* dstate is already clean at this point, hence no _init() calls */ + alarm_set("No suitable primaries for failover"); + alarm_commit(); + status_commit(); /* publish ALARM */ + + 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__); + } + + /* 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(); /* publish ALARM + FSD */ + + 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 { + 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->runtime = -1; + ups->runtime_low = -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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), 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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, sizeof(buf), 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")) { + 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, auxval); + } else { + upsdebugx(5, "%s: [%s]: got non-numeric SETAUX [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + } + + 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); + } + } + + 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; + } + + if (numargs < 4) { + goto skip_out; + } + + /* 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, 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, minval, maxval); + } else { + upsdebugx(5, "%s: [%s]: got non-numeric ADDRANGE [%s] from UPS driver", + __func__, ups->socketname, arg[1]); + } + + 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 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", + __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, 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, NUT_STRARG(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, NUT_STRARG(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, NUT_STRARG(ups->status), ups_online_count); + } +} + +static ups_device_t *get_primary_candidate(void) +{ + time_t now; + 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); + + 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_flags_t)(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_STATUSFILTERS; + } + 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_flags_t)(UPS_FLAG_DATA_OK | UPS_FLAG_ONLINE))) { + priority = PRIORITY_ONLINE; + } + else if (ups_has_flag(ups, UPS_FLAG_DATA_OK)) { + priority = PRIORITY_BATTERY; + } + else { + priority = PRIORITY_STALE; + } + } + + if (priority >= 0) { + int rt = ups->runtime; + int rt_low = ups->runtime_low; + + primaries++; + + 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_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; + best_runtime = rt; + best_runtime_low = rt_low; + } + } + + upsdebugx(4, "%s: [%s]: is a candidate (priority [%d], runtime [%d]/[%d])", + __func__, ups->socketname, priority, rt, rt_low); + } + + ups->priority = priority; + } + + ups_primary_count = primaries; + + if (best_choice) { + upsdebugx(4, "%s: [%s]: is best candidate (priority [%d], runtime [%d]/[%d])", + __func__, best_choice->socketname, best_priority, best_runtime, best_runtime_low); + } + + return best_choice; +} + +static int ups_passes_status_filters(const ups_device_t *ups) +{ + size_t i = 0; + + if (!ups->status || *ups->status == '\0') { + upsdebugx(4, "%s: [%s]: no status is available, disregarding filtering", + __func__, ups->socketname); + + return 0; + } + + 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(5, "%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(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]); + + return 0; + } + } + + for (i = 0; i < arg_status_filters.have_all_count; ++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]); + + 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(ups->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(ups->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 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) { + 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); + } + + 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, + NUT_STRARG(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, + NUT_STRARG(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(const 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]); + arg_status_filters.have_any[i] = NULL; + } + 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]); + arg_status_filters.have_all[i] = NULL; + } + 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]); + arg_status_filters.nothave_any[i] = NULL; + } + 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]); + arg_status_filters.nothave_all[i] = NULL; + } + 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 size " + "[%" PRIuSIZE "] exceeds buffer of size [%" PRIuSIZE "]: %s", + __func__, (size_t)required, outlen, out); + } + + return out; + } + + return in; +} + +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 != 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); + + *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 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 || !argcsv || !array || !countvar) { + return -1; + } + + if (*argcsv == '\0') { + return 0; + } + + tmp = xstrdup(argcsv); + + token = strtok(tmp, ","); + while (token) { + str_trim_space(token); + + if (*token == '\0') { + token = strtok(NULL, ","); + + continue; + } + + str = xstrdup(token); + + *array = xrealloc(*array, sizeof(**array) * (*countvar + 1)); + (*array)[*countvar] = str; + (*countvar)++; + + count++; + + upsdebugx(1, "%s: added [%s] to [%s] from configuration", + __func__, str, arg); + + token = strtok(NULL, ","); + } + + free(tmp); + tmp = NULL; + + return count; +} + +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; +} diff --git a/drivers/failover.h b/drivers/failover.h new file mode 100644 index 0000000000..4c89f1fc1e --- /dev/null +++ b/drivers/failover.h @@ -0,0 +1,141 @@ +/* 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_CHECK_RUNTIME 1 +#define DEFAULT_FSD_MODE 0 +#define DEFAULT_STRICT_FILTERING 0 + +typedef enum { + PRIORITY_SKIPPED = -1, + PRIORITY_FORCED = 0, + PRIORITY_STATUSFILTERS = 1, + PRIORITY_ONLINE = 2, + PRIORITY_BATTERY = 3, + PRIORITY_STALE = 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 *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 runtime; + int runtime_low; + + int force_ignore; + int force_primary; + int force_dstate_export; + + int failure_count; +} ups_device_t; + +#endif /* FAILOVER_H_SEEN */ diff --git a/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in b/scripts/upsdrvsvcctl/nut-driver-enumerator.sh.in index 2641bd2c61..838181fb5f 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,58 @@ 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}" ] \ + || { + 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 + 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 +1008,60 @@ 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}" ] \ + || { + 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 + 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'..." 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); 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'