diff --git a/COPYING b/COPYING index ba7bff0614..45355d3a4c 100644 --- a/COPYING +++ b/COPYING @@ -7,7 +7,7 @@ Public License (GPL) version 3, or (at your option) any later version. See "LICENSE-GPL3" in the root of this distribution. - The Perl client module (scripts/perl/Nut.pm) is released under the same + The Perl client module (scripts/perl/UPS/Nut.pm) is released under the same license as Perl itself. That is to say either GPL version 1 or (at your option) any later version, or the "Artistic License". diff --git a/NEWS.adoc b/NEWS.adoc index 4dd5544251..572493e98a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -59,6 +59,20 @@ https://github.com/networkupstools/nut/milestone/13 [issue #3387, PR #3402] + - NUT client libraries: + * Complete support for actions documented in `docs/net-protocol.txt` + was implemented in C++, Python and PERL bindings in-tree, and for Java + in link:https://github.com/networkupstools/jNut[jNut] nearby. Among + other things, all these libraries now support `STARTTLS` and `TRACKING` + to wait for server confirmation of a `SET VAR` or `INSTCMD` request, + and this is tested by the NIT script. [issues #656, #1348, #1349, #1350, + #1613, #1711, PR #3402] + * Enhanced client side of `STARTTLS` dialog to follow up by a simple + query (for protocol version) to verify that handshake succeeded. + This change impacted also the classic C `libupsclient` library. + [issue #3387, PR #3402] + + Release notes for NUT 2.8.5 - what's new since 2.8.4 ---------------------------------------------------- diff --git a/clients/Makefile.am b/clients/Makefile.am index 47b11baee7..b5870c2097 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -219,7 +219,7 @@ endif WITH_SSL # for the run-time dynamic linker resolution? For now the shared-library # builds are "exotic", but it makes sense to deprecate this export in a # future release. -libupsclient_la_LDFLAGS = -version-info 8:0:1 +libupsclient_la_LDFLAGS = -version-info 9:0:2 libupsclient_la_LDFLAGS += -export-symbols-regex '^(upscli_|nut_debug_level)' #|s_upsdebug|fatalx|fatal_with_errno|xcalloc|xbasename|print_banner_once)' if HAVE_WINDOWS @@ -292,7 +292,7 @@ libupsclient-version.h: libupsclient.la if HAVE_CXX11 # libnutclient version information and build libnutclient_la_SOURCES = nutclient.h nutclient.cpp -libnutclient_la_LDFLAGS = -version-info 3:0:1 +libnutclient_la_LDFLAGS = -version-info 4:0:2 # Needed in not-standalone builds with -DHAVE_NUTCOMMON=1 # which is defined for in-tree CXX builds above: if ENABLE_SHARED_PRIVATE_LIBS diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 0dd0cb8897..9bc64b158f 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -27,6 +27,8 @@ #include "nutclient.h" #include +#include +#include /* TODO: Make it a run-time option like upsdebugx(), * probably with a verbosity level variable in each @@ -1497,7 +1499,21 @@ void TcpClient::connect() _socket->setSSLConfig_NSS(_force_ssl, _certverify, _certstore_path, _key_pass, _certstore_prefix, _certhost_name, _certident_name); } + /* May throw in case of low-level problems */ _socket->startTLS(); + + /* Make sure handshake succeeded or abort early + * (there is currently no way for the server to + * report its fault to the client when connection + * is half-way secure): + */ + if (!isValidProtocolVersion()) { + if (_force_ssl) { + disconnect(); + throw nut::SSLException("STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); + } + /* TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? */ + } } } @@ -1718,6 +1734,31 @@ void TcpClient::logout() _socket->disconnect(); } +bool TcpClient::isValidProtocolVersion(const std::string& version_re) +{ + std::string version; + try { + version = sendQuery("PROTVER"); + } catch (NutException &ignored) { + NUT_UNUSED_VARIABLE(ignored); + /* Deprecated and hidden, but may be what ancient NUT servers say + * May throw if the error is due to (non-)connection */ + version = sendQuery("NETVER"); + } + + if (version_re.empty()) { + // Basic check for 1.0 through 1.3, as of NUT v2.8.2 + if (version == "1.0" || version == "1.1" || version == "1.2" || version == "1.3") { + return true; + } + } else { + // TODO: Regex + return (version_re == version); + } + + return false; +} + Device TcpClient::getDevice(const std::string& name) { try @@ -1857,20 +1898,20 @@ std::map > > TcpClient return map; } -TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) +TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { std::string query = "SET VAR " + dev + " " + name + " " + escape(value); - return sendTrackingQuery(query); + return sendTrackingQuery(query, waitIntervalSec, waitMaxCount); } -TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) +TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec, int waitMaxCount) { std::string query = "SET VAR " + dev + " " + name; for(size_t n=0; n TcpClient::getDeviceCommandNames(const std::string& dev) @@ -1891,9 +1932,9 @@ std::string TcpClient::getDeviceCommandDescription(const std::string& dev, const return get("CMDDESC", dev + " " + name)[0]; } -TrackingID TcpClient::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param) +TrackingID TcpClient::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { - return sendTrackingQuery("INSTCMD " + dev + " " + name + " " + param); + return sendTrackingQuery("INSTCMD " + dev + " " + name + " " + param, waitIntervalSec, waitMaxCount); } std::map> TcpClient::listDeviceClients(void) @@ -1983,11 +2024,13 @@ TrackingResult TcpClient::getTrackingResult(const TrackingID& id) { if (id.empty()) { - return TrackingResult::SUCCESS; + return TrackingResult::UNSET; + /* TOTHINK // return TrackingResult::SUCCESS;*/ } - std::string result = sendQuery("GET TRACKING " + id); + std::string result = sendQuery("GET TRACKING " + id.id()); + /* TOTHINK: Update id.setStatus() ? */ if (result == "PENDING") { return TrackingResult::PENDING; @@ -2010,6 +2053,67 @@ TrackingResult TcpClient::getTrackingResult(const TrackingID& id) } } +void TcpClient::enableTrackingModeOnce(void) +{ + if (_tracking != "ON") + { + setFeature(TRACKING, true); + _tracking = sendQuery("GET " + TRACKING); + } +} + +bool TcpClient::isTrackingModeEnabled(void) +{ + if (_tracking != "ON") + { + _tracking = sendQuery("GET " + TRACKING); + } + return (_tracking == "ON"); +} + +TrackingResult TcpClient::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) +{ + if (id.empty()) + { + return TrackingResult::SUCCESS; + } + + int count = 0; + while (true) + { + TrackingResult res = getTrackingResult(id); + if (res != TrackingResult::PENDING) + { + return res; + } + + if (waitMaxCount > 0 && ++count >= waitMaxCount) + { + return TrackingResult::PENDING; + } + + if (waitIntervalSec > 0) + { + // https://stackoverflow.com/questions/37358856/does-mingw-w64-support-stdthread-out-of-the-box-when-using-the-win32-threading + // Does not work on some, not all, mingw versions (headers lack threads): +#ifndef WIN32 + std::this_thread::sleep_for(std::chrono::seconds(waitIntervalSec)); +#else + Sleep(waitIntervalSec * 1000L); +#endif + } + else + { + // Default to some small sleep if not specified but we are waiting? + // Actually Perl/Python just loop or use the interval. + // If interval is 0, we might busy loop, which is bad. + // But let's follow the provided parameters. + if (waitIntervalSec == 0) break; + } + } + return TrackingResult::PENDING; +} + bool TcpClient::isFeatureEnabled(const Feature& feature) { std::string result = sendQuery("GET " + feature); @@ -2273,8 +2377,13 @@ std::string TcpClient::escape(const std::string& str) return res; } -TrackingID TcpClient::sendTrackingQuery(const std::string& req) +TrackingID TcpClient::sendTrackingQuery(const std::string& req, int waitIntervalSec, int waitMaxCount) { + if (waitIntervalSec > 0) + { + enableTrackingModeOnce(); + } + std::string reply = sendQuery(req); detectError(reply); std::vector res = explode(reply); @@ -2285,7 +2394,50 @@ TrackingID TcpClient::sendTrackingQuery(const std::string& req) } else if (res.size() == 3 && res[0] == "OK" && res[1] == "TRACKING") { - return TrackingID(res[2]); + TrackingID id = TrackingID(res[2]); + if (waitIntervalSec > 0 && !id.empty()) + { + TrackingResult wtres = waitTrackingResult(id, waitIntervalSec, waitMaxCount); + switch (wtres) { + case TrackingResult::SUCCESS: + case TrackingResult::UNSET: + case TrackingResult::PENDING: + id.setStatus(wtres); + break; + + case TrackingResult::UNKNOWN: + case TrackingResult::FAILURE: + case TrackingResult::INVALID_ARGUMENT: + id.setStatus(wtres); + throw NutException("TRACKING query failed: " + reply); + +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT +# pragma GCC diagnostic ignored "-Wcovered-switch-default" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +# pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +#endif + default: + /* Must not occur. */ + throw NutException("TRACKING query failed: " + reply); +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic pop +#endif + } + } + return id; } else { @@ -2396,20 +2548,18 @@ std::set Device::getRWVariableNames() return getClient()->getDeviceRWVariableNames(getName()); } -void Device::setVariable(const std::string& name, const std::string& value) +TrackingID Device::setVariable(const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - getClient()->setDeviceVariable(getName(), name, value); + return getClient()->setDeviceVariable(getName(), name, value, waitIntervalSec, waitMaxCount); } -void Device::setVariable(const std::string& name, const std::vector& values) +TrackingID Device::setVariable(const std::string& name, const std::vector& values, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - getClient()->setDeviceVariable(getName(), name, values); + return getClient()->setDeviceVariable(getName(), name, values, waitIntervalSec, waitMaxCount); } - - Variable Device::getVariable(const std::string& name) { if (!isOk()) throw NutException("Invalid device"); @@ -2475,10 +2625,10 @@ Command Device::getCommand(const std::string& name) return Command(nullptr, ""); } -TrackingID Device::executeCommand(const std::string& name, const std::string& param) +TrackingID Device::executeCommand(const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - return getClient()->executeDeviceCommand(getName(), name, param); + return getClient()->executeDeviceCommand(getName(), name, param, waitIntervalSec, waitMaxCount); } std::set Device::getClients() @@ -2498,10 +2648,18 @@ void Device::login() void Device::master() { if (!isOk()) throw NutException("Invalid device"); + +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif getClient()->deviceMaster(getName()); +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic pop +#endif } -void Device::primary() +void Device::becomePrimary() { if (!isOk()) throw NutException("Invalid device"); getClient()->devicePrimary(getName()); @@ -2603,14 +2761,14 @@ std::string Variable::getDescription() return getDevice()->getClient()->getDeviceVariableDescription(getDevice()->getName(), getName()); } -void Variable::setValue(const std::string& value) +TrackingID Variable::setValue(const std::string& value, int waitIntervalSec, int waitMaxCount) { - getDevice()->setVariable(getName(), value); + return getDevice()->setVariable(getName(), value, waitIntervalSec, waitMaxCount); } -void Variable::setValues(const std::vector& values) +TrackingID Variable::setValues(const std::vector& values, int waitIntervalSec, int waitMaxCount) { - getDevice()->setVariable(getName(), values); + return getDevice()->setVariable(getName(), values, waitIntervalSec, waitMaxCount); } @@ -2692,9 +2850,9 @@ std::string Command::getDescription() return getDevice()->getClient()->getDeviceCommandDescription(getDevice()->getName(), getName()); } -void Command::execute(const std::string& param) +TrackingID Command::execute(const std::string& param, int waitIntervalSec, int waitMaxCount) { - getDevice()->executeCommand(getName(), param); + return getDevice()->executeCommand(getName(), param, waitIntervalSec, waitMaxCount); } } /* namespace nut */ @@ -3304,7 +3462,14 @@ void nutclient_device_master(NUTCLIENT_t client, const char* dev) { try { +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif cl->deviceMaster(dev); +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic pop +#endif } catch(...){} } @@ -3488,7 +3653,23 @@ void nutclient_set_device_variable_value(NUTCLIENT_t client, const char* dev, co { try { - cl->setDeviceVariable(dev, var, value); + cl->setDeviceVariable(dev, var, value, -1, -1); + } + catch(...){} + } + } +} + +void nutclient_set_device_variable_value_wait(NUTCLIENT_t client, const char* dev, const char* var, const char* value, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + cl->setDeviceVariable(dev, var, value, waitIntervalSec, waitMaxCount); } catch(...){} } @@ -3519,6 +3700,30 @@ void nutclient_set_device_variable_values(NUTCLIENT_t client, const char* dev, c } } +void nutclient_set_device_variable_values_wait(NUTCLIENT_t client, const char* dev, const char* var, const strarr values, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + std::vector vals; + strarr pstr = static_cast(values); + while(*pstr) + { + vals.push_back(std::string(*pstr)); + ++pstr; + } + + cl->setDeviceVariable(dev, var, vals, waitIntervalSec, waitMaxCount); + } + catch(...){} + } + } +} + strarr nutclient_get_device_commands(NUTCLIENT_t client, const char* dev) { if(client) @@ -3579,7 +3784,23 @@ void nutclient_execute_device_command(NUTCLIENT_t client, const char* dev, const { try { - cl->executeDeviceCommand(dev, cmd, param); + cl->executeDeviceCommand(dev, cmd, param, -1, -1); + } + catch(...){} + } + } +} + +void nutclient_execute_device_command_wait(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + cl->executeDeviceCommand(dev, cmd, param, waitIntervalSec, waitMaxCount); } catch(...){} } diff --git a/clients/nutclient.h b/clients/nutclient.h index 20c6d7a2d1..dc95db6e19 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -286,16 +286,12 @@ class TimeoutException : public IOException virtual ~TimeoutException() noexcept override; }; -/** - * Cookie given when performing async action, used to redeem result at a later date. - */ -typedef std::string TrackingID; - /** * Result of an async action. */ typedef enum { + UNSET, UNKNOWN, PENDING, SUCCESS, @@ -303,6 +299,37 @@ typedef enum FAILURE, } TrackingResult; +/** + * Cookie given when performing async action, used to redeem result at a later date. + */ +class TrackingID +{ +public: + TrackingID(const std::string& id = "", TrackingResult status = UNSET) : _id(id), _status(status), _created(std::time(nullptr)), _finished(0) {} + + const std::string& id() const { return _id; } + std::time_t created() const { return _created; } + std::time_t finished() const { return _finished; } + double age() const { return std::difftime(std::time(nullptr), _created); } + double duration() const { if (_finished > 0) { return std::difftime(_finished, _created); } else { return -1; } } + + bool isValid() const { return !_id.empty(); } + bool empty() const { return _id.empty(); } + + void setStatus(TrackingResult status) { _status = status; if (status != TrackingResult::PENDING && status != TrackingResult::UNSET) { _finished = std::time(nullptr); } } + TrackingResult getStatus() const { return _status; } + + operator std::string() const { return _id; } + bool operator==(const TrackingID& other) const { return _id == other._id; } + bool operator<(const TrackingID& other) const { return _id < other._id; } + +private: + std::string _id; + TrackingResult _status; + std::time_t _created; + std::time_t _finished; +}; + typedef std::string Feature; /** @@ -334,6 +361,11 @@ class Client */ virtual void logout() = 0; + /** Query the (already established) connection to UPSD for its version + * and check it against given expectations. + */ + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) = 0; + /** * Device manipulations. * \see nut::Device @@ -426,14 +458,14 @@ class Client * \param name Variable name * \param value Variable value */ - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) = 0; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** * Intend to set the value of a variable. * \param dev Device name * \param name Variable name * \param values Vector of variable values */ - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) = 0; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** \} */ /** @@ -467,7 +499,7 @@ class Client * \param name Command name * \param param Additional command parameter */ - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") = 0; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** \} */ /** @@ -475,7 +507,10 @@ class Client * \{ */ /** - * Log the current user (if authenticated) for a device. + * Log the current user (if authenticated) for a device, + * initially as equivalent to upsmon SECONDARY role for it. + * We can further becomePrimary() if we are the system + * which manages the device. * \param dev Device name. */ virtual void deviceLogin(const std::string& dev) = 0; @@ -491,11 +526,14 @@ class Client * \return List of clients e.g. {'127.0.0.1', 'admin-workstation.local.domain'} */ virtual std::set deviceGetClients(const std::string& dev) = 0; - /* NOTE: "master" is deprecated since NUT v2.8.0 in favor of "primary". + /** NOTE: "master" is deprecated since NUT v2.8.0 in favor of "primary". * For the sake of old/new server/client interoperability, * practical implementations should try to use one and fall * back to the other, and only fail if both return "ERR". */ +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif virtual void deviceMaster(const std::string& dev) = 0; virtual void devicePrimary(const std::string& dev) = 0; virtual void deviceForcedShutdown(const std::string& dev) = 0; @@ -506,12 +544,43 @@ class Client */ virtual std::map> listDeviceClients(void) = 0; + /** + * Retrieve the result of a tracking ID. + * \param id Tracking ID. + */ + virtual TrackingResult getTrackingResult(const std::string id) { return getTrackingResult(TrackingID(id)); } + + /** + * Retrieve the result of a tracking ID. + * \param id Tracking ID. + */ + virtual TrackingResult getTrackingResult(const char *id) { return getTrackingResult(TrackingID(std::string(id))); } + /** * Retrieve the result of a tracking ID. * \param id Tracking ID. */ virtual TrackingResult getTrackingResult(const TrackingID& id) = 0; + /** + * Enable tracking mode once. + */ + virtual void enableTrackingModeOnce(void) = 0; + + /** + * Check if tracking mode is enabled. + */ + virtual bool isTrackingModeEnabled(void) = 0; + + /** + * Wait for a tracking result. + * \param id Tracking ID to wait for. + * \param waitIntervalSec Interval between checks in seconds. + * \param waitMaxCount Maximum number of checks. + * \return The tracking result. + */ + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) = 0; + virtual bool hasFeature(const Feature& feature); virtual bool isFeatureEnabled(const Feature& feature) = 0; virtual void setFeature(const Feature& feature, bool status) = 0; @@ -520,6 +589,7 @@ class Client protected: Client(); + std::string _tracking; }; /** @@ -636,6 +706,8 @@ class TcpClient : public Client virtual void authenticate(const std::string& user, const std::string& passwd) override; virtual void logout() override; + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; + virtual Device getDevice(const std::string& name) override; virtual std::set getDeviceNames() override; virtual std::string getDeviceDescription(const std::string& name) override; @@ -646,17 +718,17 @@ class TcpClient : public Client virtual std::vector getDeviceVariableValue(const std::string& dev, const std::string& name) override; virtual std::map > getDeviceVariableValues(const std::string& dev) override; virtual std::map > > getDevicesVariableValues(const std::set& devs) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual std::set getDeviceCommandNames(const std::string& dev) override; virtual std::string getDeviceCommandDescription(const std::string& dev, const std::string& name) override; - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") override; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; - /* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; virtual void deviceForcedShutdown(const std::string& dev) override; @@ -665,7 +737,12 @@ class TcpClient : public Client virtual std::map> listDeviceClients(void) override; + using Client::getTrackingResult; + virtual TrackingResult getTrackingResult(const TrackingID& id) override; + virtual void enableTrackingModeOnce(void) override; + virtual bool isTrackingModeEnabled(void) override; + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; /** * Return a bitmask of SSL capabilities supported by this build of @@ -727,7 +804,7 @@ class TcpClient : public Client std::string sendQuery(const std::string& req); void sendAsyncQueries(const std::vector& req); static void detectError(const std::string& req); - TrackingID sendTrackingQuery(const std::string& req); + TrackingID sendTrackingQuery(const std::string& req, int waitIntervalSec, int waitMaxCount); std::vector get(const std::string& subcmd, const std::string& params = ""); @@ -898,13 +975,13 @@ class Device * \param name Variable name. * \param value New variable value. */ - void setVariable(const std::string& name, const std::string& value); + TrackingID setVariable(const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Intend to set values of a variable of the device. * \param name Variable name. * \param values Vector of new variable values. */ - void setVariable(const std::string& name, const std::vector& values); + TrackingID setVariable(const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Retrieve a Variable object representing the specified variable. @@ -944,7 +1021,7 @@ class Device * \param name Command name. * \param param Additional command parameter */ - TrackingID executeCommand(const std::string& name, const std::string& param=""); + TrackingID executeCommand(const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0); /** * Login current client's user for the device. @@ -954,11 +1031,12 @@ class Device * Who did a login() to this dev? */ std::set getClients(); - /* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ + +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif void master(); - void primary(); + void becomePrimary(); void forcedShutdown(); /** * Retrieve the number of logged user for the device. @@ -1042,13 +1120,19 @@ class Variable /** * Intend to set a value to the variable. * \param value New variable value. + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void setValue(const std::string& value); + TrackingID setValue(const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Intend to set (multiple) values to the variable. * \param values Vector of new variable values. + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void setValues(const std::vector& values); + TrackingID setValues(const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0); protected: Variable(Device* dev, const std::string& name); @@ -1122,8 +1206,11 @@ class Command /** * Intend to execute the instant command on device. * \param param Optional additional command parameter + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void execute(const std::string& param=""); + TrackingID execute(const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0); protected: Command(Device* dev, const std::string& name); @@ -1216,9 +1303,6 @@ int nutclient_get_device_num_logins(NUTCLIENT_t client, const char* dev); * \param client Nut client handle. * \param dev Device name to test. */ -/* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ void nutclient_device_master(NUTCLIENT_t client, const char* dev); void nutclient_device_primary(NUTCLIENT_t client, const char* dev); @@ -1305,14 +1389,32 @@ strarr nutclient_get_device_variable_values(NUTCLIENT_t client, const char* dev, void nutclient_set_device_variable_value(NUTCLIENT_t client, const char* dev, const char* var, const char* value); /** - * Intend to set device variable multiple values. + * Intend to set device variable value and wait for result. * \param client Nut client handle. * \param dev Device name. * \param var Variable name. - * \param values Values to set. The cller is responsible to free it after call. + * \param value Value to set. + */ +void nutclient_set_device_variable_value_wait(NUTCLIENT_t client, const char* dev, const char* var, const char* value, int waitIntervalSec, int waitMaxCount); + +/** + * Intend to set device variable multiple values. + * \param client Nut client handle. + * \param dev Device name. + * \param var Variable name. + * \param values Values to set. The caller is responsible to free it after call. */ void nutclient_set_device_variable_values(NUTCLIENT_t client, const char* dev, const char* var, const strarr values); +/** + * Intend to set device variable multiple values and wait for result. + * \param client Nut client handle. + * \param dev Device name. + * \param var Variable name. + * \param values Values to set. The caller is responsible to free it after call. + */ +void nutclient_set_device_variable_values_wait(NUTCLIENT_t client, const char* dev, const char* var, const strarr values, int waitIntervalSec, int waitMaxCount); + /** * Intend to retrieve device command names. * \param client Nut client handle. @@ -1347,6 +1449,14 @@ char* nutclient_get_device_command_description(NUTCLIENT_t client, const char* d */ void nutclient_execute_device_command(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param=""); +/** + * Intend to execute device command and wait for result. + * \param client Nut client handle. + * \param dev Device name. + * \param cmd Command name. + */ +void nutclient_execute_device_command_wait(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param="", int waitIntervalSec=-1, int waitMaxCount=-1); + /** \} */ diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 1b8492a06e..8f8dc2638a 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -110,8 +110,11 @@ ListDevice MemClientStub::getDevicesVariableValues(const std::set& return res; } -TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) +TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + auto it_dev = _values.find(dev); if (it_dev == _values.end()) { @@ -134,11 +137,15 @@ TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::s map->emplace(name, list_value); } } - return ""; + + return TrackingID(""); } -TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values) +TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values, int waitIntervalSec, int waitMaxCount) { + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + auto it_dev = _values.find(dev); if (it_dev != _values.end()) { @@ -153,7 +160,8 @@ TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::s map->emplace(name, values); } } - return ""; + + return TrackingID(""); } std::set MemClientStub::getDeviceCommandNames(const std::string& dev) @@ -169,11 +177,19 @@ std::string MemClientStub::getDeviceCommandDescription(const std::string& dev, c throw NutException("Not implemented"); } -TrackingID MemClientStub::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param) +TrackingID MemClientStub::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { NUT_UNUSED_VARIABLE(dev); NUT_UNUSED_VARIABLE(name); NUT_UNUSED_VARIABLE(param); + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + throw NutException("Not implemented"); +} + +bool MemClientStub::isValidProtocolVersion(const std::string& version_re) +{ + NUT_UNUSED_VARIABLE(version_re); throw NutException("Not implemented"); } @@ -230,6 +246,32 @@ TrackingResult MemClientStub::getTrackingResult(const TrackingID& id) //return TrackingResult::SUCCESS; } +void MemClientStub::enableTrackingModeOnce(void) +{ + /* Hush warning: function 'enableTrackingModeOnce' could be declared with attribute 'noreturn' [-Wmissing-noreturn] */ + int id; + NUT_UNUSED_VARIABLE(id); + throw NutException("Not implemented"); + //return TrackingResult::SUCCESS; +} + +bool MemClientStub::isTrackingModeEnabled(void) { + /* Hush warning: function 'enableTrackingModeOnce' could be declared with attribute 'noreturn' [-Wmissing-noreturn] */ + int id; + NUT_UNUSED_VARIABLE(id); + throw NutException("Not implemented"); + //return false; +} + +TrackingResult MemClientStub::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) +{ + NUT_UNUSED_VARIABLE(id); + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + throw NutException("Not implemented"); + //return TrackingResult::SUCCESS; +} + bool MemClientStub::isFeatureEnabled(const Feature& feature) { NUT_UNUSED_VARIABLE(feature); diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 8584a9ca45..747f52b2ca 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -51,6 +51,8 @@ class MemClientStub : public Client } virtual void logout() override {} + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; + virtual Device getDevice(const std::string& name) override; virtual std::set getDeviceNames() override; virtual std::string getDeviceDescription(const std::string& name) override; @@ -61,15 +63,15 @@ class MemClientStub : public Client virtual ListValue getDeviceVariableValue(const std::string& dev, const std::string& name) override; virtual ListObject getDeviceVariableValues(const std::string& dev) override; virtual ListDevice getDevicesVariableValues(const std::set& devs) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values, int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual std::set getDeviceCommandNames(const std::string& dev) override; virtual std::string getDeviceCommandDescription(const std::string& dev, const std::string& name) override; - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") override; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; - /* Note: "master" is deprecated, but supported + /** Note: "master" is deprecated, but supported * for mixing old/new client/server combos: */ virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; @@ -78,7 +80,12 @@ class MemClientStub : public Client virtual std::set deviceGetClients(const std::string& dev) override; virtual std::map> listDeviceClients(void) override; + using Client::getTrackingResult; + virtual TrackingResult getTrackingResult(const TrackingID& id) override; + virtual void enableTrackingModeOnce(void) override; + virtual bool isTrackingModeEnabled(void) override; + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; virtual bool isFeatureEnabled(const Feature& feature) override; virtual void setFeature(const Feature& feature, bool status) override; diff --git a/clients/upsclient.c b/clients/upsclient.c index 381b1252d8..03c02cad63 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1253,8 +1253,6 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) upsdebugx(3, "%s: Succeeded to STARTTLS (OpenSSL)", __func__); - return 1; - # elif defined(WITH_NSS) /* WITH_OPENSSL */ socket = PR_ImportTCPSocket(ups->fd); @@ -1344,9 +1342,22 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) upsdebugx(3, "%s: Succeeded to STARTTLS (NSS)", __func__); - return 1; - # endif /* WITH_OPENSSL | WITH_NSS */ + + /* Make sure handshake succeeded or abort early + * (there is currently no way for the server to + * report its fault to the client when connection + * is half-way secure): + */ + if (!upscli_is_valid_protocol_version(ups, NULL)) { + upslogx(LOG_WARNING, "%s: STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required", __func__); + ups->ssl = NULL; + /* Reaction to forceSSL etc. is up to the caller */ + /* TODO: Should caller drop SSL context or restart the connection as plaintext if SSL is not required? */ + return -2; + } + + return 1; #endif /* WITH_SSL */ } @@ -1584,7 +1595,8 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags ups->upserror = UPSCLI_ERR_SSLFAIL; upscli_disconnect(ups); return -1; - } else if (tryssl && ret == -1) { + } else if (tryssl && ret < 0) { + /* TODO: (ret == -2) Drop SSL context or restart the connection as plaintext if SSL is not required? */ upslogx(LOG_NOTICE, "Error while connecting to NUT server %s, disconnect", host); upscli_disconnect(ups); return -1; @@ -2163,6 +2175,60 @@ int upscli_splitaddr(const char *buf, char **hostname, uint16_t *port) return 0; } +int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re) +{ + char version[UPSCLI_NETBUF_LEN]; + size_t len; + + if (!ups) { + return -1; + } + + net_write(ups, "PROTVER\n", 8, 0); + memset(version, 0, sizeof(version)); + if (net_read(ups, version, sizeof(version), DEFAULT_NETWORK_TIMEOUT) > 0) { + if (!strncmp(version, "ERR", 3)) { + version[0] = '\0'; + } + } + + if (!version[0]) { + /* Deprecated and hidden, but may be what ancient NUT servers say + * May throw if the error is due to (non-)connection */ + net_write(ups, "NETVER\n", 8, 0); + memset(version, 0, sizeof(version)); + if (net_read(ups, version, sizeof(version), DEFAULT_NETWORK_TIMEOUT) > 0) { + if (!strncmp(version, "ERR", 3)) { + version[0] = '\0'; + } + } + } + + if (!version[0]) { + upsdebugx(3, "%s: PROTVER and NETVER queries returned an error, assuming disconnection or non-compliant NUT server", __func__); + return -1; + } + + len = strlen(version); + if (len > 0 && version[len-1] == '\n') { + version[len-1] = '\0'; + } + + upsdebugx(3, "%s: PROTVER or NETVER returned '%s', matching against '%s'", + __func__, version, NUT_STRARG(version_re)); + + if (!version_re) { + /* Basic check for 1.0 through 1.3, as of NUT v2.8.2 */ + return ( + !strcmp(version, "1.0") || !strcmp(version, "1.1") || + !strcmp(version, "1.2") || !strcmp(version, "1.3") + ); + } + + // TODO: Regex + return (!strcmp(version_re, version)); +} + int upscli_disconnect(UPSCONN_t *ups) { char tmp[UPSCLI_NETBUF_LEN]; diff --git a/clients/upsclient.h b/clients/upsclient.h index e84c28c5f0..f10a93b41e 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -172,6 +172,10 @@ int upscli_disconnect(UPSCONN_t *ups); int upscli_fd(UPSCONN_t *ups); int upscli_upserror(UPSCONN_t *ups); +/** Query the (already established) connection to UPSD for its version + * and check it against given expectations. */ +int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re); + /* returns 1 if SSL mode is active for this connection */ int upscli_ssl(UPSCONN_t *ups); diff --git a/m4/ax_c_pragmas.m4 b/m4/ax_c_pragmas.m4 index b93b63dede..18da10ee93 100644 --- a/m4/ax_c_pragmas.m4 +++ b/m4/ax_c_pragmas.m4 @@ -1120,6 +1120,33 @@ dnl ### [CFLAGS="${CFLAGS_SAVED} -Werror=pragmas -Werror=unknown-warning" AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_EXIT_TIME_DESTRUCTORS_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wexit-time-destructors" (outside functions)]) ]) + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wdeprecated-declarations"], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[void func(void) { +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +} +]], [])], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations=yes], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_deprecated_declarations" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wdeprecated-declarations"]) + ]) + + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wdeprecated-declarations" (outside functions)], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#pragma GCC diagnostic ignored "-Wdeprecated-declarations"]], [])], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc=yes], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wdeprecated-declarations" (outside functions)]) + ]) + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wsuggest-override" (outside functions)], [ax_cv__pragma__gcc__diags_ignored_suggest_override_besidefunc], [AC_COMPILE_IFELSE( diff --git a/scripts/HP-UX/nut.psf.in b/scripts/HP-UX/nut.psf.in index feb98a5971..926ba924b7 100644 --- a/scripts/HP-UX/nut.psf.in +++ b/scripts/HP-UX/nut.psf.in @@ -133,7 +133,7 @@ product title "libups-nut-perl" revision @PACKAGE_VERSION@ - file -u 644 -g bin -o bin @top_srcdir@/scripts/perl/Nut.pm @prefix@/share/perl5/UPS/Nut.pm + file -u 644 -g bin -o bin @top_srcdir@/scripts/perl/UPS/Nut.pm @prefix@/share/perl5/UPS/Nut.pm end # ---------------------------------------- diff --git a/scripts/Makefile.am b/scripts/Makefile.am index d24c20adb7..09565e864e 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -12,7 +12,8 @@ EXTRA_DIST = \ logrotate/nutlogd \ misc/nut.bash_completion.in \ misc/osd-notify \ - perl/Nut.pm \ + perl/UPS/Nut.pm \ + perl/test_nutclient.pl \ RedHat/halt.patch \ RedHat/README.adoc \ RedHat/ups.in \ diff --git a/scripts/obs/debian.libups-nut-perl.install b/scripts/obs/debian.libups-nut-perl.install index 355c6fe0a5..5c141c73c0 100644 --- a/scripts/obs/debian.libups-nut-perl.install +++ b/scripts/obs/debian.libups-nut-perl.install @@ -1 +1 @@ -scripts/perl/Nut.pm /usr/share/perl5/UPS/ +scripts/perl/UPS/Nut.pm /usr/share/perl5/UPS/ diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm deleted file mode 100644 index 9ddefd50d6..0000000000 --- a/scripts/perl/Nut.pm +++ /dev/null @@ -1,964 +0,0 @@ -# UPS::Nut - a class to talk to a UPS via the Network Utility Tools upsd. -# Original author Kit Peters -# Rewritten by Gabor Kiss -# Idea to implement TLS:http://www.logix.cz/michal/devel/smtp-cli/smtp-client.pl - -# ### changelog: made debug messages slightly more descriptive, improved -# ### changelog: comments in code -# ### changelog: Removed timeleft() function. - -package UPS::Nut; -use strict; -use Carp; -use FileHandle; -use IO::Socket; -use IO::Select; -use Dumpvalue; my $dumper = Dumpvalue->new; - -# The following globals dictate whether the accessors and instant-command -# functions are created. -# ### changelog: tie hash interface and AUTOLOAD contributed by -# ### changelog: Wayne Wylupski - -my $_eol = "\n"; - -BEGIN { - use Exporter (); - use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - $VERSION = 1.51; - @ISA = qw(Exporter IO::Socket::INET); - @EXPORT = qw(); - @EXPORT_OK = qw(); - %EXPORT_TAGS = (); -} - -sub new { -# Author: Kit Peters - my $proto = shift; - my $class = ref($proto) || $proto; - my %arg = @_; # hash of arguments - my $self = {}; # _initialize will fill it later - bless $self, $class; - unless ($self->_initialize(%arg)) { # can't initialize - carp "Can't initialize: $self->{err}"; - return undef; - } - return $self; -} - -# accessor functions. Return a value if successful, return undef -# otherwise. - -sub BattPercent { # get battery percentage - return shift->GetVar('battery.charge'); -} - -sub LoadPercent { # get load percentage - my $self = shift; - my $context = shift; - $context = "L$context" if $context =~ /^[123]$/; - $context = ".$context" if $context; - return $self->GetVar("output$context.power.percent"); -} - -sub LineVoltage { # get line voltage - my $self = shift; - my $context = shift; - $context = "L$context-N" if $context =~ /^[123]$/; - $context = ".$context" if $context; - return $self->GetVar("input$context.voltage"); -} - -sub Status { # get status of UPS - return shift->GetVar('ups.status'); -} - -sub Temperature { # get the internal temperature of UPS - return shift->GetVar('battery.temperature'); -} - -# control functions: they control our relationship to upsd, and send -# commands to upsd. - -sub Login { # login to upsd, so that it won't shutdown unless we say we're - # ok. This should only be used if you're actually connected - # to the ups that upsd is monitoring. - -# Author: Kit Peters -# ### changelog: modified login logic a bit. Now it doesn't check to see -# ### changelog: if we got OK, ERR, or something else from upsd. It -# ### changelog: simply checks for a response beginning with OK from upsd. -# ### changelog: Anything else is an error. -# -# ### changelog: uses the new _send command -# - my $self = shift; # myself - my $user = shift; # username - my $pass = shift; # password - my $errmsg; # error message, sent to _debug and $self->{err} - my $ans; # scalar to hold responses from upsd - - $self->Authenticate($user, $pass) or return; - $ans = $self->_send( "LOGIN $self->{name}" ); - if (defined $ans && $ans =~ /^OK/) { # Login successful. - $self->_debug("LOGIN successful."); - return 1; - } - if (defined $ans) { - $errmsg = "LOGIN failed. Last message from upsd: $ans"; - } - else { - $errmsg = "Network error: $!"; - } - $self->_debug($self->{err} = $errmsg); - return undef; -} - -sub Authenticate { # Announce to the UPS who we are to set up the proper - # management level. See upsd.conf man page for details. - -# Contributor: Wayne Wylupski - my $self = shift; # myself - my $user = shift; # username - my $pass = shift; # password - - my $errmsg; # error message, sent to _debug and $self->{err} - my $ans; # scalar to hold responses from upsd - - # only attempt authentication if username and password given - if (defined $user and defined $pass) { - $ans = $self->_send("USERNAME $user"); - if (defined $ans && $ans =~ /^OK/) { # username OK, send password - - $ans = $self->_send("PASSWORD $pass"); - return 1 if (defined $ans && $ans =~ /^OK/); - } - } - if (defined $ans) { - $errmsg = "Authentication failed. Last message from upsd: $ans"; - } - else { - $errmsg = "Network error: $!"; - } - $self->_debug($self->{err} = $errmsg); - return undef; -} - -sub Logout { # logout of upsd -# Author: Kit Peters -# ### changelog: uses the new _send command -# - my $self = shift; - if ($self->{srvsock}) { # are we still connected to upsd? - my $ans = $self->_send( "LOGOUT" ); - close ($self->{srvsock}); - delete ($self->{srvsock}); - } -} - -# internal functions. These are only used by UPS::Nut internally, so -# please don't use them otherwise. If you really think an internal -# function should be externalized, let me know. - -sub _initialize { -# Author: Kit Peters - my $self = shift; - my %arg = @_; - my $host = $arg{HOST} || 'localhost'; # Host running upsd and probably drivers - my $port = $arg{PORT} || '3493'; # 3493 is IANA assigned port for NUT - my $proto = $arg{PROTO} || 'tcp'; # use tcp unless user tells us to - my $user = $arg{USERNAME} || undef; # username passed to upsd - my $pass = $arg{PASSWORD} || undef; # password passed to upsd - my $login = $arg{LOGIN} || 0; # login to upsd on init? - - - $self->{name} = $arg{NAME} || 'default'; # UPS name in etc/ups.conf on $host - $self->{timeout} = $arg{TIMEOUT} || 30; # timeout - $self->{debug} = $arg{DEBUG} || 0; # debugging? - $self->{debugout} = $arg{DEBUGOUT} || undef; # where to send debug messages - - my $srvsock = $self->{srvsock} = # establish connection to upsd - IO::Socket::INET->new( - PeerAddr => $host, - PeerPort => $port, - Proto => $proto - ); - - unless ( defined $srvsock) { # can't connect - $self->{err} = "Unable to connect via $proto to $host:$port: $!"; - return undef; - } - - $self->{select} = IO::Select->new( $srvsock ); - - if ($user and $pass) { # attempt login to upsd if that option is specified - if ($login) { # attempt login to upsd if that option is specified - $self->Login($user, $pass) or carp $self->{err}; - } - else { - $self->Authenticate($user, $pass) or carp $self->{err}; - } - } - - # get a hash of vars for both the TIE functions as well as for - # expanding vars. - $self->{vars} = $self->ListVar; - - unless ( defined $self->{vars} ) { - $self->{err} = "Network error: $!"; - return undef; - } - - return $self; -} - -# -# _send -# -# Sends a command to the server and retrieves the results. -# If there was a network error, return undef; $! will contain the -# error. -sub _send -{ -# Contributor: Wayne Wylupski - my $self = shift; - my $cmd = shift; - my @handles; - my $result; # undef by default - - my $socket = $self->{srvsock}; - my $select = $self->{select}; - - @handles = IO::Select->select( undef, $select, $select, $self->{timeout} ); - return undef if ( !scalar $handles[1] ); - - $socket->print( $cmd . $_eol ); - - @handles = IO::Select->select( $select, undef, $select, $self->{timeout} ); - return undef if ( !scalar $handles[0]); - - $result = $socket->getline; - return undef if ( !defined ( $result ) ); - chomp $result; - - return $result; -} - -sub _getline -{ -# Contributor: Wayne Wylupski - my $self = shift; - my $result; # undef by default - - my $socket = $self->{srvsock}; - my $select = $self->{select}; - - # Different versions of IO::Socket has different error detection routines. - return undef if ( $IO::Socket::{has_error} && $select->has_error(0) ); - return undef if ( $IO::Socket::{has_exception} && $select->has_exception(0) ); - - chomp ( $result = $socket->getline ); - return $result; -} - -# Compatibility layer -sub Request { goto &GetVar; } - -sub GetVar { # request a variable from the UPS -# Author: Kit Peters - my $self = shift; -# ### changelog: 8/3/2002 - KP - Request() now returns undef if not -# ### changelog: connected to upsd via $srvsock -# ### changelog: uses the new _send command -# -# Modified by Gabor Kiss according to protocol version 1.5+ - my $var = shift; - my $req = "GET VAR $self->{name} $var"; # build request - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans. Requested $var."; - return undef; - } - elsif ($ans =~ /^VAR/) { - my $checkvar; # to make sure the var we asked for is the var we got. - my $retval; # returned value for requested VAR - (undef, undef, $checkvar, $retval) = split(' ', $ans, 4); - # get checkvar and retval from the answer - if ($checkvar ne $var) { # did not get expected var - $self->{err} = "requested $var, received $checkvar"; - return undef; - } - $retval =~ s/^"(.*)"$/$1/; - return $retval; # return the requested value - } - else { # unrecognized response - $self->{err} = "Unrecognized response from upsd: $ans"; - return undef; - } -} - -sub Set { -# Contributor: Wayne Wylupski -# ### changelog: uses the new _send command -# - my $self = shift; - my $var = shift; - (my $value = shift) =~ s/^"?(.*)"?$/"$1"/; # add quotes if missing - - my $req = "SET VAR $self->{name} $var $value"; # build request - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; - return undef; - } - elsif ($ans =~ /^OK/) { - return $value; - } - else { # unrecognized response - $self->{err} = "Unrecognized response from upsd: $ans"; - return undef; - } -} - -sub FSD { # set forced shutdown flag -# Author: Kit Peters -# ### changelog: uses the new _send command -# - my $self = shift; - - my $req = "FSD $self->{name}"; # build request - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { # can't set forced shutdown flag - $self->{err} = "Can't set FSD flag. Upsd reports: $ans"; - return undef; - } - elsif ($ans =~ /^OK FSD-SET/) { # forced shutdown flag set - $self->_debug("FSD flag set successfully."); - return 1; - } - else { - $self->{err} = "Unrecognized response from upsd: $ans"; - return undef; - } -} - -sub InstCmd { # send instant command to ups -# Contributor: Wayne Wylupski - my $self = shift; - - chomp (my $cmd = shift); - - my $req = "INSTCMD $self->{name} $cmd"; - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { # error reported from upsd - $self->{err} = "Can't send instant command $cmd. Reason: $ans"; - return undef; - } - elsif ($ans =~ /^OK/) { # command successful - $self->_debug("Instant command $cmd sent successfully."); - return 1; - } - else { # unrecognized response - $self->{err} = "Can't send instant command $cmd. Unrecognized response from upsd: $ans"; - return undef; - } -} - -sub ListUPS { - my $self = shift; - return $self->_get_list("LIST UPS", 2, 1); -} - -sub ListVar { - my $self = shift; - my $vars = $self->_get_list("LIST VAR $self->{name}", 3, 2); - return $vars unless @_; # return all variables - return {map { $_ => $vars->{$_} } @_}; # return selected ones -} - -sub ListRW { - my $self = shift; - return $self->_get_list("LIST RW $self->{name}", 3, 2); -} - -sub ListCmd { - my $self = shift; - return $self->_get_list("LIST CMD $self->{name}", 2); -} - -sub ListEnum { - my $self = shift; - my $var = shift; - return $self->_get_list("LIST ENUM $self->{name} $var", 3); -} - -sub _get_list { - my $self = shift; - my ($req, $valueidx, $keyidx) = @_; - my $ans = $self->_send($req); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; - return undef; - } - elsif ($ans =~ /^BEGIN LIST/) { # command successful - my $retval = $keyidx ? {} : []; - my $line; - while ($line = $self->_getline) { - last if $line =~ /^END LIST/; - my @fields = split(' ', $line, $valueidx+1); - (my $value = $fields[$valueidx]) =~ s/^"(.*)"$/$1/; - if ($keyidx) { - $retval->{$fields[$keyidx]} = $value; - } - else { - push(@$retval, $value); - } - } - unless ($line) { - $self->{err} = "Network error: $!"; - return undef; - }; - $self->_debug("$req command sent successfully."); - return $retval; - } - else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; - return undef; - } -} - -# Compatibility layer -sub VarDesc { goto &GetDesc; } - -sub GetDesc { -# Contributor: Wayne Wylupski -# Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $var = shift; - - my $req = "GET DESC $self->{name} $var"; - my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; - return undef; - } - elsif ($ans =~ /^DESC/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - $ans =~ s/^"(.*)"$/$1/; - return $ans; - } - else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; - return undef; - } -} - -# Compatibility layer -sub VarType { goto &GetType; } - -sub GetType { -# Contributor: Wayne Wylupski -# Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $var = shift; - - my $req = "GET TYPE $self->{name} $var"; - my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; - return undef; - } - elsif ($ans =~ /^TYPE/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - return $ans; - } - else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; - return undef; - } -} - -# Compatibility layer -sub InstCmdDesc { goto &GetCmdDesc; } - -sub GetCmdDesc { -# Contributor: Wayne Wylupski -# Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $cmd = shift; - - my $req = "GET CMDDESC $self->{name} $cmd"; - my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; - return undef; - } - elsif ($ans =~ /^DESC/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - $ans =~ s/^"(.*)"$/$1/; - return $ans; - } - else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; - return undef; - } -} - -sub DESTROY { # destructor, all it does is call Logout -# Author: Kit Peters - my $self = shift; - $self->_debug("Object destroyed."); - $self->Logout(); -} - -sub _debug { # print debug messages to stdout or file -# Author: Kit Peters - my $self = shift; - if ($self->{debug}) { - chomp (my $msg = shift); - my $out; # filehandle for output - if ($self->{debugout}) { # if filename is given, use that - $out = new FileHandle ($self->{debugout}, ">>") or warn "Error: $!"; - } - if ($out) { # if out was set to a filehandle, create nifty timestamp - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); - $year = sprintf("%02d", $year % 100); # Y2.1K compliant, even! - my $timestamp = join '/', ($mon + 1), $mday, $year; # today - $timestamp .= " "; - $timestamp .= join ':', $hour, $min, $sec; - print $out "$timestamp $msg\n"; - } - else { print "DEBUG: $msg\n"; } # otherwise, print to stdout - } -} - -sub Error { # what was the last thing that went bang? -# Author: Kit Peters - my $self = shift; - if ($self->{err}) { return $self->{err}; } - else { return "No error explanation available."; } -} - -sub Master { # check for MASTER level access -# Author: Kit Peters -# ### changelog: uses the new _send command -# -# TODO: API change pending to replace MASTER with PRIMARY -# (and backwards-compatible alias handling) - my $self = shift; - - my $req = "MASTER $self->{name}"; # build request - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^OK/) { # access granted - $self->_debug("MASTER level access granted. Upsd reports: $ans"); - return 1; - } - else { # access denied, or unrecognized reponse - $self->{err} = "MASTER level access denied. Upsd responded: $ans"; -# ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on -# ### failure. this makes it consistent with other methods - return undef; - } -} - -sub AUTOLOAD { -# Contributor: Wayne Wylupski - my $self = shift; - my $name = $UPS::Nut::AUTOLOAD; - $name =~ s/^.*:://; - - # for a change we will only load cmds if needed. - if (!defined $self->{cmds} ) { - %{$self->{cmds}} = map{ $_ =>1 } @{$self->ListCmd}; - } - - croak "No such InstCmd: $name" if (! $self->{cmds}{$name} ); - - return $self->InstCmd( $name ); -} - -#------------------------------------------------------------------------- -# tie hash interface -# -# The variables of the array, including the hidden 'numlogins' can -# be accessed as a hash array through this method. -# -# Example: -# tie %ups, 'UPS::Nut', -# NAME => "my-ups", -# HOST => "some-machine.somewhere.com", -# ... # same options as new(); -# ; -# -# $ups{UPSIDENT} = "MyUPS"; -# print $ups{MFR}, " " $ups{MODEL}, "\n"; -# -#------------------------------------------------------------------------- -sub TIEHASH { - my $class = shift || 'UPS::Nut'; - return $class->new( @_ ); -} - -sub FETCH { - my $self = shift; - my $key = shift; - - return $self->Request( $key ); -} - -sub STORE { - my $self = shift; - my $key = shift; - my $value = shift; - - return $self->Set( $key, $value ); -} - -sub DELETE { - croak "DELETE operation not supported"; -} - -sub CLEAR { - croak "CLEAR operation not supported"; -} - -sub EXISTS { - exists shift->{vars}{shift}; -} - -sub FIRSTKEY { - my $self = shift; - my $a = keys %{$self->{vars}}; - return scalar each %{$self->{vars}}; -} - -sub NEXTKEY { - my $self = shift; - return scalar each %{$self->{vars}}; -} - -sub UNTIE { - $_[0]->Logout; -} - -=head1 NAME - -Nut - a module to talk to a UPS via NUT (Network UPS Tools) upsd - -=head1 SYNOPSIS - - use UPS::Nut; - - $ups = new UPS::Nut( NAME => "my-ups", - HOST => "some-machine.somewhere.com", - PORT => "3493", - USERNAME => "upsuser", - PASSWORD => "upspasswd", - TIMEOUT => 30, - DEBUG => 1, - DEBUGOUT => "/some/file/somewhere", - ); - if ($ups->Status() =~ /OB/) { - print "Oh, no! Power failure!\n"; - } - - tie %other_ups, 'UPS::Nut', - NAME => "my-ups", - HOST => "some-machine.somewhere.com", - ... # same options as new(); - ; - - print $other_ups{MFR}, " ", $other_ups{MODEL}, "\n"; - -=head1 DESCRIPTION - -This is an object-oriented (whoo!) interface between Perl and upsd from -the Network UPS Tools package version 1.5 and above -(https://www.networkupstools.org/). -Note that it only talks to upsd for you in a Perl-ish way. -It does not monitor the UPS continuously. - -=head1 CONSTRUCTOR - -Shown with defaults: new UPS::Nut( NAME => "default", - HOST => "localhost", - PORT => "3493", - USERNAME => "", - PASSWORD => "", - DEBUG => 0, - DEBUGOUT => "", - ); -* NAME is the name of the UPS to monitor, as specified in ups.conf -* HOST is the host running upsd -* PORT is the port that upsd is running on -* USERNAME and PASSWORD are those that you use to login to upsd. This - gives you the right to do certain things, as specified in upsd.conf. -* DEBUG turns on debugging output, set to 1 or 0 -* DEBUGOUT is de thing you do when de s*** hits the fan. Actually, it's - the filename where you want debugging output to go. If it's not - specified, debugging output comes to standard output. - -=head1 Important notice - -This version of UPS::Nut is not compatible with version 0.04. It is totally -rewritten in order to talk the new protocol of NUT 1.5+. You should not use -this module as a drop-in replacement of previous version from 2002. -Almost all method has changed slightly. - -=head1 Methods - -Unlike in version 0.04 no methods return list values but a -single reference or undef. - -=head2 Methods for querying UPS status - -=over 4 - -=item Getvar($varname) - -returns value of the specified variable. Returns undef if variable -unsupported. -Old method named Request() is also supported for compatibility. - -=item Set($varname, $value) - -sets the value of the specified variable. Returns undef if variable -unsupported, or if variable cannot be set for some other reason. See -Authenticate() if you wish to use this function. - -=item BattPercent() - -returns percentage of battery left. Returns undef if we can't get -battery percentage for some reason. Same as GetVar('battery.charge'). - -=item LoadPercent($context) - -returns percentage of the load on the UPS. Returns undef if load -percentage is unavailable. $context is a selector of 3 phase systems. -Possible values are 1, 2, 3, 'L1', 'L2', 'L3'. It should be omitted -in case of single phase UPS. - -=item LineVoltage($context) - -returns input line (e.g. the outlet) voltage. Returns undef if line -voltage is unavailable. $context is a selector of 3 phase systems. -Possible values are 1, 2, 3, 'L1', 'L2', 'L3'. It should be omitted -in case of single phase UPS. - -=item Status() - -returns UPS status, one of OL or OB. OL or OB may be followed by LB, -which signifies low battery state. OL or OB may also be followed by -FSD, which denotes that the forced shutdown state -( see UPS::Nut->FSD() ) has been set on upsd. Returns undef if status -unavailable. Same as GetVar('ups.status'). - -=item Temperature() - -returns UPS internal temperature. Returns undef if internal -temperature unavailable. Same as GetVar('battery.temperature'). - -=back - -=head2 Other methods - -These all operate on the UPS specified in the NAME argument to the -constructor. - -=over 4 - -=item Authenticate($username, $password) - -With NUT certain operations are only available if the user has the -privilege. The program has to authenticate with one of the accounts -defined in upsd.conf. - -=item Login($username, $password) - -Notify upsd that client is drawing power from the given UPS. -It is automatically done if new() is called with USERNAME, PASSWORD -and LOGIN parameters. - -=item Logout() - -Notify upsd that client is released UPS. (E.g. it is shutting down.) -It is automatically done if connection closed. - -=item Master() - -Use this to find out whether or not we have MASTER privileges for -this UPS. Returns 1 if we have MASTER privileges, returns 0 otherwise. - -TODO: API change pending to replace MASTER with PRIMARY -(and backwards-compatible alias handling) - -=item ListVar($variable, ...) - -This is an implementation of "LIST VAR" command. -Returns a hash reference to selected variable names and values supported -by the UPS. If no variables given it returns all. -Returns undef if "LIST VAR" failed. -(Note: This method significantly differs from the old ListVars() -and ListRequest().) - -=item ListRW() - -Similar to ListVar() but cares only with read/writeable variables. - -=item ListEnum($variable) - -Returns a reference to the list of all possible values of $variable. -List is empty if $variable is not an ENUM type. (See GetType().) -Returns undef if error occurred. - -=item ListCmd() - -Returns a reference to the list of all instant commands supported -by the UPS. Returns undef if these are unavailable. -This method replaces the old ListInstCmds(). - -=item InstCmd($command) - -Send an instant command to the UPS. Returns 1 on success. Returns -undef if the command can't be completed. - -=item FSD() - -Set the FSD (forced shutdown) flag for the UPS. This means that we're -planning on shutting down the UPS very soon, so the attached load should -be shut down as well. Returns 1 on success, returns undef on failure. -This cannot be unset, so don't set it unless you mean it. - -=item Error() - -why did the previous operation fail? The answer is here. It will -return a concise, well-written, and brilliantly insightful few words as -to why whatever you just did went bang. - -=item GetDesc($variable) - -Returns textual description of $variable or undef in case of error. -Old method named VarDesc() is also supported for compatibility. - -=item GetCmdDesc($command) - -This is like GetDesc() above but applies to the instant commands. -Old method named InstCmdDesc() is also supported for compatibility. - -=item GetType($variable) - -Returns a string UNKNOWN or constructed one or more words of RW, -ENUM and STRING:n (where n is a number). (Seems to be not working -perfectly at upsd 2.2.) -Old method named VarType() is also supported for compatibility. - -=item ListUPS() - -Returns a reference to hash of all available UPS names and descriptions. - -=back - -=head1 AUTOLOAD - -The "instant commands" are available as methods of the UPS object. They -are AUTOLOADed when called. For example, if the instant command is FPTEST, -then it can be called by $ups->FPTEST. - -=head1 TIE Interface - -If you wish to simply query or set values, you can tie a hash value to -UPS::Nut and pass as extra options what you need to connect to the host. -If you need to exercise an occasional command, you may find the return -value of 'tie' useful, as in: - - my %ups; - my $ups_obj = tie %ups, 'UPS::Nut', HOSTNAME=>"firewall"; - - print $ups{UPSIDENT}, "\n"; - - $ups_obj->Authenticate( "user", "pass" ); - - $ups{UPSIDENT} = "MyUPS"; - -=head1 AUTHOR - - Original version made by Kit Peters - perl@clownswilleatyou.com - http://www.awod.com/staff/kpeters/perl/ - - Rewritten by Gabor Kiss . - -=head1 CREDITS - -Developed with the kind support of A World Of Difference, Inc. - - -Many thanks to Ryan Jessen at CyberPower -Systems for much-needed assistance. - -Thanks to Wayne Wylupski for the code to make -accessor methods for all supported vars. - -=head1 LICENSE - -This module is distributed under the same license as Perl itself. - -=cut - -1; -__END__ - diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm new file mode 100644 index 0000000000..c5f236e100 --- /dev/null +++ b/scripts/perl/UPS/Nut.pm @@ -0,0 +1,1455 @@ +# UPS::Nut - a class to talk to a UPS via the Network Utility Tools upsd. +# Original author Kit Peters +# Rewritten by Gabor Kiss +# Idea to implement TLS:http://www.logix.cz/michal/devel/smtp-cli/smtp-client.pl + +# ### changelog: made debug messages slightly more descriptive, improved +# ### changelog: comments in code +# ### changelog: Removed timeleft() function. +# ### changelog: 1.60: JK 2026-04-08: Added basic STARTTLS, as well as TRACKING support, LIST CLIENT, LIST RANGE, GET UPSDESC and PRIMARY/MASTER aliasing. +# ### changelog: 1.61: JK 2026-04-08: Make TrackingID a class, similar to C++. +# ### changelog: 1.62: JK 2026-04-10: Added a testing script nearby; revised API and NUT protocol support, notably TRACKING and STARTTLS. + +package UPS::Nut; +use strict; +use warnings FATAL => 'all'; +use Carp; +use FileHandle; +use IO::Socket; +use IO::Select; +use Dumpvalue; my $dumper = Dumpvalue->new; + +# The following globals dictate whether the accessors and instant-command +# functions are created. +# ### changelog: tie hash interface and AUTOLOAD contributed by +# ### changelog: Wayne Wylupski + +my $_eol = "\n"; + +BEGIN { + use Exporter (); + use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + $VERSION = 1.62; + @ISA = qw(Exporter IO::Socket::INET); + @EXPORT = qw(); + @EXPORT_OK = qw(); + %EXPORT_TAGS = (); +} + +sub new { +# Author: Kit Peters + my $proto = shift; + my $class = ref($proto) || $proto; + my %arg = @_; # hash of arguments + my $self = {}; # _initialize will fill it later + bless $self, $class; + unless ($self->_initialize(%arg)) { # can't initialize + carp "Can't initialize: $self->{err}"; + return undef; + } + return $self; +} + +# accessor functions. Return a value if successful, return undef +# otherwise. + +sub BattPercent { # get battery percentage + return shift->GetVar('battery.charge'); +} + +sub LoadPercent { # get load percentage + my $self = shift; + my $context = shift; + $context = "L$context" if $context =~ /^[123]$/; + $context = ".$context" if $context; + return $self->GetVar("output$context.power.percent"); +} + +sub LineVoltage { # get line voltage + my $self = shift; + my $context = shift; + $context = "L$context-N" if $context =~ /^[123]$/; + $context = ".$context" if $context; + return $self->GetVar("input$context.voltage"); +} + +sub Status { # get status of UPS + return shift->GetVar('ups.status'); +} + +sub Temperature { # get the internal temperature of UPS + return shift->GetVar('battery.temperature'); +} + +# control functions: they control our relationship to upsd, and send +# commands to upsd. + +sub SetTrackingMode { + # Enable/disable TRACKING ability for SETVAR/INSTCMD ('ON'/'OFF') + # Remember in $self->{tracking} if we could set it with this upsd + # version, and then to which value? + my $self = shift; + my $value = shift; + my $ans; # scalar to hold responses from upsd + + # 'ON'/'OFF'/undef + if (!(defined $value)) { + $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested: undef"); + return undef; + } + + if ($value ne 'ON' && $value ne 'OFF') { + $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested: '$value'"); + return undef; + } + + $ans = $self->_send("SET TRACKING $value"); + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^OK/) { + $self->{tracking} = $value; + return $value; + } + + $self->{tracking} = undef; + $self->_debug($self->{err} = "Error: $ans"); + return undef; +} + +sub EnableTrackingModeOnce { + my $self = shift; + + if (defined $self->{tracking} && $self->{tracking} eq 'ON') { + return 1; + } + + my $actualMode = $self->SetTrackingMode('ON'); + if (defined $actualMode && $actualMode eq 'ON') { + return 1; + } + + # Unsupported by server? Other errors? + return undef; +} + +sub isValidProtocolVersion { + # Sends a PROTVER/NETVER query using the active connection and + # returns True if the returned version string matches a valid + # NUT protocol version regex (defaults to an "X(.Y)" number aka + # "^\\d+(?:\\.\\d+)?$" if version_re is None). + my ($self, $version_re) = @_; + + my $ans = $self->_send("PROTVER"); + if (!defined $ans) { + # Deprecated and hidden, but may be what ancient NUT servers say + # May throw if the error is due to (non-)connection? + $ans = $self->_send("NETVER"); + } + + if (!defined $ans) { + return undef; + } + chomp $ans; + + if (!defined $version_re) { + # Valid versions as of NUT 2.8.2: 1.0, 1.1, 1.2, 1.3 + # Is it an X(.Y) number? + $version_re = qr/^\d+(\.\d+)?$/; + } + + return ($ans =~ $version_re); +} + +sub StartTLS { + my $self = shift; + my %arg = @_; + my $ans; + + if (defined $self->{debugssl} && $self->{debugssl} > 0) { + # Pass to SSL lib as numeric level "debug": + $self->_debug("debugssl = $self->{debugssl}"); + eval "use IO::Socket::SSL qw(debug$self->{debugssl}); 1;"; + } else { + eval "require IO::Socket::SSL; 1;"; + } + if ($@) { + $self->_debug($self->{err} = "IO::Socket::SSL not available: FEATURE-NOT-SUPPORTED on client side"); + return undef; + } + + $ans = $self->_send("STARTTLS"); + if (defined $ans && $ans =~ /^OK STARTTLS/) { + $self->_debug("STARTTLS accepted, upgrading socket."); + my %argdef = (); + my $strarg = "[" . scalar(%arg) . "]"; + for (keys %arg) { + $strarg .= " $_=>" . ($arg{$_}//"undef"); + if (defined $arg{$_}) { + $argdef{$_} = $arg{$_}; + } + } + $self->_debug("STARTTLS args: SSL_verify_mode=>" + . ($arg{CERTVERIFY} ? "SSL_VERIFY_PEER" : "SSL_VERIFY_NONE") + . " Other args: " . $strarg + ); + + if (!defined($argdef{SSL_hostname}) && defined($argdef{HOST})) { + # If specified, allows cert validation for host names *and* IP addresses + $argdef{SSL_hostname} = $argdef{HOST}; + } + + if ($self->{debug}) { + $dumper->dumpValue(\%argdef); + } + + if ($arg{CERTVERIFY} && ($argdef{SSL_ca_file} || $argdef{SSL_ca_path}) && ($^O eq "darwin" )) { + # https://github.com/networkupstools/nut/issues/3404 + print STDERR "WARNING: Custom CA certificate verification may fail on $^O platform, in that case unset CERTVERIFY in your client configuration"; + } + + # NOTE: Currently nothing fancy like client's own certificate databases... + IO::Socket::SSL->start_SSL( + $self->{srvsock}, + SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), + %argdef + ) or do { + $self->_debug($self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr()); + return undef; + }; + return 1; + } + + $self->_debug($self->{err} = "STARTTLS failed: $ans"); + return undef; +} + +sub Login { # login to upsd, so that it won't shutdown unless we say we're + # ok. This should only be used if you're actually connected + # to the ups that upsd is monitoring. + # This assumes the upsmon SECONDARY role so we can be alerted + # and initiate a shutdown if someone else sends the FSD command + # to this UPS; we can further becomePrimary() if we are the + # system which manages it. + +# Author: Kit Peters +# ### changelog: modified login logic a bit. Now it doesn't check to see +# ### changelog: if we got OK, ERR, or something else from upsd. It +# ### changelog: simply checks for a response beginning with OK from upsd. +# ### changelog: Anything else is an error. +# +# ### changelog: uses the new _send command +# + my $self = shift; # myself + my $user = shift; # username + my $pass = shift; # password + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $errmsg; # error message, sent to _debug and $self->{err} + my $ans; # scalar to hold responses from upsd + + if (!($self->Authenticate($user, $pass))) { + if ($self->{err} !~ /ERR ALREADY-SET-USERNAME/) { + $self->_debug("Authenticate before LOGIN failed."); + return undef; + } + } + + $ans = $self->_send( "LOGIN $ups" ); + if (defined $ans && $ans =~ /^OK/) { # Login successful. + $self->_debug("LOGIN successful."); + return 1; + } + if (defined $ans) { + $errmsg = "LOGIN failed. Last message from upsd: $ans"; + } + else { + $errmsg = "Network error: $!"; + } + $self->_debug($self->{err} = $errmsg); + return undef; +} + +sub Authenticate { # Announce to the UPS who we are to set up the proper + # management level. See upsd.conf man page for details. + +# Contributor: Wayne Wylupski + my $self = shift; # myself + my $user = shift; # username + my $pass = shift; # password + + if ($self->{authenticated}) { + $self->_debug("Already authenticated, skip"); + return 1; + } + + my $errmsg; # error message, sent to _debug and $self->{err} + my $ans; # scalar to hold responses from upsd + + # only attempt authentication if username and password given + if (defined $user and defined $pass) { + $ans = $self->_send("USERNAME $user"); + if (defined $ans && $ans =~ /^OK/) { # username OK, send password + + $ans = $self->_send("PASSWORD $pass"); + if (defined $ans && $ans =~ /^OK/) { + $self->{authenticated} = 1; + return 1 + } + } + } else { + $self->_debug($self->{err} = "Authentication failed: username and/or password not provided, internal equivalent of USERNAME-REQUIRED"); + return undef; + } + if (defined $ans) { + $errmsg = "Authentication failed. Last message from upsd: $ans"; + } + else { + $errmsg = "Network error: $!"; + } + $self->_debug($self->{err} = $errmsg); + return undef; +} + +sub Logout { # logout of upsd and close connection +# Author: Kit Peters +# ### changelog: uses the new _send command +# + my $self = shift; + if ($self->{srvsock}) { # are we still connected to upsd? + my $ans = $self->_send( "LOGOUT" ); + close ($self->{srvsock}); + delete ($self->{srvsock}); + $self->{authenticated} = 0; + } +} + +# internal functions. These are only used by UPS::Nut internally, so +# please don't use them otherwise. If you really think an internal +# function should be externalized, let me know. + +sub _initialize { +# Author: Kit Peters + my $self = shift; + my %arg = @_; + my $host = $arg{HOST} || 'localhost'; # Host running upsd and probably drivers + my $port = $arg{PORT} || '3493'; # 3493 is IANA assigned port for NUT + my $proto = $arg{PROTO} || 'tcp'; # use tcp unless user tells us to + my $user = $arg{USERNAME} || undef; # username passed to upsd + my $pass = $arg{PASSWORD} || undef; # password passed to upsd + my $login = $arg{LOGIN} || 0; # login to upsd on init? + + # Explicitly enable/disable TRACKING mode for SETVAR/INSTCMD on init? + # Remember in $self->{tracking} if we could toggle it with + # this upsd version, and to which value? ('ON'/'OFF'/undef) + my $tracking = $arg{TRACKING} || undef; + + $self->{name} = $arg{NAME} || 'default'; # UPS name in etc/ups.conf on $host + $self->{timeout} = $arg{TIMEOUT} || 30; # timeout + $self->{debug} = $arg{DEBUG} || 0; # debugging? + $self->{debugssl} = defined $arg{DEBUGSSL} ? $arg{DEBUGSSL} : $self->{debug}; # debugging IO::Socket::SSL upon use? + $self->{debugout} = $arg{DEBUGOUT} || undef; # where to send debug messages + + my $srvsock = $self->{srvsock} = # establish connection to upsd + IO::Socket::INET->new( + PeerAddr => $host, + PeerPort => $port, + Proto => $proto + ); + + unless ( defined $srvsock) { # can't connect + $self->_debug($self->{err} = "Unable to connect via $proto to $host:$port: $!"); + return undef; + } + + $self->{select} = IO::Select->new( $srvsock ); + + my $can_ssl = 1; + eval "require IO::Socket::SSL; 1"; + if ($@) { + $can_ssl = 0; + } + + my $use_ssl = $arg{USESSL}; + if (!defined $use_ssl) { + $self->_debug("USESSL option was undef, flipping to IO::Socket::SSL module availability: $can_ssl"); + $use_ssl = $can_ssl; + } + + if (($can_ssl && $use_ssl) || $arg{FORCESSL}) { + # Always try to elevate, do not bother if this fails unless required by args + my $startedTLS = $self->StartTLS(%arg); + if (defined $startedTLS && $startedTLS) { + # Make sure handshake succeeded or abort early + # (there is currently no way for the server to + # report its fault to the client when connection + # is half-way secure): + if (!$self->isValidProtocolVersion()) { + if ($arg{FORCESSL}) { + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required: $self->{err}"); + return undef; + } + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required: $self->{err}"); + # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? + } + } else { + if ($arg{FORCESSL}) { + $self->_debug($self->{err} = "SSL setup failed but it is required: $self->{err}"); + return undef; + } + } + } else { + $self->_debug("SSL setup neither requested nor required, skipped StartTLS altogether"); + } + + $self->{authenticated} = 0; + if ($user and $pass) { # attempt login to upsd if that option is specified + if ($login) { # attempt login to upsd if that option is specified + $self->Login($user, $pass) or carp $self->{err}; + } + else { + $self->Authenticate($user, $pass) or carp $self->{err}; + } + } + + # get a hash of vars for both the TIE functions as well as for + # expanding vars. + $self->{vars} = $self->ListVar; + + unless ( defined $self->{vars} ) { + # FIXME: Can well be `ERR UNKNOWN-UPS` due to NAME=default + # Better report that as such... + $self->_debug($self->{err} = "Network error: $!"); + return undef; + } + + # Can error out on invalid "TRACKING" value setting, returns undef then: + $self->{tracking} = $self->SetTrackingMode($tracking); + + return $self; +} + +# +# _send +# +# Sends a command to the server and retrieves the results. +# If there was a network error, return undef; $! will contain the +# error. +sub _send +{ +# Contributor: Wayne Wylupski + my $self = shift; + my $cmd = shift; + my @handles; + my $result; # undef by default + + my $socket = $self->{srvsock}; + my $select = $self->{select}; + + $self->{err} = ""; + + @handles = IO::Select->select( undef, $select, $select, $self->{timeout} ); + return undef if ( !scalar $handles[1] ); + + $socket->print( $cmd . $_eol ); + + @handles = IO::Select->select( $select, undef, $select, $self->{timeout} ); + return undef if ( !scalar $handles[0]); + + $result = $socket->getline; + return undef if ( !defined ( $result ) ); + chomp $result; + + return $result; +} + +sub _getline +{ +# Contributor: Wayne Wylupski + my $self = shift; + my $result; # undef by default + + my $socket = $self->{srvsock}; + my $select = $self->{select}; + + # Different versions of IO::Socket has different error detection routines. + return undef if ( $IO::Socket::{has_error} && $select->has_error(0) ); + return undef if ( $IO::Socket::{has_exception} && $select->has_exception(0) ); + + chomp ( $result = $socket->getline ); + return $result; +} + +# Compatibility layer +sub Request { goto &GetVar; } + +sub GetVar { # request a variable from the UPS +# Author: Kit Peters + my $self = shift; +# ### changelog: 8/3/2002 - KP - Request() now returns undef if not +# ### changelog: connected to upsd via $srvsock +# ### changelog: uses the new _send command +# +# Modified by Gabor Kiss according to protocol version 1.5+ + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "GET VAR $ups $var"; # build request + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans. Requested $var."); + return undef; + } + elsif ($ans =~ /^VAR/) { + my $checkvar; # to make sure the var we asked for is the var we got. + my $retval; # returned value for requested VAR + (undef, undef, $checkvar, $retval) = split(' ', $ans, 4); + # get checkvar and retval from the answer + if ($checkvar ne $var) { # did not get expected var + $self->_debug($self->{err} = "Requested $var, received $checkvar"); + return undef; + } + $retval =~ s/^"(.*)"$/$1/; + return $retval; # return the requested value + } + else { # unrecognized response + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); + return undef; + } +} + +sub Set { +# Contributor: Wayne Wylupski +# ### changelog: uses the new _send command +# + my $self = shift; + my $var = shift; + (my $value = shift) =~ s/^"?(.*)"?$/"$1"/; # add quotes if missing + + # Optional TRACKING wait support: + my $wait_interval_sec = shift || undef; + my $wait_max_count = shift || undef; + + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $do_wait = 0; + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { + $self->EnableTrackingModeOnce; + $do_wait = 1; + } + + my $req = "SET VAR $ups $var $value"; # build request + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^OK TRACKING /) { # command successful + my $id; + (undef, undef, $id) = split(' ', $ans, 3); + if (defined $id && $id =~ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + ) { + # UUID + $self->_debug("Variable setting $var $value sent successfully, got tracking ID '$id'."); + my $tid = UPS::Nut::TrackingID->new($id); + if ($do_wait && $self->WaitTrackingResult($tid, $wait_interval_sec, $wait_max_count)) { + return $value; + } + return ($value, $tid); + } + $self->_debug("Variable setting $var $value sent successfully, but got bogus tracking ID: $ans"); + return $value; + } + elsif ($ans =~ /^OK/) { + $self->_debug("Variable setting $var $value sent successfully."); + return $value; + } + else { # unrecognized response + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); + return undef; + } +} + +sub FSD { # set forced shutdown flag +# Author: Kit Peters +# ### changelog: uses the new _send command +# + my $self = shift; + my $ups = shift || $self->{name}; + + my $req = "FSD $ups"; # build request + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { # can't set forced shutdown flag + $self->_debug($self->{err} = "Can't set FSD flag. Upsd reports: $ans"); + return undef; + } + elsif ($ans =~ /^OK FSD-SET/) { # forced shutdown flag set + $self->_debug("FSD flag set successfully."); + return 1; + } + else { + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); + return undef; + } +} + +sub InstCmd { # send instant command to ups +# Contributor: Wayne Wylupski + my $self = shift; + + chomp (my $cmd = shift); + + # Optional TRACKING wait support: + my $wait_interval_sec = shift || undef; + my $wait_max_count = shift || undef; + + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $do_wait = 0; + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { + $self->EnableTrackingModeOnce; + $do_wait = 1; + } + + my $req = "INSTCMD $ups $cmd"; + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { # error reported from upsd + $self->_debug($self->{err} = "Can't send instant command $cmd. Reason: $ans"); + return undef; + } + elsif ($ans =~ /^OK TRACKING /) { # command successful + my $id; + (undef, undef, $id) = split(' ', $ans, 3); + if (defined $id && $id =~ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + ) { + # UUID + $self->_debug("Instant command $cmd sent successfully, got tracking ID '$id'."); + my $tid = UPS::Nut::TrackingID->new($id); + if ($do_wait && $self->WaitTrackingResult($tid, $wait_interval_sec, $wait_max_count)) { + return 1; + } + return (1, $tid); + } + $self->_debug("Instant command $cmd sent successfully, but got bogus tracking ID: $ans"); + return 1; + } + elsif ($ans =~ /^OK/) { # command successful + $self->_debug("Instant command $cmd sent successfully."); + return 1; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send instant command $cmd. Unrecognized response from upsd: $ans"); + return undef; + } +} + +sub ListUPS { + my $self = shift; + return $self->_get_list("LIST UPS", 2, 1); +} + +sub GetTrackingResult { + my $self = shift; + my $tid = shift; + + my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; + my $ans = $self->_send("GET TRACKING $id"); + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + chomp $ans; + if ($ans eq 'SUCCESS') { + $self->_debug("Request with TRACKING ID $id has successfully completed"); + } elsif ($ans =~ 'ERR') { + $self->_debug($self->{err} = "Request with TRACKING ID $id has completed with a failure: $ans"); + } elsif ($ans eq 'PENDING') { + $self->_debug("Still waiting for TRACKING ID $id"); + } else { + $self->_debug($self->{err} = "Got bogus reply while waiting for TRACKING ID $id: $ans"); + return undef; + } + + return $ans; +} + +sub WaitTrackingResult { + my $self = shift; + my $tid = shift; + my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; + + my $wait_interval_sec = shift || 1; + my $wait_max_count = shift || 10; + + if (!(defined $id && defined $wait_interval_sec && defined $wait_max_count)) { + return undef; + } + + do { + my $value = $self->GetTrackingResult($tid); + if (defined $value) { + # Note: debug messages are printed by GetTrackingResult() already + chomp $value; + if ($value eq 'SUCCESS') { + return 1; + } elsif ($value =~ 'ERR') { + return -1; + } + } else { + # TOTHINK: Keep retrying? Here in case of network or explicit error... + $self->_debug("Got bogus reply while waiting for TRACKING ID $id: undef"); + } + + sleep($wait_interval_sec); + $wait_max_count = $wait_max_count - 1; + } while ($wait_max_count > 0); + + # Timed out?.. + $self->_debug("Timed out while waiting for TRACKING ID $id"); + return 0; +} + +sub ListClient { + my $self = shift; + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST CLIENT $ups", 2, 2); +} + +sub GetUPSDesc { + my $self = shift; + my $ups = shift || $self->{name}; + + my $ans = $self->_send("GET UPSDESC $ups"); + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + if ($ans =~ /^UPSDESC/) { + my @fields = split(' ', $ans, 3); + my $desc = $fields[2]; + $desc =~ s/^"(.*)"$/$1/; + return $desc; + } + + $self->_debug($self->{err} = "Error: $ans"); + return undef; +} + +sub ListVar { + my $self = shift; + my $ups = shift || $self->{name}; + + my $vars = $self->_get_list("LIST VAR $ups", 3, 2); + return $vars unless @_; # return all variables + return {map { $_ => $vars->{$_} } @_}; # return selected ones +} + +sub ListRW { + my $self = shift; + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST RW $ups", 3, 2); +} + +sub ListCmd { + my $self = shift; + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST CMD $ups", 2); +} + +sub ListEnum { + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after $var, to avoid breaking + # API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST ENUM $ups $var", 3); +} + +sub ListRange { + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "LIST RANGE $ups $var"; + my $ans = $self->_send($req); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^BEGIN LIST RANGE/) { + my $retval = []; + my $line; + while ($line = $self->_getline) { + last if $line =~ /^END LIST RANGE/; + # RANGE "" "" + if ($line =~ /^RANGE \S+ \S+ "([^"]+)" "([^"]+)"/) { + push(@$retval, { min => $1, max => $2 }); + } + } + return $retval; + } + + $self->_debug($self->{err} = "Unrecognized response: $ans"); + return undef; +} + +sub _get_list { + my $self = shift; + my ($req, $valueidx, $keyidx) = @_; + my $ans = $self->_send($req); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^BEGIN LIST/) { # command successful + my $retval = $keyidx ? {} : []; + my $line; + while ($line = $self->_getline) { + last if $line =~ /^END LIST/; + my @fields = split(' ', $line, $valueidx+1); + (my $value = $fields[$valueidx]) =~ s/^"(.*)"$/$1/; + if ($keyidx) { + $retval->{$fields[$keyidx]} = $value; + } + else { + push(@$retval, $value); + } + } + unless ($line) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + $self->_debug("$req command sent successfully."); + return $retval; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } +} + +# Compatibility layer +sub VarDesc { goto &GetDesc; } + +sub GetDesc { +# Contributor: Wayne Wylupski +# Modified by Gabor Kiss according to protocol version 1.5+ + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "GET DESC $ups $var"; + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^DESC/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + $ans =~ s/^"(.*)"$/$1/; + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } +} + +# Compatibility layer +sub VarType { goto &GetType; } + +sub GetType { +# Contributor: Wayne Wylupski +# Modified by Gabor Kiss according to protocol version 1.5+ + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "GET TYPE $ups $var"; + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^TYPE/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } +} + +# Compatibility layer +sub InstCmdDesc { goto &GetCmdDesc; } + +sub GetCmdDesc { +# Contributor: Wayne Wylupski +# Modified by Gabor Kiss according to protocol version 1.5+ + my $self = shift; + my $cmd = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "GET CMDDESC $ups $cmd"; + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^DESC/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + $ans =~ s/^"(.*)"$/$1/; + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } +} + +sub DESTROY { # destructor, all it does is call Logout +# Author: Kit Peters + my $self = shift; + $self->_debug("Object destroyed."); + $self->Logout(); +} + +sub _debug { # print debug messages to stdout or file +# Author: Kit Peters + my $self = shift; + if ($self->{debug}) { + chomp (my $msg = shift); + my $out; # filehandle for output + if ($self->{debugout}) { # if filename is given, use that + $out = new FileHandle ($self->{debugout}, ">>") or warn "Error: $!"; + } + if ($out) { # if out was set to a filehandle, create nifty timestamp + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + $year = sprintf("%02d", $year % 100); # Y2.1K compliant, even! + my $timestamp = join '/', ($mon + 1), $mday, $year; # today + $timestamp .= " "; + $timestamp .= join ':', $hour, $min, $sec; + print $out "$timestamp $msg\n"; + } + else { print "DEBUG: $msg\n"; } # otherwise, print to stdout + } +} + +sub Error { # what was the last thing that went bang? +# Author: Kit Peters + my $self = shift; + if ($self->{err}) { return $self->{err}; } + else { return "No error explanation available."; } +} + +sub becomePrimary { goto &Master; } + +sub Master { # check for MASTER level access +# Author: Kit Peters +# ### changelog: uses the new _send command +# ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on +# ### failure. this makes it consistent with other methods +# +# NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY +# (and backwards-compatible alias handling) + my $self = shift; + my $ups = shift || $self->{name}; + + my $req = "PRIMARY $ups"; # build request + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^OK/) { # access granted + $self->_debug("PRIMARY level access granted. Upsd reports: $ans"); + return 1; + } + + # Retry with MASTER if PRIMARY failed + $req = "MASTER $ups"; + $ans = $self->_send( $req ); + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^OK/) { # access granted + $self->_debug("MASTER level access granted. Upsd reports: $ans"); + return 1; + } + else { # access denied, or unrecognized response + $self->_debug($self->{err} = "PRIMARY/MASTER level access denied. Upsd responded: $ans"); + return undef; + } +} + +sub AUTOLOAD { +# Contributor: Wayne Wylupski + my $self = shift; + my $name = $UPS::Nut::AUTOLOAD; + $name =~ s/^.*:://; + + # for a change we will only load cmds if needed. + if (!defined $self->{cmds} ) { + %{$self->{cmds}} = map{ $_ =>1 } @{$self->ListCmd}; + } + + croak "No such InstCmd: $name" if (! $self->{cmds}{$name} ); + + return $self->InstCmd( $name ); +} + +#------------------------------------------------------------------------- +# tie hash interface +# +# The variables of the array, including the hidden 'numlogins' can +# be accessed as a hash array through this method. +# +# Example: +# tie %ups, 'UPS::Nut', +# NAME => "my-ups", +# HOST => "some-machine.somewhere.com", +# ... # same options as new(); +# ; +# +# $ups{UPSIDENT} = "MyUPS"; +# print $ups{MFR}, " " $ups{MODEL}, "\n"; +# +#------------------------------------------------------------------------- +sub TIEHASH { + my $class = shift || 'UPS::Nut'; + return $class->new( @_ ); +} + +sub FETCH { + my $self = shift; + my $key = shift; + + return $self->Request( $key ); +} + +sub STORE { + my $self = shift; + my $key = shift; + my $value = shift; + + return $self->Set( $key, $value ); +} + +sub DELETE { + croak "DELETE operation not supported"; +} + +sub CLEAR { + croak "CLEAR operation not supported"; +} + +sub EXISTS { + exists shift->{vars}{shift}; +} + +sub FIRSTKEY { + my $self = shift; + my $a = keys %{$self->{vars}}; + return scalar each %{$self->{vars}}; +} + +sub NEXTKEY { + my $self = shift; + return scalar each %{$self->{vars}}; +} + +sub UNTIE { + $_[0]->Logout; +} + +=head1 NAME + +Nut - a module to talk to a UPS via NUT (Network UPS Tools) upsd + +=head1 SYNOPSIS + + use UPS::Nut; + + $ups = new UPS::Nut( NAME => "my-ups", + HOST => "some-machine.somewhere.com", + PORT => "3493", + USERNAME => "upsuser", + PASSWORD => "upspasswd", + TIMEOUT => 30, + DEBUG => 1, + DEBUGOUT => "/some/file/somewhere", + ); + if ($ups->Status() =~ /OB/) { + print "Oh, no! Power failure!\n"; + } + + tie %other_ups, 'UPS::Nut', + NAME => "my-ups", + HOST => "some-machine.somewhere.com", + ... # same options as new(); + ; + + print $other_ups{MFR}, " ", $other_ups{MODEL}, "\n"; + +=head1 DESCRIPTION + +This is an object-oriented (whoo!) interface between Perl and upsd from +the Network UPS Tools package version 1.5 and above +(https://www.networkupstools.org/). +Note that it only talks to upsd for you in a Perl-ish way. +It does not monitor the UPS continuously. + +=head1 CONSTRUCTOR + +Shown with defaults: new UPS::Nut( NAME => "default", + HOST => "localhost", + PORT => "3493", + USERNAME => "", + PASSWORD => "", + DEBUG => 0, + DEBUGOUT => "", + ); +* NAME is the name of the UPS to monitor, as specified in ups.conf +* HOST is the host running upsd +* PORT is the port that upsd is running on +* USERNAME and PASSWORD are those that you use to login to upsd. This + gives you the right to do certain things, as specified in upsd.conf. +* DEBUG turns on debugging output, set to 1 or 0 +* DEBUGOUT is de thing you do when de s*** hits the fan. Actually, it's + the filename where you want debugging output to go. If it's not + specified, debugging output comes to standard output. + +=head1 Important notice + +This version of UPS::Nut is not compatible with version 0.04. It is totally +rewritten in order to talk the new protocol of NUT 1.5+. You should not use +this module as a drop-in replacement of previous version from 2002. +Almost all method has changed slightly. + +=head1 Methods + +Unlike in version 0.04 no methods return list values but a +single reference or undef. + +=head2 Methods for querying UPS status + +=over 4 + +=item Getvar($varname) + +returns value of the specified variable. Returns undef if variable +unsupported. +Old method named Request() is also supported for compatibility. + +=item Set($varname, $value) + +sets the value of the specified variable. Returns undef if variable +unsupported, or if variable cannot be set for some other reason. See +Authenticate() if you wish to use this function. + +=item BattPercent() + +returns percentage of battery left. Returns undef if we can't get +battery percentage for some reason. Same as GetVar('battery.charge'). + +=item LoadPercent($context) + +returns percentage of the load on the UPS. Returns undef if load +percentage is unavailable. $context is a selector of 3 phase systems. +Possible values are 1, 2, 3, 'L1', 'L2', 'L3'. It should be omitted +in case of single phase UPS. + +=item LineVoltage($context) + +returns input line (e.g. the outlet) voltage. Returns undef if line +voltage is unavailable. $context is a selector of 3 phase systems. +Possible values are 1, 2, 3, 'L1', 'L2', 'L3'. It should be omitted +in case of single phase UPS. + +=item Status() + +returns UPS status, one of OL or OB. OL or OB may be followed by LB, +which signifies low battery state. OL or OB may also be followed by +FSD, which denotes that the forced shutdown state +( see UPS::Nut->FSD() ) has been set on upsd. Returns undef if status +unavailable. Same as GetVar('ups.status'). + +=item Temperature() + +returns UPS internal temperature. Returns undef if internal +temperature unavailable. Same as GetVar('battery.temperature'). + +=back + +=head2 Other methods + +These all operate on the UPS specified in the NAME argument to the +constructor. + +=over 4 + +=item Authenticate($username, $password) + +With NUT certain operations are only available if the user has the +privilege. The program has to authenticate with one of the accounts +defined in upsd.conf. + +=item Login($username, $password) + +Notify upsd that client is drawing power from the given UPS. +It is automatically done if new() is called with USERNAME, PASSWORD +and LOGIN parameters. + +=item Logout() + +Notify upsd that client is released UPS. (E.g. it is shutting down.) +It is automatically done if connection closed. + +=item Master() + +Use this to find out whether or not we have MASTER privileges for +this UPS. Returns 1 if we have MASTER privileges, returns 0 otherwise. + +TODO: API change pending to replace MASTER with PRIMARY +(and backwards-compatible alias handling) + +=item ListVar($variable, ...) + +This is an implementation of "LIST VAR" command. +Returns a hash reference to selected variable names and values supported +by the UPS. If no variables given it returns all. +Returns undef if "LIST VAR" failed. +(Note: This method significantly differs from the old ListVars() +and ListRequest().) + +=item ListRW() + +Similar to ListVar() but cares only with read/writeable variables. + +=item ListEnum($variable) + +Returns a reference to the list of all possible values of $variable. +List is empty if $variable is not an ENUM type. (See GetType().) +Returns undef if error occurred. + +=item ListCmd() + +Returns a reference to the list of all instant commands supported +by the UPS. Returns undef if these are unavailable. +This method replaces the old ListInstCmds(). + +=item InstCmd($command) + +Send an instant command to the UPS. Returns 1 on success. Returns +undef if the command can't be completed. + +=item FSD() + +Set the FSD (forced shutdown) flag for the UPS. This means that we're +planning on shutting down the UPS very soon, so the attached load should +be shut down as well. Returns 1 on success, returns undef on failure. +This cannot be unset, so don't set it unless you mean it. + +=item Error() + +why did the previous operation fail? The answer is here. It will +return a concise, well-written, and brilliantly insightful few words as +to why whatever you just did went bang. + +=item GetDesc($variable) + +Returns textual description of $variable or undef in case of error. +Old method named VarDesc() is also supported for compatibility. + +=item GetCmdDesc($command) + +This is like GetDesc() above but applies to the instant commands. +Old method named InstCmdDesc() is also supported for compatibility. + +=item GetType($variable) + +Returns a string UNKNOWN or constructed one or more words of RW, +ENUM and STRING:n (where n is a number). (Seems to be not working +perfectly at upsd 2.2.) +Old method named VarType() is also supported for compatibility. + +=item ListUPS() + +Returns a reference to hash of all available UPS names and descriptions. + +=back + +=head1 AUTOLOAD + +The "instant commands" are available as methods of the UPS object. They +are AUTOLOADed when called. For example, if the instant command is FPTEST, +then it can be called by $ups->FPTEST. + +=head1 TIE Interface + +If you wish to simply query or set values, you can tie a hash value to +UPS::Nut and pass as extra options what you need to connect to the host. +If you need to exercise an occasional command, you may find the return +value of 'tie' useful, as in: + + my %ups; + my $ups_obj = tie %ups, 'UPS::Nut', HOSTNAME=>"firewall"; + + print $ups{UPSIDENT}, "\n"; + + $ups_obj->Authenticate( "user", "pass" ); + + $ups{UPSIDENT} = "MyUPS"; + +=head1 AUTHOR + + Original version made by Kit Peters + perl@clownswilleatyou.com + http://www.awod.com/staff/kpeters/perl/ + + Rewritten by Gabor Kiss . + +=head1 CREDITS + +Developed with the kind support of A World Of Difference, Inc. + + +Many thanks to Ryan Jessen at CyberPower +Systems for much-needed assistance. + +Thanks to Wayne Wylupski for the code to make +accessor methods for all supported vars. + +=head1 LICENSE + +This module is distributed under the same license as Perl itself. + +=cut + +package UPS::Nut::TrackingID; +use strict; + +sub new { + my $class = shift; + my $id = shift; + my $self = { + id => $id, + created => time() + }; + bless $self, $class; + return $self; +} + +sub id { + my $self = shift; + return $self->{id}; +} + +sub created { + my $self = shift; + return $self->{created}; +} + +sub age { + my $self = shift; + return time() - $self->{created}; +} + +sub toString { + my $self = shift; + return $self->{id}; +} + +sub isValid { + my $self = shift; + return defined $self->{id} && $self->{id} ne ""; +} + +1; +__END__ + diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl new file mode 100755 index 0000000000..d3120e698e --- /dev/null +++ b/scripts/perl/test_nutclient.pl @@ -0,0 +1,283 @@ +#!/usr/bin/perl +# -*- coding: utf-8 -*- + +# This source code is provided for testing/debugging purpose ;) +# This script is a Perl equivalent of scripts/python/module/test_nutclient.py.in +# Copyright (C) 2026- Jim Klimov + +use strict; +use warnings; +use UPS::Nut; +use Term::ANSIColor; + +# Main logic +if (1) { + my $NUT_HOST = $ENV{'NUT_HOST'} || '127.0.0.1'; + my $NUT_PORT = $ENV{'NUT_PORT'} || '3493'; + my $NUT_USER = $ENV{'NUT_USER'} || undef; + my $NUT_PASS = $ENV{'NUT_PASS'} || undef; + my $NUT_SSL = $ENV{'NUT_SSL'} || undef; + if (defined $NUT_SSL) { + $NUT_SSL = ($NUT_SSL eq "true" ? 1 : ($NUT_SSL eq "false" ? 0 : undef)); + } + my $NUT_FORCESSL = (($ENV{'NUT_FORCESSL'} || "false") eq "true" || ($ENV{'NUT_FORCESSL'} || "false") eq "1") ? 1 : 0; + my $NUT_CERTVERIFY = (($ENV{'NUT_CERTVERIFY'} || "false") eq "true" || ($ENV{'NUT_CERTVERIFY'} || "false") eq "1") ? 1 : 0; + my $NUT_CAFILE = $ENV{'NUT_CAFILE'} || undef; + my $NUT_CAPATH = $ENV{'NUT_CAPATH'} || undef; + # Note: Python's cert_file, key_file, key_pass are not directly + # supported by current Nut.pm STARTTLS as independent args, but + # passed via %arg. Nut.pm uses STARTTLS method which takes %arg. + + my $NUT_DEBUG = (($ENV{'DEBUG'} || "false") eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; + # Numeric if set, values defined by SSL.pm module: + my $NUT_DEBUG_SSL = $ENV{'NUT_DEBUG_SSL_PERL'} ; #(($ENV{'NUT_DEBUG_SSL_PERL'} || "") eq "" ? undef : $ENV{'NUT_DEBUG_SSL_PERL'}; + + # Account "unexpected" failures (more due to coding than circumstances) + # e.g. lack of protected access when no credentials were passed is okay + my @failed = (); + + print "UPS::Nut test...\n"; + + my $nut; + eval { + $nut = UPS::Nut->new( + # A name is used right where we initialize the object, + # otherwise it falls back to "default". It should be + # registered in ups.conf; one UPS at a time per connection! + # We fiddle those for NIT script (dummy, UPS1, UPS2) below. + NAME => "dummy", + HOST => $NUT_HOST, + PORT => $NUT_PORT, + USERNAME => $NUT_USER, + PASSWORD => $NUT_PASS, + DEBUG => $NUT_DEBUG, + DEBUGSSL => $NUT_DEBUG_SSL, + # TRACKING => 'ON', # undef by default, enabled in certain tests below + # STARTTLS related (passed via %arg to StartTLS in Nut.pm) + USESSL => $NUT_SSL, + CERTVERIFY => $NUT_CERTVERIFY, + FORCESSL => $NUT_FORCESSL, + # In case PyNUT's cert_file, key_file are needed: + SSL_ca_file => $NUT_CAFILE, + SSL_ca_path => $NUT_CAPATH, + SSL_cert_file => $ENV{'NUT_CERTFILE'}, + SSL_key_file => $ENV{'NUT_KEYFILE'}, + SSL_key_pass => $ENV{'NUT_KEYPASS'} + ); + }; + if ($@ || !defined($nut)) { + my $ex = $@ || (defined($nut) ? $nut->Error() : "N/A: \$nut object already discarded, can not retrieve its Error()"); + print "EXCEPTION during initialization: $ex\n"; + if ($NUT_SSL && ($ex =~ /FEATURE-NOT-CONFIGURED/ || $ex =~ /FEATURE-NOT-SUPPORTED/)) { + print "(anticipated error: server does not support STARTTLS)\n"; + exit(0); + } + die $ex; + } + + print "-" x 80 . "\nTesting 'ListUPS' :\n"; + my $result = $nut->ListUPS(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); + + # [dummy] + # driver = dummy-ups + # desc = "Test device" + # port = /src/nut/data/evolution500.seq + print "-" x 80 . "\nTesting 'ListVar' for 'dummy' (should be registered in ups.conf) :\n"; + # TOTHINK: Extend into a test for ListVar("bogus") - that it should fail? + $result = $nut->ListVar("dummy"); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', map { "$_ => $result->{$_}" } keys %$result) : (defined($result) ? $result : "NULL") ); + + print "-" x 80 . "\nTesting 'CheckUPSAvailable' (via ListUPS) :\n"; + my $ups_list = $nut->ListUPS(); + $result = exists($ups_list->{"dummy"}) ? "Available" : "Not Available"; + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", $result ); + + print "-" x 80 . "\nTesting 'ListCmd' :\n"; + $result = $nut->ListCmd(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'ARRAY') ? join(', ', @$result) : (defined($result) ? $result : "NULL") ); + + print "-" x 80 . "\nTesting 'ListRW' :\n"; + $result = $nut->ListRW(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); + + print "-" x 80 . "\nTesting 'InstCmd' (Test front panel) :\n"; + eval { + $nut->{name} = "UPS1"; + $result = $nut->InstCmd("test.panel.start"); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /CMD-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'InstCmd'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'Set' (set ups.id to test):\n"; + eval { + $nut->{name} = "UPS1"; + $result = $nut->Set("ups.id", "test"); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /VAR-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'SetVar'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'Set' with TRACKING for 'driver.debug' (1s interval, 10s timeout):\n"; + eval { + my $target_ups = "UPS1"; + $nut->{name} = $target_ups; + # Check if UPS exists + my $vars = $nut->ListVar(); + if (!defined($vars)) { + $target_ups = "dummy"; + $nut->{name} = $target_ups; + } + + # Set with tracking + my ($tid_res, $tid) = $nut->Set("driver.debug", "1", 1, 10); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; + } + + if (ref($tid) eq 'UPS::Nut::TrackingID') { + printf("Got TRACKING ID: %s, created: %s, age: %0.2fs\n", $tid->id, $tid->created, $tid->age); + $result = $tid->id; + } else { + $result = $tid_res; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /VAR-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'SetVar-Tracking'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + # testing who has an upsmon-like log-in session to a device + print "-" x 80 . "\nTesting 'ListClient' for 'dummy' (should be registered in ups.conf) before test client is connected :\n"; + eval { + $result = $nut->ListClient("dummy"); + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; + push @failed, 'ListClient-dummy-before'; + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); + + print "-" x 80 . "\nTesting 'ListClient' for missing device (should raise an exception) :\n"; + eval { + $result = $nut->ListClient("MissingBogusDummy"); + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if ($ex =~ /UNKNOWN-UPS/) { + $result .= "\n(anticipated error: bogus device name was tested)"; + } else { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'ListClient-MissingBogusDummy'; + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + my $loggedIntoDummy = 0; + print "-" x 80 . "\nTesting 'Login' (DeviceLogin) for 'dummy' (should be registered in ups.conf; current credentials should have an upsmon role in upsd.users) :\n"; + eval { + $nut->{name} = "dummy"; + $result = $nut->Login($NUT_USER, $NUT_PASS); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; + } + if (!defined($result)) { + die "Failed to LOGIN: $nut->{err}" + } + $loggedIntoDummy = 1; + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'Login-dummy'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'ListClient' for all (passing no args to ListClient uses \$nut->{name}) :\n"; + eval { + # Python's nut.ListClients() with no args lists all devices and their clients. + # In Nut.pm, ListClient(undef) uses $self->{name}. + # PyNUT's ListClients(None) iterates over all UPSes. + # Let's implement similar logic here. + my $ups_list_all = $nut->ListUPS(); + my %all_clients = (); + foreach my $ups_name (keys %$ups_list_all) { + my $clients = $nut->ListClient($ups_name); + if (defined($clients)) { + $all_clients{$ups_name} = $clients; + } + } + $result = \%all_clients; + + if (ref($result) ne 'HASH') { + die "ListClient() did not return a hash ref: $nut->{err}"; + } else { + if ($loggedIntoDummy) { + if (!exists($result->{'dummy'})) { + die "ListClient() result missing 'dummy' key: $nut->{err}"; + } + if (scalar keys %{$result->{'dummy'}} < 1) { + die "ListClient() returned an empty hash for 'dummy' where at least one client was expected: $nut->{err}"; + } + } + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; + push @failed, 'ListClient-all-after'; + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', map { "$_ => [" . join(', ', keys %{$result->{$_}}) . "]" } keys %$result) : (defined($result) ? $result : "NULL") ); + + print "-" x 80 . "\nTesting 'UPS::Nut' instance teardown (end of test script)\n"; + + if (scalar @failed > 0) { + print "SOME TEST CASES FAILED in an unexpected manner: " . join(', ', @failed) . "\n"; + exit(1); + } +} diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index b86c2d3799..8553943144 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -70,6 +70,8 @@ # NUT STARTTLS protocol action. import socket +import time +import re ssl_available = False try: @@ -78,6 +80,27 @@ try: except: pass +class TrackingID: + """ Cookie given when performing async action, used to redeem result at a later date. """ + def __init__(self, tracking_id): + self.__id = tracking_id + self.__created = time.time() + + def id(self): + return self.__id + + def created(self): + return self.__created + + def age(self): + return time.time() - self.__created + + def __str__(self): + return self.__id + + def isValid(self): + return self.__id is not None and self.__id != "" + class PyNUTError( Exception ) : """ Base class for custom exceptions """ @@ -97,15 +120,15 @@ class PyNUTClient : __use_ssl = False __ssl_context = None - __version = "1.9.0" - __release = "2026-03-16" + __version = "1.10.0" + __release = "2026-04-08" try: NUT_DEFAULT_CONNECT_TIMEOUT = float(os.getenv("NUT_DEFAULT_CONNECT_TIMEOUT", "5.0")) except: NUT_DEFAULT_CONNECT_TIMEOUT = 5.0 - def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, + def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, tracking=False, use_ssl=False, ssl_context=None, cert_verify=None, ca_file=None, ca_path=None, cert_file=None, key_file=None, key_pass=None, force_ssl=False ) : """ Class initialization method @@ -116,6 +139,7 @@ login : Login used to connect to NUT server (default to None for no authen password : Password used when using authentication (default to None) debug : Boolean, put class in debug mode (prints everything on console, default to False) timeout : Timeout used to wait for network response +tracking : Boolean or 'ON'/'OFF', try to enable TRACKING support right away use_ssl : Boolean, use SSL/TLS for connection (default to False; subject to 'ssl' module availability) ssl_context : ssl.SSLContext object to use for SSL/TLS connection (default to None; subject to 'ssl' module availability) cert_verify : Boolean, verify server certificate (default to None, which means True if CA info is provided) @@ -173,6 +197,8 @@ force_ssl : Boolean, if True, the connection must be secure (default to False) self.__connect() + self.__tracking = self.SetTrackingMode(tracking) + # Try to disconnect cleanly when class is deleted ;) def __del__( self ) : """ Class destructor method """ @@ -222,10 +248,37 @@ if something goes wrong. print( "[DEBUG] STARTTLS accepted, wrapping socket" ) if self.__ssl_context is None : self.__ssl_context = ssl.create_default_context() - self.__srv_handler = self.__ssl_context.wrap_socket( - self.__srv_handler, - server_hostname=self.__host - ) + + # Not sure if we can enter this river twice (e.g. if the server + # side of the handshake would still be listening), but we can try: + for i in range(5, 0, -1): + try: + self.__srv_handler = self.__ssl_context.wrap_socket( + self.__srv_handler, + server_hostname=self.__host + ) + break + except TimeoutError as te: + if self.__debug: + print( "[DEBUG] STARTTLS handshake timed out%s: %s" %( + ("" if (i < 1) else (", retrying (%d)" % i)), + str(te)) ) + + if (i > 1): + sleep(1) + else: + raise te + + # Make sure handshake succeeded or abort early + # (there is currently no way for the server to + # report its fault to the client when connection + # is half-way secure): + if not self.isValidProtocolVersion(): + if self.__debug : + print( "[DEBUG] STARTTLS setup claimed to succeed, but protocol version check in the secured session failed" ) + if self.__force_ssl: + raise PyNUTError("STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required") + # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? else : if self.__debug : print( "[DEBUG] STARTTLS failed: %s" % result ) @@ -256,6 +309,220 @@ if something goes wrong. else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def SetTrackingMode( self, value="" ) : + """ Enable/disable TRACKING ability for SETVAR/INSTCMD ('ON'/'OFF') + + Remember in $self.__tracking if we could set it with this upsd + version, and then to which value? + """ + if value is None or str(value).strip() == "": + return None + + if str(value) == 'False': + value = 'OFF' + + if str(value) == 'True': + value = 'ON' + + if value not in ['ON', 'OFF']: + if self.__debug : + print( "[DEBUG] Invalid setting for TRACKING mode was requested" ) + # TOTHINK # raise PyNUTError( "Invalid setting for TRACKING mode was requested" ) + return None + + self.__srv_handler.send( ("SET TRACKING %s\n" % value).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result == b"OK\n" : + self.__tracking = value + return value + + if self.__debug : + print( "[DEBUG] Failed to set TRACKING mode: %s" % result.replace( b"\n", b"" ).decode('ascii') ) + + self.__tracking = None + return None + + def EnableTrackingModeOnce( self ) : + if self.__tracking is not None and self.__tracking == 'ON': + return True + + if self.SetTrackingMode('ON') == 'ON': + return True + + return False + + def GetTrackingResult( self, tracking_id="" ) : + """ Returns the result of an asynchronous command execution + """ + if isinstance(tracking_id, TrackingID): + tracking_id = tracking_id.id() + + if self.__debug : + print( "[DEBUG] GetTrackingResult for %s" % tracking_id ) + + self.__srv_handler.send( ("GET TRACKING %s\n" % tracking_id).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result in [b"PENDING\n", b"SUCCESS\n"]: + return result.replace( b"\n", b"" ).decode('ascii') + else : + # ERR... + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def WaitTrackingResult( self, tracking_id="", wait_interval_sec = 1, wait_max_count = 10) : + tid = tracking_id + if isinstance(tracking_id, TrackingID): + tracking_id = tracking_id.id() + + if tracking_id is None or str(tracking_id).strip() == "" or \ + wait_interval_sec is None or wait_interval_sec < 1 or \ + wait_max_count is None or wait_max_count < 1 : + # Non-fatal skip; TOTHINK: raise a special exception for bad args? + return None + + while wait_max_count > 0: + value = self.GetTrackingResult(tid) + + if value == 'SUCCESS': + if self.__debug : + print( "[DEBUG] Request with TRACKING ID %s has successfully completed" % tracking_id ) + return True + + if value == 'PENDING': + if self.__debug : + print( "[DEBUG] Still waiting for TRACKING ID %s..." % tracking_id ) + else: + if self.__debug: + print( "[DEBUG] Got bogus reply while waiting for TRACKING ID %s: %s" % (tracking_id, str(value)) ) + + time.sleep(wait_interval_sec) + wait_max_count = wait_max_count - 1 + + if self.__debug : + print( "[DEBUG] Timed out waiting for TRACKING ID %s" % tracking_id ) + + return False + + def isValidProtocolVersion(self, version_re = None): + """ Sends a PROTVER/NETVER query using the active connection and + returns True if the returned version string matches a valid + NUT protocol version regex (defaults to an "X(.Y)" number aka + "^\\d+(?:\\.\\d+)?$" if version_re is None). + """ + result = None + try: + self.__srv_handler.send( ("PROTVER\n").encode('ascii') ) + result = self.__read_until( b"\n" ) + except PyNUTError as ignored: + # Deprecated and hidden, but may be what ancient NUT servers say + # May throw if the error is due to (non-)connection + self.__srv_handler.send( ("NETVER\n").encode('ascii') ) + result = self.__read_until( b"\n" ) + + # print ( "[DEBUG] isValidProtocolVersion: version_re='%s' result='%s'" % (str(version_re), str(result))) + + if result is None: + return False + + result = result.replace( b"\n", b"" ).decode('ascii') + + if version_re is None: + # Currently supported: 1.0, 1.1, 1.2, 1.3 => r'^1\.[0-3]$' + # Is it an X(.Y) number? + version_re = r'^\d+(\.\d+)?$' + + if re.match(version_re, result): + return True + + return False + + def GetVariableType( self, ups="", var="" ) : + """ Returns the type of the specified variable + """ + if self.__debug : + print( "[DEBUG] GetVariableType for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("GET TYPE %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:4] == b"TYPE" : + # TYPE + off = len( ("TYPE %s %s " % ( ups, var )).encode('ascii') ) + return result[off:-1].decode('ascii') + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def GetVariableDescription( self, ups="", var="" ) : + """ Returns the description of the specified variable + """ + if self.__debug : + print( "[DEBUG] GetVariableDescription for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("GET DESC %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:4] == b"DESC" : + # DESC "" + off = len( ("DESC %s %s " % ( ups, var )).encode('ascii') ) + return result[off:-1].split(b'"')[1].decode('ascii') + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def GetEnumList( self, ups="", var="" ) : + """ Returns the list of possible values for an ENUM variable + """ + if self.__debug : + print( "[DEBUG] GetEnumList for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result != ("BEGIN LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + enum_list = [] + result = self.__read_until( ("END LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + offset = len( ("ENUM %s %s " % ( ups, var )).encode('ascii') ) + end_offset = 0 - ( len( ("END LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + 1 ) + + for current in result[:end_offset].split( b"\n" ) : + enum_list.append( current[offset:].split( b'"' )[1].decode('ascii') ) + + return enum_list + + def GetRangeList( self, ups="", var="" ) : + """ Returns the list of possible ranges for a RANGE variable + """ + if self.__debug : + print( "[DEBUG] GetRangeList for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result != ("BEGIN LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + range_list = [] + result = self.__read_until( ("END LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + offset = len( ("RANGE %s %s " % ( ups, var )).encode('ascii') ) + end_offset = 0 - ( len( ("END LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + 1 ) + + for current in result[:end_offset].split( b"\n" ) : + # RANGE "" "" + ranges = current[offset:].split( b'"' ) + range_list.append( { 'min' : ranges[1].decode('ascii'), 'max' : ranges[3].decode('ascii') } ) + + return range_list + + def GetDeviceNumLogins( self, ups="" ) : + """ Returns the number of connected clients for the specified UPS + """ + if self.__debug : + print( "[DEBUG] GetDeviceNumLogins for %s" % ups ) + + self.__srv_handler.send( ("GET NUMLOGINS %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:9] == b"NUMLOGINS" : + # NUMLOGINS + return int( result.replace( b"\n", b"" ).split(b' ')[2] ) + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def GetUPSList( self ) : """ Returns the list of available UPS from the NUT server @@ -409,21 +676,37 @@ The result is presented as a dictionary containing 'key->val' pairs return( rw_vars ) - def SetRWVar( self, ups="", var="", value="" ): + def SetRWVar( self, ups="", var="", value="", wait_interval_sec = None, wait_max_count = None ): """ Set a variable to the specified value on selected UPS The variable must be a writable value (cf GetRWVars) and you must have the proper rights to set it (maybe login/password). """ + do_wait = False + if not (wait_interval_sec is None or wait_interval_sec < 1 or + wait_max_count is None or wait_max_count < 1) : + do_wait = True + self.EnableTrackingModeOnce() + self.__srv_handler.send( ("SET VAR %s %s %s\n" % ( ups, var, value )).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) - else : - raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + else: + if ( result[:12] == b"OK TRACKING " ) : + tracking_id = result.replace( b"\n", b"" ).split(b" ")[2].decode('ascii') + if self.__debug : + print( "[DEBUG] Variable setting %s %s sent successfully, got tracking ID '%s'" % (var, value, tracking_id) ) + + tid = TrackingID(tracking_id) + if do_wait and self.WaitTrackingResult(tid, wait_interval_sec, wait_max_count): + return( "OK" ) + return( tid ) + else: + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - def RunUPSCommand( self, ups="", command="" ) : + def RunUPSCommand( self, ups="", command="", wait_interval_sec = None, wait_max_count = None ) : """ Send a command to the specified UPS Returns OK on success or raises an error @@ -432,21 +715,42 @@ Returns OK on success or raises an error if self.__debug : print( "[DEBUG] RunUPSCommand called..." ) + do_wait = False + if not (wait_interval_sec is None or wait_interval_sec < 1 or + wait_max_count is None or wait_max_count < 1) : + do_wait = True + self.EnableTrackingModeOnce() + self.__srv_handler.send( ("INSTCMD %s %s\n" % ( ups, command )).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) else : - raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + if ( result[:12] == b"OK TRACKING " ) : + tracking_id = result.replace( b"\n", b"" ).split(b" ")[2].decode('ascii') + if self.__debug : + print( "[DEBUG] Instant command %s sent successfully, got tracking ID '%s'" % (command, tracking_id) ) + + tid = TrackingID(tracking_id) + if do_wait and self.WaitTrackingResult(tid, wait_interval_sec, wait_max_count): + return( "OK" ) + return( tid ) + else: + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) def DeviceLogin( self, ups="") : """ Establish a login session with a device (like upsmon does) +This assumes the upsmon SECONDARY role so we can be alerted and initiate a shutdown +if someone else sends the FSD command to this UPS; we can further becomePrimary() +if we are the system which manages it. + Returns OK on success or raises an error + USERNAME and PASSWORD must have been specified earlier in the session (once) and upsd.conf should permit that user with one of `upsmon` role types. -Note there is no "device LOGOUT" in the protocol, just one for general end +Note there is no "device LOGOUT" in the protocol, just one for a general end of connection. """ @@ -469,29 +773,42 @@ of connection. else : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - def FSD( self, ups="") : - """ Send FSD command + def becomePrimary( self, ups="") : + """ Assume the upsmon PRIMARY role so we can send FSD command Returns OK on success or raises an error NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY (and backwards-compatible alias handling) """ - if self.__debug : print( "[DEBUG] PRIMARY called..." ) self.__srv_handler.send( ("PRIMARY %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) - if ( result != b"OK PRIMARY-GRANTED\n" ) : - if self.__debug : - print( "[DEBUG] Retrying: MASTER called..." ) - self.__srv_handler.send( ("MASTER %s\n" % ups).encode('ascii') ) - result = self.__read_until( b"\n" ) - if ( result != b"OK MASTER-GRANTED\n" ) : - if self.__debug : - print( "[DEBUG] Primary level functions are not available" ) - raise PyNUTError( "ERR ACCESS-DENIED" ) + if ( result == b"OK PRIMARY-GRANTED\n" ) : + return( "OK" ) + + if self.__debug : + print( "[DEBUG] Retrying: MASTER called..." ) + self.__srv_handler.send( ("MASTER %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) + if ( result == b"OK MASTER-GRANTED\n" ) : + return( "OK" ) + + if self.__debug : + print( "[DEBUG] Primary level functions are not available" ) + raise PyNUTError( "ERR ACCESS-DENIED" ) + + def FSD( self, ups="", becomePrimaryFirst = True) : + """ Send FSD command + +Returns OK on success or raises an error + """ + + if becomePrimaryFirst: + # raises an error if not "OK", so not checking return value + self.becomePrimary(ups) if self.__debug : print( "[DEBUG] FSD called..." ) diff --git a/scripts/python/module/test_nutclient.py.in b/scripts/python/module/test_nutclient.py.in index f92f4baae0..701f09c853 100755 --- a/scripts/python/module/test_nutclient.py.in +++ b/scripts/python/module/test_nutclient.py.in @@ -53,7 +53,7 @@ if __name__ == "__main__" : # driver = dummy-ups # desc = "Test device" # port = /src/nut/data/evolution500.seq - print( 80*"-" + "\nTesting 'GetUPSVars' for 'dummy' (should be registered in upsd.conf) :") + print( 80*"-" + "\nTesting 'GetUPSVars' for 'dummy' (should be registered in ups.conf) :") result = nut.GetUPSVars( "dummy" ) print( "\033[01;33m%s\033[0m\n" % result ) @@ -101,8 +101,38 @@ if __name__ == "__main__" : failed.append('SetUPSVar') print( "\033[01;33m%s\033[0m\n" % result ) + print( 80*"-" + "\nTesting 'SetRWVar' with TRACKING for 'driver.debug' (1s interval, 10s timeout):") + try : + # Note: UPS1 is often used in NUT tests as a common device name + # Also try 'dummy' which is used in this test script + target_ups = "UPS1" + try: + nut.GetUPSVars(target_ups) + except: + target_ups = "dummy" + + tid = nut.SetRWVar( target_ups, "driver.debug", "1", wait_interval_sec=1, wait_max_count=10 ) + if (NUT_USER is None): + raise AssertionError("Secure operation should have failed due to lack of credentials, but did not") + + if isinstance(tid, PyNUT.TrackingID): + print( "Got TRACKING ID: %s, created: %s, age: %0.2fs" % (str(tid), tid.created(), tid.age()) ) + result = str(tid) + else: + result = tid + except : + ex = str(sys.exc_info()[1]) + result = "EXCEPTION: " + ex + if (NUT_USER is None and ex == 'ERR USERNAME-REQUIRED'): + result = result + "\n(anticipated error: no credentials were provided)" + else: + if (ex != 'ERR VAR-NOT-SUPPORTED' and (NUT_USER is not None and ex != 'ERR ACCESS-DENIED') ): + result = result + "\nTEST-CASE FAILED" + failed.append('SetUPSVar-Tracking') + print( "\033[01;33m%s\033[0m\n" % result ) + # testing who has an upsmon-like log-in session to a device - print( 80*"-" + "\nTesting 'ListClients' for 'dummy' (should be registered in upsd.conf) before test client is connected :") + print( 80*"-" + "\nTesting 'ListClients' for 'dummy' (should be registered in ups.conf) before test client is connected :") try : result = nut.ListClients( "dummy" ) except : @@ -126,7 +156,7 @@ if __name__ == "__main__" : print( "\033[01;33m%s\033[0m\n" % result ) loggedIntoDummy = False - print( 80*"-" + "\nTesting 'DeviceLogin' for 'dummy' (should be registered in upsd.conf; current credentials should have an upsmon role in upsd.users) :") + print( 80*"-" + "\nTesting 'DeviceLogin' for 'dummy' (should be registered in ups.conf; current credentials should have an upsmon role in upsd.users) :") try : result = nut.DeviceLogin( "dummy" ) if (NUT_USER is None): diff --git a/server/netssl.c b/server/netssl.c index 56c74c04cf..4bdeaea46c 100644 --- a/server/netssl.c +++ b/server/netssl.c @@ -375,9 +375,21 @@ void net_starttls(nut_ctype_t *client, size_t numarg, const char **arg) PRFileDesc *socket; # endif /* WITH_OPENSSL | WITH_NSS */ + static char msg_id_ssl[] = +# ifdef WITH_OPENSSL + " OpenSSL" +# elif defined(WITH_NSS) /* WITH_OPENSSL */ + " NSS" +# else /* neither */ + "out" +# endif /* WITH_OPENSSL | WITH_NSS */ + ; + NUT_UNUSED_VARIABLE(numarg); NUT_UNUSED_VARIABLE(arg); + upsdebugx(2, "%s: handling a connection upgrade request, server side built with%s SSL support", __func__, msg_id_ssl); + if (client->ssl) { upsdebugx(2, "%s: NUT_ERR_ALREADY_SSL_MODE because this connection is already initialized as SSL", __func__); diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 2acc69cbd0..1f84ce8fb7 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -11,6 +11,10 @@ # To speed up this practical developer-testing aspect, you can just # `make check-NIT-sandbox{,-devel}` (optionally with custom DEBUG_SLEEP). # +# Also note that it can be used not only for in-tree checks, but for +# development of other NUT clients against even a packaged installation, +# for a practical example see https://github.com/networkupstools/jNut +# # WARNING: Current working directory when starting the script should be # the location where it may create temporary data (e.g. the BUILDDIR). # Caller can export envvars to impact the script behavior, e.g.: @@ -2782,6 +2786,168 @@ testcases_sandbox_python() { #################################### +setenv_ssl_perl() { + setenv_ssl_python + + if isTestablePerl && [ -n "${PERL}" ] ; then + $PERL -e "use IO::Socket::SSL;" || { + log_warn "The perl interpreter '$PERL' can not use IO::Socket::SSL module, so we will not FORCESSL in the test" + NUT_FORCESSL=0 + export NUT_FORCESSL + # Let the test script and eventually module auto-detect undef => can_ssl + unset NUT_SSL + } + fi + + # Numeric result of equality (1) inverted vs shell success code (0), so `ne`: + if $PERL -e 'exit ( $^O ne "darwin" );' ; then + # TODO: Fix https://github.com/networkupstools/nut/issues/3404 + log_warn "Disabling CERTVERIFY on darwin platform" + NUT_CERTVERIFY=0 + export NUT_CERTVERIFY + #unset NUT_CERTVERIFY + fi + + if [ x"${NUT_DEBUG_SSL_PERL}" = x ] ; then + log_info "Neutering NUT_DEBUG_SSL_PERL to make less noise by default" + NUT_DEBUG_SSL_PERL=-1 + export NUT_DEBUG_SSL_PERL + fi +} + +PL_SHEBANG="" +PL_RES=127 +isTestablePerl() { + # Currently we use any PERL on path and do not detect it in configure script: + case x"${PL_SHEBANG}" in + x"") ;; # Fall through to detection + *) return $PL_RES ;; # Probably resolved (if not a comment)? + esac + + if [ x"${TOP_SRCDIR}" = x ] \ + || [ ! -x "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" ] \ + ; then + return 1 + fi + + if [ ! -s "${TOP_SRCDIR}/scripts/perl/UPS/Nut.pm" ] \ + ; then + return 1 + fi + + if [ x"${PERL}" != x ] ; then + PL_SHEBANG="#!${PERL}" + PL_RES=0 + return 0 + fi + + PL_SHEBANG="`head -1 \"${TOP_SRCDIR}/scripts/perl/test_nutclient.pl\"`" + PL_RES=3 + case x"${PL_SHEBANG}" in + x"#!"/*|x"#!"?":\\"*|x"#!"?":/"*) PL_RES=0 ;; # Seems like a full path + x"#!no") PL_RES=1 ;; # Explicitly skipped + x"#!@") PL_RES=2 ;; # Unresolved + *) PL_RES=3 ;; # Unexpected twist + esac + if [ x"${PL_RES}" = x0 ] ; then + log_debug "=======\nDetected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" + # Currently we use any PERL on path and do not detect it in configure script, + # so the hard-coded value may be bogus: + PERL="`echo \"${PL_SHEBANG}\" | sed 's,^#!,,'`" + if [ -x "$PERL" ] ; then : ; else PERL="`command -v perl`" ; fi + if [ -n "$PERL" ] && [ -x "$PERL" ] ; then : ; else PL_RES=3 ; fi + else + log_error "[isTestablePerl] Detected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" + fi + + PERL_OPTS_INC="-I${TOP_SRCDIR}/scripts/perl" + PERL_OPTS_DEBUG='' + if [ x"$NIT_DEBUG_PERL" = xtrue ] ; then + if [ -d "${HOME}/perl5/lib/perl5" ] ; then + PERL_OPTS_DEBUG="-I${HOME}/perl5/lib/perl5" + fi + $PERL $PERL_OPTS_DEBUG -e 'use Devel::DumpTrace;' && PERL_OPTS_DEBUG="$PERL_OPTS_DEBUG -d:DumpTrace" \ + || { $PERL $PERL_OPTS_DEBUG -e 'use Devel::Trace;' && PERL_OPTS_DEBUG="$PERL_OPTS_DEBUG -d:Trace" ; } \ + || { log_warn "Could not find Devel::DumpTrace nor Devel::Trace" ; unset PERL_OPTS_DEBUG ; } + fi + + return $PL_RES +} + +testcase_sandbox_perl_without_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + log_separator + log_info "[testcase_sandbox_perl_without_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) without login credentials" + if ( unset NUT_USER || true + unset NUT_PASS || true + setenv_ssl_perl + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_without_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_without_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_without_credentials" + fi +} + +testcase_sandbox_perl_with_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + # That script says it expects data/evolution500.seq (as the UPS1 dummy) + # but the dummy data does not currently let issue the commands and + # setvars tested from perl script. + log_separator + log_info "[testcase_sandbox_perl_with_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) with login credentials" + if ( + NUT_USER='admin' + NUT_PASS="${TESTPASS_ADMIN}" + export NUT_USER NUT_PASS + setenv_ssl_perl + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_with_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_with_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_with_credentials" + fi +} + +testcase_sandbox_perl_with_upsmon_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + log_separator + log_info "[testcase_sandbox_perl_with_upsmon_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) with upsmon role login credentials" + if ( + NUT_USER='dummy-admin' + NUT_PASS="${TESTPASS_UPSMON_PRIMARY}" + export NUT_USER NUT_PASS + setenv_ssl_perl + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_with_upsmon_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_with_upsmon_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_with_upsmon_credentials" + fi +} + +testcases_sandbox_perl() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + testcase_sandbox_perl_without_credentials + testcase_sandbox_perl_with_credentials + testcase_sandbox_perl_with_upsmon_credentials +} + +#################################### + isTestableCppNIT() { # We optionally make and here can run C++ client tests: if [ x"${TOP_BUILDDIR}" = x ] \ @@ -3056,6 +3222,7 @@ testgroup_sandbox() { testcase_sandbox_upsc_query_timer testcases_sandbox_python testcases_sandbox_cppnit + testcases_sandbox_perl testcases_sandbox_nutscanner log_separator @@ -3071,6 +3238,15 @@ testgroup_sandbox_python() { sandbox_forget_configs } +testgroup_sandbox_perl() { + # Arrange for quick test iterations + testcase_sandbox_start_drivers_after_upsd + testcases_sandbox_perl + + log_separator + sandbox_forget_configs +} + testgroup_sandbox_cppnit() { # Arrange for quick test iterations testcase_sandbox_start_drivers_after_upsd @@ -3112,9 +3288,46 @@ testgroup_sandbox_upsmon_master() { case "${NIT_CASE}" in isBusy_NUT_PORT) DEBUG=yes isBusy_NUT_PORT ;; - cppnit) testgroup_sandbox_cppnit ;; - python) testgroup_sandbox_python ;; - nutscanner|nut-scanner) testgroup_sandbox_nutscanner ;; + cppnit) + if isTestableCppNIT ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_cppnit" + testgroup_sandbox_cppnit + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + python) + if isTestablePython && [ -n "${PYTHON}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_python" + testgroup_sandbox_python + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + perl) + if isTestablePerl && [ -n "${PERL}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_perl" + testgroup_sandbox_perl + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + nutscanner|nut-scanner) + if isTestableNutScanner && [ -n "${PERL}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_nutscanner" + testgroup_sandbox_nutscanner + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; testcase_*|testgroup_*|testcases_*|testgroups_*) log_warn "========================================================" log_warn "You asked to run just a specific testcase* or testgroup*" @@ -3123,6 +3336,7 @@ case "${NIT_CASE}" in log_warn "========================================================" # NOTE: Not quoted, can have further arguments # e.g. NIT_CASE="testgroup_sandbox_upsmon_master 1" + log_info "Running NIT_CASE='$NIT_CASE'" eval ${NIT_CASE} ;; generatecfg_*|is*) @@ -3134,6 +3348,7 @@ case "${NIT_CASE}" in log_warn "Notably, NUT_CONFPATH='$NUT_CONFPATH' now" log_warn "========================================================" # NOTE: Not quoted, can have further arguments + log_info "Running NIT_CASE='$NIT_CASE'" eval ${NIT_CASE} if [ $? = 0 ] ; then PASSED="`expr $PASSED + 1`" diff --git a/tests/cpputest-client.cpp b/tests/cpputest-client.cpp index 3fef89413d..f470b82ae4 100644 --- a/tests/cpputest-client.cpp +++ b/tests/cpputest-client.cpp @@ -526,9 +526,14 @@ void NutActiveClientTest::test_auth_user() { usleep(100); } if (tres != SUCCESS) { - std::cerr << "[D] Failed to set device variable: " + std::cerr << "[D] Failed to confirm setting device variable: " << "tracking result is " << tres << std::endl; - noException = false; + if (c.isTrackingModeEnabled()) { + noException = false; + } else { + std::cerr << "[D] Tracking mode is NOT enabled in this session so far" + << std::endl; + } } /* Check what we got after set */ /* Note that above we told the server to tell the driver @@ -555,9 +560,14 @@ void NutActiveClientTest::test_auth_user() { usleep(100); } if (tres != SUCCESS) { - std::cerr << "[D] Failed to set device variable: " + std::cerr << "[D] Failed to confirm setting device variable: " << "tracking result is " << tres << std::endl; - noException = false; + if (c.isTrackingModeEnabled()) { + noException = false; + } else { + std::cerr << "[D] Tracking mode is NOT enabled in this session so far" + << std::endl; + } } std::string s3; for (i = 0; i < 100 ; i++) { @@ -587,6 +597,31 @@ void NutActiveClientTest::test_auth_user() { if (noException) { std::cerr << "[D] Tweaked device variable value OK" << std::endl; } + + /* Specific test: SET VAR driver.debug 1 with TRACKING (1s interval, 10s timeout) */ + std::cerr << "[D] Testing SET VAR " << env_NUT_SETVAR_DEVICE << " driver.debug 1 with TRACKING..." << std::endl; + try { + tid = c.setDeviceVariable(env_NUT_SETVAR_DEVICE, "driver.debug", "1", 1, 10); + if (tid.empty()) { + std::cerr << "[D] Failed to get tracking ID for driver.debug" << std::endl; + noException = false; + } else { + tres = tid.getStatus(); + std::cerr << "[D] Got tracking ID: " << (std::string)tid << ", created: " << tid.created() << ", status: " << tres << std::endl; + if (tres == PENDING || tres == UNSET) + tres = c.getTrackingResult(tid); + std::cerr << "[D] Final tracking result: " << tres << " (age: " << tid.age() << "s, duration: " << tid.duration() << "s)" << std::endl; + if (tres != SUCCESS) { + std::cerr << "[D] TRACKING failed for driver.debug" << std::endl; + noException = false; + } + } + } + catch(nut::NutException& ex) + { + std::cerr << "[D] Failed to set driver.debug with tracking: " << ex.what() << std::endl; + noException = false; + } } catch(nut::NutException& ex) {