From 94f6109dfcc34952b00179526b84a6d82eb24e34 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:06:00 +0000 Subject: [PATCH 01/16] added replacement read_until and leftover buffer --- scripts/python/module/PyNUT.py.in | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 6838fc373a..484df8b25a 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -84,6 +84,7 @@ class PyNUTClient : __password = None __timeout = None __srv_handler = None + __recv_leftover = b'' __version = "1.6.0" __release = "2023-01-18" @@ -122,6 +123,19 @@ timeout : Timeout used to wait for network response except : pass + def __read_until(self, finished_reading_data): + data = self.__recv_leftover + while (data_end_index := data.find(finished_reading_data)) == -1: + data += self.__srv_handler.recv(50) # nut_telnetlib.py uses 50 + data_end_index += len(finished_reading_data) + + if data_end_index == len(data): + self.__recv_leftover = b'' + else: + self.__recv_leftover = data[data_end_index:] + data = data[:data_end_index] + return data + def __connect( self ) : """ Connects to the defined server From 60a7ca19a87491be7b58f693977ea5a365290842 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:14:33 +0000 Subject: [PATCH 02/16] switch to socket send and replacement read_until --- scripts/python/module/PyNUT.py.in | 107 ++++++++++++++---------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 484df8b25a..18f43954ca 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -55,8 +55,16 @@ # 2023-01-18 Jim Klimov - Version 1.6.0 # Added CheckUPSAvailable() method originally by Michal Hlavinka # from 2013-01-07 RedHat/Fedora packaging +# +# 2024-07-01 Jim Klimov - Version 1.7.0 +# Re-arranged dependency on telnetlib module (deprecated/removed +# since Python 3.11/3.13), so we can fall back on a privately +# stashed copy until a better solution is developed. +# +# 2025-01-31 cgar - Version 1.8.0 +# Removed telnetlib dependency. Switched to using socket directly. -import telnetlib +import socket class PyNUTError( Exception ) : """ Base class for custom exceptions """ @@ -119,7 +113,7 @@ timeout : Timeout used to wait for network response def __del__( self ) : """ Class destructor method """ try : - self.__srv_handler.write( b"LOGOUT\n" ) + self.__srv_handler.send( b"LOGOUT\n" ) except : pass @@ -145,23 +139,24 @@ if something goes wrong. if self.__debug : print( "[DEBUG] Connecting to host" ) - self.__srv_handler = telnetlib.Telnet( self.__host, self.__port ) + self.__srv_handler = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) + self.__srv_handler.connect( (self.__host, self.__port) ) if self.__login != None : - self.__srv_handler.write( ("USERNAME %s\n" % self.__login).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n", self.__timeout ) + self.__srv_handler.send( ("USERNAME %s\n" % self.__login).encode('ascii') ) + result = self.__read_until( b"\n" ) if result[:2] != b"OK" : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) if self.__password != None : - self.__srv_handler.write( ("PASSWORD %s\n" % self.__password).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n", self.__timeout ) + self.__srv_handler.send( ("PASSWORD %s\n" % self.__password).encode('ascii') ) + result = self.__read_until( b"\n" ) if result[:2] != b"OK" : if result == b"ERR INVALID-ARGUMENT\n" : # Quote the password (if it has whitespace etc) # TODO: Escape special chard like NUT does? - self.__srv_handler.write( ("PASSWORD \"%s\"\n" % self.__password).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n", self.__timeout ) + self.__srv_handler.send( ("PASSWORD \"%s\"\n" % self.__password).encode('ascii') ) + result = self.__read_until( b"\n" ) if result[:2] != b"OK" : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) else: @@ -179,12 +174,12 @@ which is of little concern for Python2 but is important in Python3 if self.__debug : print( "[DEBUG] GetUPSList from server" ) - self.__srv_handler.write( b"LIST UPS\n" ) - result = self.__srv_handler.read_until( b"\n" ) - if result != b"BEGIN LIST UPS\n" : + self.__srv_handler.send( b"LIST UPS\n" ) + result = self.__read_until( b"\n" ) + if result != b"BEGIN LIST UPS\n": raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - result = self.__srv_handler.read_until( b"END LIST UPS\n" ) + result = self.__read_until( b"END LIST UPS\n" ) ups_list = {} for line in result.split( b"\n" ) : @@ -219,13 +214,13 @@ available vars. if self.__debug : print( "[DEBUG] GetUPSVars called..." ) - self.__srv_handler.write( ("LIST VAR %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("LIST VAR %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if result != ("BEGIN LIST VAR %s\n" % ups).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) ups_vars = {} - result = self.__srv_handler.read_until( ("END LIST VAR %s\n" % ups).encode('ascii') ) + result = self.__read_until( ("END LIST VAR %s\n" % ups).encode('ascii') ) offset = len( ("VAR %s " % ups ).encode('ascii') ) end_offset = 0 - ( len( ("END LIST VAR %s\n" % ups).encode('ascii') ) + 1 ) @@ -245,12 +240,12 @@ The result is True (reachable) or False (unreachable) if self.__debug : print( "[DEBUG] CheckUPSAvailable called..." ) - self.__srv_handler.write( ("LIST CMD %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("LIST CMD %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if result != ("BEGIN LIST CMD %s\n" % ups).encode('ascii') : return False - self.__srv_handler.read_until( ("END LIST CMD %s\n" % ups).encode('ascii') ) + self.__read_until( ("END LIST CMD %s\n" % ups).encode('ascii') ) return True def GetUPSCommands( self, ups="" ) : @@ -262,13 +257,13 @@ of the command as value if self.__debug : print( "[DEBUG] GetUPSCommands called..." ) - self.__srv_handler.write( ("LIST CMD %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("LIST CMD %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if result != ("BEGIN LIST CMD %s\n" % ups).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) ups_cmds = {} - result = self.__srv_handler.read_until( ("END LIST CMD %s\n" % ups).encode('ascii') ) + result = self.__read_until( ("END LIST CMD %s\n" % ups).encode('ascii') ) offset = len( ("CMD %s " % ups).encode('ascii') ) end_offset = 0 - ( len( ("END LIST CMD %s\n" % ups).encode('ascii') ) + 1 ) @@ -277,8 +272,8 @@ of the command as value # For each var we try to get the available description try : - self.__srv_handler.write( ("GET CMDDESC %s %s\n" % ( ups, var )).encode('ascii') ) - temp = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("GET CMDDESC %s %s\n" % ( ups, var )).encode('ascii') ) + temp = self.__read_until( b"\n" ) if temp[:7] != b"CMDDESC" : raise PyNUTError else : @@ -299,12 +294,12 @@ The result is presented as a dictionary containing 'key->val' pairs if self.__debug : print( "[DEBUG] GetUPSVars from '%s'..." % ups ) - self.__srv_handler.write( ("LIST RW %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("LIST RW %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if ( result != ("BEGIN LIST RW %s\n" % ups).encode('ascii') ) : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - result = self.__srv_handler.read_until( ("END LIST RW %s\n" % ups).encode('ascii') ) + result = self.__read_until( ("END LIST RW %s\n" % ups).encode('ascii') ) offset = len( ("VAR %s" % ups).encode('ascii') ) end_offset = 0 - ( len( ("END LIST RW %s\n" % ups).encode('ascii') ) + 1 ) rw_vars = {} @@ -327,8 +322,8 @@ The variable must be a writable value (cf GetRWVars) and you must have the prope rights to set it (maybe login/password). """ - self.__srv_handler.write( ("SET VAR %s %s %s\n" % ( ups, var, value )).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + 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 : @@ -343,8 +338,8 @@ Returns OK on success or raises an error if self.__debug : print( "[DEBUG] RunUPSCommand called..." ) - self.__srv_handler.write( ("INSTCMD %s %s\n" % ( ups, command )).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + 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 : @@ -369,12 +364,12 @@ of connection. print( "[DEBUG] DeviceLogin: %s is not a valid UPS" % ups ) raise PyNUTError( "ERR UNKNOWN-UPS" ) - self.__srv_handler.write( ("LOGIN %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("LOGIN %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if ( result.startswith( ("User %s@" % self.__login).encode('ascii')) and result.endswith (("[%s]\n" % ups).encode('ascii')) ): # User dummy-user@127.0.0.1 logged into UPS [dummy] # Read next line then - result = self.__srv_handler.read_until( b"\n" ) + result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) else : @@ -392,13 +387,13 @@ NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY if self.__debug : print( "[DEBUG] PRIMARY called..." ) - self.__srv_handler.write( ("PRIMARY %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + 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.write( ("MASTER %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + 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" ) @@ -406,8 +401,8 @@ NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY if self.__debug : print( "[DEBUG] FSD called..." ) - self.__srv_handler.write( ("FSD %s\n" % ups).encode('ascii') ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( ("FSD %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) if ( result == b"OK FSD-SET\n" ) : return( "OK" ) else : @@ -420,8 +415,8 @@ NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY if self.__debug : print( "[DEBUG] HELP called..." ) - self.__srv_handler.write( b"HELP\n" ) - return self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( b"HELP\n" ) + return self.__read_until( b"\n" ) def ver(self) : """ Send VER command @@ -430,8 +425,8 @@ NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY if self.__debug : print( "[DEBUG] VER called..." ) - self.__srv_handler.write( b"VER\n" ) - return self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( b"VER\n" ) + return self.__read_until( b"\n" ) def ListClients( self, ups = None ) : """ Returns the list of connected clients from the NUT server @@ -449,12 +444,12 @@ The result is a dictionary containing 'key->val' pairs of 'UPSName' and a list o raise PyNUTError( "ERR UNKNOWN-UPS" ) if ups: - self.__srv_handler.write( ("LIST CLIENT %s\n" % ups).encode('ascii') ) + self.__srv_handler.send( ("LIST CLIENT %s\n" % ups).encode('ascii') ) else: # NOTE: Currently NUT does not support just listing all clients # (not providing an "ups" argument) => NUT_ERR_INVALID_ARGUMENT - self.__srv_handler.write( b"LIST CLIENT\n" ) - result = self.__srv_handler.read_until( b"\n" ) + self.__srv_handler.send( b"LIST CLIENT\n" ) + result = self.__read_until( b"\n" ) if ( (ups and result != ("BEGIN LIST CLIENT %s\n" % ups).encode('ascii')) or (ups is None and result != b"BEGIN LIST CLIENT\n") ): if ups is None and (result == b"ERR INVALID-ARGUMENT\n") : # For ups==None, list all upses, list their clients @@ -472,11 +467,11 @@ The result is a dictionary containing 'key->val' pairs of 'UPSName' and a list o raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - if ups : - result = self.__srv_handler.read_until( ("END LIST CLIENT %s\n" % ups).encode('ascii') ) + if ups: + result = self.__read_until( ("END LIST CLIENT %s\n" % ups).encode('ascii') ) else: # Should not get here with current NUT: - result = self.__srv_handler.read_until( b"END LIST CLIENT\n" ) + result = self.__read_until( b"END LIST CLIENT\n" ) ups_list = {} for line in result.split( b"\n" ): From 1209078c62ad4b6138029f59b33dff4008235ad5 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 31 Jan 2025 06:19:16 +0000 Subject: [PATCH 03/16] Switch to socket timeout, fix its init argument --- scripts/python/module/PyNUT.py.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 18f43954ca..66e4b6c0d6 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -105,7 +105,7 @@ timeout : Timeout used to wait for network response self.__port = port self.__login = login self.__password = password - self.__timeout = 5 + self.__timeout = timeout self.__connect() @@ -141,6 +141,7 @@ if something goes wrong. self.__srv_handler = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) self.__srv_handler.connect( (self.__host, self.__port) ) + self.__srv_handler.settimeout( self.__timeout ) if self.__login != None : self.__srv_handler.send( ("USERNAME %s\n" % self.__login).encode('ascii') ) From 7ee628f920bd11dd5565b0e10cbfa6fd54aa23c8 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:27:01 +0000 Subject: [PATCH 04/16] Remove walrus op for older python compatibility. --- scripts/python/module/PyNUT.py.in | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 66e4b6c0d6..ced5231193 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -119,8 +119,12 @@ timeout : Timeout used to wait for network response def __read_until(self, finished_reading_data): data = self.__recv_leftover - while (data_end_index := data.find(finished_reading_data)) == -1: - data += self.__srv_handler.recv(50) # nut_telnetlib.py uses 50 + while True: + data_end_index = data.find(finished_reading_data) + if data_end_index == -1: + data += self.__srv_handler.recv(50) # nut_telnetlib.py uses 50 + else: + break data_end_index += len(finished_reading_data) if data_end_index == len(data): From 9f81e81af3f8bed3fb1e501b5c0b8f638f28f125 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:45:40 +0000 Subject: [PATCH 05/16] Use socket create_connection instead of connect, Handles choosing IPv4 and IPv6 automatically. --- scripts/python/module/PyNUT.py.in | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index ced5231193..b815c4af73 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -143,9 +143,10 @@ if something goes wrong. if self.__debug : print( "[DEBUG] Connecting to host" ) - self.__srv_handler = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) - self.__srv_handler.connect( (self.__host, self.__port) ) - self.__srv_handler.settimeout( self.__timeout ) + self.__srv_handler = socket.create_connection( + (self.__host, self.__port), + self.__timeout + ) if self.__login != None : self.__srv_handler.send( ("USERNAME %s\n" % self.__login).encode('ascii') ) From cf0ea3e447f3d97237ba8575202437a59355bd3a Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:47:38 +0000 Subject: [PATCH 06/16] Update PyNUT changelog, bump version to 1.8.0 --- scripts/python/module/PyNUT.py.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index b815c4af73..0e136d3669 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -74,8 +82,8 @@ class PyNUTClient : __srv_handler = None __recv_leftover = b'' - __version = "1.6.0" - __release = "2023-01-18" + __version = "1.8.0" + __release = "2025-02-07" def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=5 ) : From 0488ec573943468fb0e0570c396b66c2dfcc35d9 Mon Sep 17 00:00:00 2001 From: cgarz <20268068+cgarz@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:57:21 +0000 Subject: [PATCH 12/16] Update setup.py.in, remove install_requires lib --- scripts/python/module/setup.py.in | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/python/module/setup.py.in b/scripts/python/module/setup.py.in index 10ada3ecd6..d520c960f9 100644 --- a/scripts/python/module/setup.py.in +++ b/scripts/python/module/setup.py.in @@ -30,7 +30,6 @@ setup( #data_files = [('', ['tox.ini'])], #scripts = ['PyNUTClient/test_nutclient.py', 'PyNUTClient/__init__.py'], python_requires = '>=2.6', - # install_requires = ['telnetlib'], # NOTE: telnetlib.py is part of Python core for tested 2.x and 3.x versions, not something 'pip' can download keywords = ['pypi', 'cicd', 'python', 'nut', 'Network UPS Tools'], classifiers = [ "Development Status :: 5 - Production/Stable", From 6d671b039e6df10091f6219ed9ae7169bf25c6e5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 7 Feb 2025 12:24:14 +0100 Subject: [PATCH 14/16] configure.ac: restore nut_with_pynut_py* checks from v2.8.2, but test for "socket" instead of "telnetlib" module [#2183] Signed-off-by: Jim Klimov --- configure.ac | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/configure.ac b/configure.ac index 07febb3cb8..8d278d8085 100644 --- a/configure.ac +++ b/configure.ac @@ -2384,7 +2384,7 @@ if test x"${nut_with_pynut}" != xno \ if test -n "${PYTHON2}" \ && (command -v ${PYTHON2} || which ${PYTHON2}) >/dev/null 2>/dev/null \ ; then - if ${PYTHON2} -c "import telnetlib" \ + if ${PYTHON2} -c "import socket" \ ; then nut_with_pynut_py2="yes" fi @@ -2393,7 +2393,7 @@ if test x"${nut_with_pynut}" != xno \ if test -n "${PYTHON3}" \ && (command -v ${PYTHON3} || which ${PYTHON3}) >/dev/null 2>/dev/null \ ; then - if ${PYTHON3} -c "import telnetlib" \ + if ${PYTHON3} -c "import socket" \ ; then nut_with_pynut_py3="yes" fi @@ -2404,7 +2404,7 @@ if test x"${nut_with_pynut}" != xno \ && (command -v ${PYTHON} || which ${PYTHON}) >/dev/null 2>/dev/null \ && test "${PYTHON}" != "${PYTHON2}" -a "${PYTHON}" != "${PYTHON3}" \ ; then - if ${PYTHON} -c "import telnetlib" \ + if ${PYTHON} -c "import socket" \ ; then nut_with_pynut_py="yes" fi From c5540c43d28d7bfcae9426b92a44a805d7ac27bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 7 Feb 2025 15:32:34 +0100 Subject: [PATCH 16/16] tests/NIT/nit.sh: update comment about use of two explicit localhost IPv4/6 addresses, not a name, for upsd.conf Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index befec52803..cf0a98bb7f 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -363,8 +363,8 @@ EOF fi # Some systems listining on symbolic "localhost" actually - # only bind to IPv6, and Python telnetlib resolves IPv4 - # and fails its connection tests. Others fare well with + # only bind to IPv6, and some (Python) client might resolve + # IPv4 and fail its connection tests. Others fare well with # both addresses in one command. for LH in 127.0.0.1 '::1' ; do if (