diff --git a/package/nut/0001-Provide-a-private-copy-of-telnetlib-for-PyNUTClient.patch b/package/nut/0001-Provide-a-private-copy-of-telnetlib-for-PyNUTClient.patch new file mode 100644 index 0000000000..9273ee517b --- /dev/null +++ b/package/nut/0001-Provide-a-private-copy-of-telnetlib-for-PyNUTClient.patch @@ -0,0 +1,878 @@ +From 84c8ddef0b70ae14a8867639366501e2988a1092 Mon Sep 17 00:00:00 2001 +From: Jim Klimov +Date: Mon, 1 Jul 2024 15:47:46 +0200 +Subject: [PATCH] Provide a fallback copy of telnetlib module for PyNUTClient + [#2183] + +Signed-off-by: Jim Klimov + +Upstream: https://github.com/networkupstools/nut/commit/84c8ddef0b70ae14a8867639366501e2988a1092 + +Signed-off-by: Bernd Kuhls +--- + configure.ac | 17 +- + docs/nut.dict | 3 +- + scripts/python/Makefile.am | 1 + + scripts/python/README.adoc | 15 + + scripts/python/module/Makefile.am | 7 +- + scripts/python/module/PyNUT.py.in | 18 +- + scripts/python/module/nut_telnetlib.py | 683 +++++++++++++++++++++++++ + scripts/python/module/setup.py.in | 6 +- + 9 files changed, 746 insertions(+), 8 deletions(-) + create mode 100644 scripts/python/module/nut_telnetlib.py + +diff --git a/configure.ac b/configure.ac +index d369ea82b8..6f90e50a5e 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -2447,7 +2447,7 @@ if test x"${nut_with_nut_monitor}" != xno ; then + fi + + dnl ${nut_with_pynut}: TODO: arg values to request python 2, 3 or both +-AC_MSG_CHECKING([if we can and should install PyNUT module]) ++AC_MSG_CHECKING([if we can and should install PyNUT module (note for warnings from python 3.11 and beyond: we have a fallback nut_telnetlib module just in case)]) + nut_with_pynut_py="" + nut_with_pynut_py2="" + nut_with_pynut_py3="" +@@ -2469,6 +2469,15 @@ if test x"${nut_with_pynut}" != xno \ + if ${PYTHON3} -c "import telnetlib" \ + ; then + nut_with_pynut_py3="yes" ++ else ++ dnl We have a stashed copy from Python 3.10, so ++ dnl this line essentially checks for presence of ++ dnl a usable interpreter implementation compatible ++ dnl with Python 3.x syntax. ++ if (cd script/python/module && ${PYTHON3} -c "import nut_telnetlib as telnetlib") \ ++ ; then ++ nut_with_pynut_py3="yes" ++ fi + fi + fi + +@@ -2480,6 +2489,12 @@ if test x"${nut_with_pynut}" != xno \ + if ${PYTHON} -c "import telnetlib" \ + ; then + nut_with_pynut_py="yes" ++ else ++ dnl See comments above ++ if (cd script/python/module && ${PYTHON} -c "import nut_telnetlib as telnetlib") \ ++ ; then ++ nut_with_pynut_py="yes" ++ fi + fi + fi + fi +diff --git a/scripts/python/Makefile.am b/scripts/python/Makefile.am +index 0b83249cd5..a737f50c91 100644 +--- a/scripts/python/Makefile.am ++++ b/scripts/python/Makefile.am +@@ -70,6 +70,7 @@ NUT_MONITOR_COMMON = \ + app/locale/ru/LC_MESSAGES/NUT-Monitor.mo + + PYNUT_COMMON = \ ++ module/nut_telnetlib.py \ + module/README.adoc + + # Note: we both distribute and install the generated *.mo translation files +diff --git a/scripts/python/README.adoc b/scripts/python/README.adoc +index 72e268063b..8770d4ca06 100644 +--- a/scripts/python/README.adoc ++++ b/scripts/python/README.adoc +@@ -35,6 +35,21 @@ and protocol support. + + For one practical example, you can research `tests/NIT/nit.sh` in NUT sources. + ++telnetlib ++^^^^^^^^^ ++ ++Historically, the `PyNUTClient` class relied on `telnetlib` module for socket ++communications with the NUT data server, which was provided as part of Python ++core installation with tested 2.6+ and 3.* versions. The module was, however, ++marked deprecated since Python 3.11 and not provided since 3.13. Due to this, ++as a quick stop-gap solution, NUT sources provide `nut_telnetlib.py` - a copy ++of the module from Python 3.10 installation, and no longer should require its ++presence in the Python deployment. ++ ++A better solution would be to find or create a new layer for the constrained ++use-case of NUT client interactions with a data server, which do not need the ++fully fledged Telnet-like capabilities. ++ + app + ~~~ + +diff --git a/scripts/python/module/Makefile.am b/scripts/python/module/Makefile.am +index fc46eab0ad..5eb650969c 100644 +--- a/scripts/python/module/Makefile.am ++++ b/scripts/python/module/Makefile.am +@@ -16,7 +16,7 @@ check-local: + tox: dist .pypi-tools-tox + tox + +-EXTRA_DIST = tox.ini MANIFEST.in ++EXTRA_DIST = tox.ini MANIFEST.in nut_telnetlib.py + + NUT_SOURCE_GITREV_NUMERIC = @NUT_SOURCE_GITREV_NUMERIC@ + PYTHON = @PYTHON@ +@@ -64,7 +64,9 @@ upload publish: + esac + + # README.txt is also a part of module standard expectations +-.pypi-src: test_nutclient.py.in PyNUT.py.in setup.py.in README.adoc Makefile $(top_srcdir)/LICENSE-GPL3 ++# nut_telnetlib.py is not converted from a .in template, it ++# does not even have a shebang line. ++.pypi-src: test_nutclient.py.in PyNUT.py.in setup.py.in nut_telnetlib.py README.adoc Makefile $(top_srcdir)/LICENSE-GPL3 + @echo " PYPI Generate PyPI module source" + @rm -rf $(GENERATED_SRC) "$@" + @mkdir -p PyNUTClient +@@ -83,6 +85,7 @@ upload publish: + sed -e "s,[@]PYTHON[@],@PYTHON@," < "$(srcdir)/$${B}.in" > "PyNUTClient/$${B}" || exit ; \ + if test -x "$(srcdir)/$${B}.in" ; then chmod +x "PyNUTClient/$${B}"; fi ; \ + done ; \ ++ cp -pf "$(srcdir)/nut_telnetlib.py" PyNUTClient/ || exit ; \ + cp -pf "$(srcdir)/README.adoc" README.txt || exit ; \ + cp -pf "$(top_srcdir)/LICENSE-GPL3" . || exit ; \ + echo "from . import PyNUT" > PyNUTClient/__init__.py || exit +diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in +index 6f3c4ff510..6838fc373a 100644 +--- a/scripts/python/module/PyNUT.py.in ++++ b/scripts/python/module/PyNUT.py.in +@@ -55,8 +55,20 @@ + # 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. ++# + +-import telnetlib ++try: ++ import telnetlib ++except ModuleNotFoundError: ++ import os ++ if "true" == os.getenv('DEBUG', 'false'): ++ print( "[DEBUG] Fall back to private copy of telnetlib for PyNUTClient\n" ) ++ import nut_telnetlib as telnetlib + + class PyNUTError( Exception ) : + """ Base class for custom exceptions """ +@@ -73,8 +85,8 @@ class PyNUTClient : + __timeout = None + __srv_handler = None + +- __version = "1.6.0" +- __release = "2023-01-18" ++ __version = "1.7.0" ++ __release = "2024-07-01" + + + def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=5 ) : +diff --git a/scripts/python/module/nut_telnetlib.py b/scripts/python/module/nut_telnetlib.py +new file mode 100644 +index 0000000000..a9c444f10a +--- /dev/null ++++ b/scripts/python/module/nut_telnetlib.py +@@ -0,0 +1,683 @@ ++r"""TELNET client class. ++ ++------ ++This file has been copied for PyNUTClient fallback needs from Python 3.10 ++(newer Python releases obsoleted the module) and not modified except for ++this notice. ++------ ++ ++Based on RFC 854: TELNET Protocol Specification, by J. Postel and ++J. Reynolds ++ ++Example: ++ ++>>> from telnetlib import Telnet ++>>> tn = Telnet('www.python.org', 79) # connect to finger port ++>>> tn.write(b'guido\r\n') ++>>> print(tn.read_all()) ++Login Name TTY Idle When Where ++guido Guido van Rossum pts/2 snag.cnri.reston.. ++ ++>>> ++ ++Note that read_all() won't read until eof -- it just reads some data ++-- but it guarantees to read at least one byte unless EOF is hit. ++ ++It is possible to pass a Telnet object to a selector in order to wait until ++more data is available. Note that in this case, read_eager() may return b'' ++even if there was data on the socket, because the protocol negotiation may have ++eaten the data. This is why EOFError is needed in some cases to distinguish ++between "no data" and "connection closed" (since the socket also appears ready ++for reading when it is closed). ++ ++To do: ++- option negotiation ++- timeout should be intrinsic to the connection object instead of an ++ option on one of the read calls only ++ ++""" ++ ++ ++# Imported modules ++import sys ++import socket ++import selectors ++from time import monotonic as _time ++ ++__all__ = ["Telnet"] ++ ++# Tunable parameters ++DEBUGLEVEL = 0 ++ ++# Telnet protocol defaults ++TELNET_PORT = 23 ++ ++# Telnet protocol characters (don't change) ++IAC = bytes([255]) # "Interpret As Command" ++DONT = bytes([254]) ++DO = bytes([253]) ++WONT = bytes([252]) ++WILL = bytes([251]) ++theNULL = bytes([0]) ++ ++SE = bytes([240]) # Subnegotiation End ++NOP = bytes([241]) # No Operation ++DM = bytes([242]) # Data Mark ++BRK = bytes([243]) # Break ++IP = bytes([244]) # Interrupt process ++AO = bytes([245]) # Abort output ++AYT = bytes([246]) # Are You There ++EC = bytes([247]) # Erase Character ++EL = bytes([248]) # Erase Line ++GA = bytes([249]) # Go Ahead ++SB = bytes([250]) # Subnegotiation Begin ++ ++ ++# Telnet protocol options code (don't change) ++# These ones all come from arpa/telnet.h ++BINARY = bytes([0]) # 8-bit data path ++ECHO = bytes([1]) # echo ++RCP = bytes([2]) # prepare to reconnect ++SGA = bytes([3]) # suppress go ahead ++NAMS = bytes([4]) # approximate message size ++STATUS = bytes([5]) # give status ++TM = bytes([6]) # timing mark ++RCTE = bytes([7]) # remote controlled transmission and echo ++NAOL = bytes([8]) # negotiate about output line width ++NAOP = bytes([9]) # negotiate about output page size ++NAOCRD = bytes([10]) # negotiate about CR disposition ++NAOHTS = bytes([11]) # negotiate about horizontal tabstops ++NAOHTD = bytes([12]) # negotiate about horizontal tab disposition ++NAOFFD = bytes([13]) # negotiate about formfeed disposition ++NAOVTS = bytes([14]) # negotiate about vertical tab stops ++NAOVTD = bytes([15]) # negotiate about vertical tab disposition ++NAOLFD = bytes([16]) # negotiate about output LF disposition ++XASCII = bytes([17]) # extended ascii character set ++LOGOUT = bytes([18]) # force logout ++BM = bytes([19]) # byte macro ++DET = bytes([20]) # data entry terminal ++SUPDUP = bytes([21]) # supdup protocol ++SUPDUPOUTPUT = bytes([22]) # supdup output ++SNDLOC = bytes([23]) # send location ++TTYPE = bytes([24]) # terminal type ++EOR = bytes([25]) # end or record ++TUID = bytes([26]) # TACACS user identification ++OUTMRK = bytes([27]) # output marking ++TTYLOC = bytes([28]) # terminal location number ++VT3270REGIME = bytes([29]) # 3270 regime ++X3PAD = bytes([30]) # X.3 PAD ++NAWS = bytes([31]) # window size ++TSPEED = bytes([32]) # terminal speed ++LFLOW = bytes([33]) # remote flow control ++LINEMODE = bytes([34]) # Linemode option ++XDISPLOC = bytes([35]) # X Display Location ++OLD_ENVIRON = bytes([36]) # Old - Environment variables ++AUTHENTICATION = bytes([37]) # Authenticate ++ENCRYPT = bytes([38]) # Encryption option ++NEW_ENVIRON = bytes([39]) # New - Environment variables ++# the following ones come from ++# http://www.iana.org/assignments/telnet-options ++# Unfortunately, that document does not assign identifiers ++# to all of them, so we are making them up ++TN3270E = bytes([40]) # TN3270E ++XAUTH = bytes([41]) # XAUTH ++CHARSET = bytes([42]) # CHARSET ++RSP = bytes([43]) # Telnet Remote Serial Port ++COM_PORT_OPTION = bytes([44]) # Com Port Control Option ++SUPPRESS_LOCAL_ECHO = bytes([45]) # Telnet Suppress Local Echo ++TLS = bytes([46]) # Telnet Start TLS ++KERMIT = bytes([47]) # KERMIT ++SEND_URL = bytes([48]) # SEND-URL ++FORWARD_X = bytes([49]) # FORWARD_X ++PRAGMA_LOGON = bytes([138]) # TELOPT PRAGMA LOGON ++SSPI_LOGON = bytes([139]) # TELOPT SSPI LOGON ++PRAGMA_HEARTBEAT = bytes([140]) # TELOPT PRAGMA HEARTBEAT ++EXOPL = bytes([255]) # Extended-Options-List ++NOOPT = bytes([0]) ++ ++ ++# poll/select have the advantage of not requiring any extra file descriptor, ++# contrarily to epoll/kqueue (also, they require a single syscall). ++if hasattr(selectors, 'PollSelector'): ++ _TelnetSelector = selectors.PollSelector ++else: ++ _TelnetSelector = selectors.SelectSelector ++ ++ ++class Telnet: ++ ++ """Telnet interface class. ++ ++ An instance of this class represents a connection to a telnet ++ server. The instance is initially not connected; the open() ++ method must be used to establish a connection. Alternatively, the ++ host name and optional port number can be passed to the ++ constructor, too. ++ ++ Don't try to reopen an already connected instance. ++ ++ This class has many read_*() methods. Note that some of them ++ raise EOFError when the end of the connection is read, because ++ they can return an empty string for other reasons. See the ++ individual doc strings. ++ ++ read_until(expected, [timeout]) ++ Read until the expected string has been seen, or a timeout is ++ hit (default is no timeout); may block. ++ ++ read_all() ++ Read all data until EOF; may block. ++ ++ read_some() ++ Read at least one byte or EOF; may block. ++ ++ read_very_eager() ++ Read all data available already queued or on the socket, ++ without blocking. ++ ++ read_eager() ++ Read either data already queued or some data available on the ++ socket, without blocking. ++ ++ read_lazy() ++ Read all data in the raw queue (processing it first), without ++ doing any socket I/O. ++ ++ read_very_lazy() ++ Reads all data in the cooked queue, without doing any socket ++ I/O. ++ ++ read_sb_data() ++ Reads available data between SB ... SE sequence. Don't block. ++ ++ set_option_negotiation_callback(callback) ++ Each time a telnet option is read on the input flow, this callback ++ (if set) is called with the following parameters : ++ callback(telnet socket, command, option) ++ option will be chr(0) when there is no option. ++ No other action is done afterwards by telnetlib. ++ ++ """ ++ ++ def __init__(self, host=None, port=0, ++ timeout=socket._GLOBAL_DEFAULT_TIMEOUT): ++ """Constructor. ++ ++ When called without arguments, create an unconnected instance. ++ With a hostname argument, it connects the instance; port number ++ and timeout are optional. ++ """ ++ self.debuglevel = DEBUGLEVEL ++ self.host = host ++ self.port = port ++ self.timeout = timeout ++ self.sock = None ++ self.rawq = b'' ++ self.irawq = 0 ++ self.cookedq = b'' ++ self.eof = 0 ++ self.iacseq = b'' # Buffer for IAC sequence. ++ self.sb = 0 # flag for SB and SE sequence. ++ self.sbdataq = b'' ++ self.option_callback = None ++ if host is not None: ++ self.open(host, port, timeout) ++ ++ def open(self, host, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): ++ """Connect to a host. ++ ++ The optional second argument is the port number, which ++ defaults to the standard telnet port (23). ++ ++ Don't try to reopen an already connected instance. ++ """ ++ self.eof = 0 ++ if not port: ++ port = TELNET_PORT ++ self.host = host ++ self.port = port ++ self.timeout = timeout ++ sys.audit("telnetlib.Telnet.open", self, host, port) ++ self.sock = socket.create_connection((host, port), timeout) ++ ++ def __del__(self): ++ """Destructor -- close the connection.""" ++ self.close() ++ ++ def msg(self, msg, *args): ++ """Print a debug message, when the debug level is > 0. ++ ++ If extra arguments are present, they are substituted in the ++ message using the standard string formatting operator. ++ ++ """ ++ if self.debuglevel > 0: ++ print('Telnet(%s,%s):' % (self.host, self.port), end=' ') ++ if args: ++ print(msg % args) ++ else: ++ print(msg) ++ ++ def set_debuglevel(self, debuglevel): ++ """Set the debug level. ++ ++ The higher it is, the more debug output you get (on sys.stdout). ++ ++ """ ++ self.debuglevel = debuglevel ++ ++ def close(self): ++ """Close the connection.""" ++ sock = self.sock ++ self.sock = None ++ self.eof = True ++ self.iacseq = b'' ++ self.sb = 0 ++ if sock: ++ sock.close() ++ ++ def get_socket(self): ++ """Return the socket object used internally.""" ++ return self.sock ++ ++ def fileno(self): ++ """Return the fileno() of the socket object used internally.""" ++ return self.sock.fileno() ++ ++ def write(self, buffer): ++ """Write a string to the socket, doubling any IAC characters. ++ ++ Can block if the connection is blocked. May raise ++ OSError if the connection is closed. ++ ++ """ ++ if IAC in buffer: ++ buffer = buffer.replace(IAC, IAC+IAC) ++ sys.audit("telnetlib.Telnet.write", self, buffer) ++ self.msg("send %r", buffer) ++ self.sock.sendall(buffer) ++ ++ def read_until(self, match, timeout=None): ++ """Read until a given string is encountered or until timeout. ++ ++ When no match is found, return whatever is available instead, ++ possibly the empty string. Raise EOFError if the connection ++ is closed and no cooked data is available. ++ ++ """ ++ n = len(match) ++ self.process_rawq() ++ i = self.cookedq.find(match) ++ if i >= 0: ++ i = i+n ++ buf = self.cookedq[:i] ++ self.cookedq = self.cookedq[i:] ++ return buf ++ if timeout is not None: ++ deadline = _time() + timeout ++ with _TelnetSelector() as selector: ++ selector.register(self, selectors.EVENT_READ) ++ while not self.eof: ++ if selector.select(timeout): ++ i = max(0, len(self.cookedq)-n) ++ self.fill_rawq() ++ self.process_rawq() ++ i = self.cookedq.find(match, i) ++ if i >= 0: ++ i = i+n ++ buf = self.cookedq[:i] ++ self.cookedq = self.cookedq[i:] ++ return buf ++ if timeout is not None: ++ timeout = deadline - _time() ++ if timeout < 0: ++ break ++ return self.read_very_lazy() ++ ++ def read_all(self): ++ """Read all data until EOF; block until connection closed.""" ++ self.process_rawq() ++ while not self.eof: ++ self.fill_rawq() ++ self.process_rawq() ++ buf = self.cookedq ++ self.cookedq = b'' ++ return buf ++ ++ def read_some(self): ++ """Read at least one byte of cooked data unless EOF is hit. ++ ++ Return b'' if EOF is hit. Block if no data is immediately ++ available. ++ ++ """ ++ self.process_rawq() ++ while not self.cookedq and not self.eof: ++ self.fill_rawq() ++ self.process_rawq() ++ buf = self.cookedq ++ self.cookedq = b'' ++ return buf ++ ++ def read_very_eager(self): ++ """Read everything that's possible without blocking in I/O (eager). ++ ++ Raise EOFError if connection closed and no cooked data ++ available. Return b'' if no cooked data available otherwise. ++ Don't block unless in the midst of an IAC sequence. ++ ++ """ ++ self.process_rawq() ++ while not self.eof and self.sock_avail(): ++ self.fill_rawq() ++ self.process_rawq() ++ return self.read_very_lazy() ++ ++ def read_eager(self): ++ """Read readily available data. ++ ++ Raise EOFError if connection closed and no cooked data ++ available. Return b'' if no cooked data available otherwise. ++ Don't block unless in the midst of an IAC sequence. ++ ++ """ ++ self.process_rawq() ++ while not self.cookedq and not self.eof and self.sock_avail(): ++ self.fill_rawq() ++ self.process_rawq() ++ return self.read_very_lazy() ++ ++ def read_lazy(self): ++ """Process and return data that's already in the queues (lazy). ++ ++ Raise EOFError if connection closed and no data available. ++ Return b'' if no cooked data available otherwise. Don't block ++ unless in the midst of an IAC sequence. ++ ++ """ ++ self.process_rawq() ++ return self.read_very_lazy() ++ ++ def read_very_lazy(self): ++ """Return any data available in the cooked queue (very lazy). ++ ++ Raise EOFError if connection closed and no data available. ++ Return b'' if no cooked data available otherwise. Don't block. ++ ++ """ ++ buf = self.cookedq ++ self.cookedq = b'' ++ if not buf and self.eof and not self.rawq: ++ raise EOFError('telnet connection closed') ++ return buf ++ ++ def read_sb_data(self): ++ """Return any data available in the SB ... SE queue. ++ ++ Return b'' if no SB ... SE available. Should only be called ++ after seeing a SB or SE command. When a new SB command is ++ found, old unread SB data will be discarded. Don't block. ++ ++ """ ++ buf = self.sbdataq ++ self.sbdataq = b'' ++ return buf ++ ++ def set_option_negotiation_callback(self, callback): ++ """Provide a callback function called after each receipt of a telnet option.""" ++ self.option_callback = callback ++ ++ def process_rawq(self): ++ """Transfer from raw queue to cooked queue. ++ ++ Set self.eof when connection is closed. Don't block unless in ++ the midst of an IAC sequence. ++ ++ """ ++ buf = [b'', b''] ++ try: ++ while self.rawq: ++ c = self.rawq_getchar() ++ if not self.iacseq: ++ if c == theNULL: ++ continue ++ if c == b"\021": ++ continue ++ if c != IAC: ++ buf[self.sb] = buf[self.sb] + c ++ continue ++ else: ++ self.iacseq += c ++ elif len(self.iacseq) == 1: ++ # 'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]' ++ if c in (DO, DONT, WILL, WONT): ++ self.iacseq += c ++ continue ++ ++ self.iacseq = b'' ++ if c == IAC: ++ buf[self.sb] = buf[self.sb] + c ++ else: ++ if c == SB: # SB ... SE start. ++ self.sb = 1 ++ self.sbdataq = b'' ++ elif c == SE: ++ self.sb = 0 ++ self.sbdataq = self.sbdataq + buf[1] ++ buf[1] = b'' ++ if self.option_callback: ++ # Callback is supposed to look into ++ # the sbdataq ++ self.option_callback(self.sock, c, NOOPT) ++ else: ++ # We can't offer automatic processing of ++ # suboptions. Alas, we should not get any ++ # unless we did a WILL/DO before. ++ self.msg('IAC %d not recognized' % ord(c)) ++ elif len(self.iacseq) == 2: ++ cmd = self.iacseq[1:2] ++ self.iacseq = b'' ++ opt = c ++ if cmd in (DO, DONT): ++ self.msg('IAC %s %d', ++ cmd == DO and 'DO' or 'DONT', ord(opt)) ++ if self.option_callback: ++ self.option_callback(self.sock, cmd, opt) ++ else: ++ self.sock.sendall(IAC + WONT + opt) ++ elif cmd in (WILL, WONT): ++ self.msg('IAC %s %d', ++ cmd == WILL and 'WILL' or 'WONT', ord(opt)) ++ if self.option_callback: ++ self.option_callback(self.sock, cmd, opt) ++ else: ++ self.sock.sendall(IAC + DONT + opt) ++ except EOFError: # raised by self.rawq_getchar() ++ self.iacseq = b'' # Reset on EOF ++ self.sb = 0 ++ pass ++ self.cookedq = self.cookedq + buf[0] ++ self.sbdataq = self.sbdataq + buf[1] ++ ++ def rawq_getchar(self): ++ """Get next char from raw queue. ++ ++ Block if no data is immediately available. Raise EOFError ++ when connection is closed. ++ ++ """ ++ if not self.rawq: ++ self.fill_rawq() ++ if self.eof: ++ raise EOFError ++ c = self.rawq[self.irawq:self.irawq+1] ++ self.irawq = self.irawq + 1 ++ if self.irawq >= len(self.rawq): ++ self.rawq = b'' ++ self.irawq = 0 ++ return c ++ ++ def fill_rawq(self): ++ """Fill raw queue from exactly one recv() system call. ++ ++ Block if no data is immediately available. Set self.eof when ++ connection is closed. ++ ++ """ ++ if self.irawq >= len(self.rawq): ++ self.rawq = b'' ++ self.irawq = 0 ++ # The buffer size should be fairly small so as to avoid quadratic ++ # behavior in process_rawq() above ++ buf = self.sock.recv(50) ++ self.msg("recv %r", buf) ++ self.eof = (not buf) ++ self.rawq = self.rawq + buf ++ ++ def sock_avail(self): ++ """Test whether data is available on the socket.""" ++ with _TelnetSelector() as selector: ++ selector.register(self, selectors.EVENT_READ) ++ return bool(selector.select(0)) ++ ++ def interact(self): ++ """Interaction function, emulates a very dumb telnet client.""" ++ if sys.platform == "win32": ++ self.mt_interact() ++ return ++ with _TelnetSelector() as selector: ++ selector.register(self, selectors.EVENT_READ) ++ selector.register(sys.stdin, selectors.EVENT_READ) ++ ++ while True: ++ for key, events in selector.select(): ++ if key.fileobj is self: ++ try: ++ text = self.read_eager() ++ except EOFError: ++ print('*** Connection closed by remote host ***') ++ return ++ if text: ++ sys.stdout.write(text.decode('ascii')) ++ sys.stdout.flush() ++ elif key.fileobj is sys.stdin: ++ line = sys.stdin.readline().encode('ascii') ++ if not line: ++ return ++ self.write(line) ++ ++ def mt_interact(self): ++ """Multithreaded version of interact().""" ++ import _thread ++ _thread.start_new_thread(self.listener, ()) ++ while 1: ++ line = sys.stdin.readline() ++ if not line: ++ break ++ self.write(line.encode('ascii')) ++ ++ def listener(self): ++ """Helper for mt_interact() -- this executes in the other thread.""" ++ while 1: ++ try: ++ data = self.read_eager() ++ except EOFError: ++ print('*** Connection closed by remote host ***') ++ return ++ if data: ++ sys.stdout.write(data.decode('ascii')) ++ else: ++ sys.stdout.flush() ++ ++ def expect(self, list, timeout=None): ++ """Read until one from a list of a regular expressions matches. ++ ++ The first argument is a list of regular expressions, either ++ compiled (re.Pattern instances) or uncompiled (strings). ++ The optional second argument is a timeout, in seconds; default ++ is no timeout. ++ ++ Return a tuple of three items: the index in the list of the ++ first regular expression that matches; the re.Match object ++ returned; and the text read up till and including the match. ++ ++ If EOF is read and no text was read, raise EOFError. ++ Otherwise, when nothing matches, return (-1, None, text) where ++ text is the text received so far (may be the empty string if a ++ timeout happened). ++ ++ If a regular expression ends with a greedy match (e.g. '.*') ++ or if more than one expression can match the same input, the ++ results are undeterministic, and may depend on the I/O timing. ++ ++ """ ++ re = None ++ list = list[:] ++ indices = range(len(list)) ++ for i in indices: ++ if not hasattr(list[i], "search"): ++ if not re: import re ++ list[i] = re.compile(list[i]) ++ if timeout is not None: ++ deadline = _time() + timeout ++ with _TelnetSelector() as selector: ++ selector.register(self, selectors.EVENT_READ) ++ while not self.eof: ++ self.process_rawq() ++ for i in indices: ++ m = list[i].search(self.cookedq) ++ if m: ++ e = m.end() ++ text = self.cookedq[:e] ++ self.cookedq = self.cookedq[e:] ++ return (i, m, text) ++ if timeout is not None: ++ ready = selector.select(timeout) ++ timeout = deadline - _time() ++ if not ready: ++ if timeout < 0: ++ break ++ else: ++ continue ++ self.fill_rawq() ++ text = self.read_very_lazy() ++ if not text and self.eof: ++ raise EOFError ++ return (-1, None, text) ++ ++ def __enter__(self): ++ return self ++ ++ def __exit__(self, type, value, traceback): ++ self.close() ++ ++ ++def test(): ++ """Test program for telnetlib. ++ ++ Usage: python telnetlib.py [-d] ... [host [port]] ++ ++ Default host is localhost; default port is 23. ++ ++ """ ++ debuglevel = 0 ++ while sys.argv[1:] and sys.argv[1] == '-d': ++ debuglevel = debuglevel+1 ++ del sys.argv[1] ++ host = 'localhost' ++ if sys.argv[1:]: ++ host = sys.argv[1] ++ port = 0 ++ if sys.argv[2:]: ++ portstr = sys.argv[2] ++ try: ++ port = int(portstr) ++ except ValueError: ++ port = socket.getservbyname(portstr, 'tcp') ++ with Telnet() as tn: ++ tn.set_debuglevel(debuglevel) ++ tn.open(host, port, timeout=0.5) ++ tn.interact() ++ ++if __name__ == '__main__': ++ test() +diff --git a/scripts/python/module/setup.py.in b/scripts/python/module/setup.py.in +index 18f956d20c..c6276fd2f2 100644 +--- a/scripts/python/module/setup.py.in ++++ b/scripts/python/module/setup.py.in +@@ -30,7 +30,11 @@ 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 ++ # NOTE: telnetlib.py is part of Python core for tested 2.x and 3.x versions, ++ # not something 'pip' can download as a package. It is deprecated since 3.11 ++ # and removed since 3.13, so as a hotfix, NUT provides a copy nut_telnetlib.py ++ # (taken from Python 3.10 - the latest one without a deprecation warning). ++ # install_requires = ['telnetlib'], + keywords = ['pypi', 'cicd', 'python', 'nut', 'Network UPS Tools'], + classifiers = [ + "Development Status :: 5 - Production/Stable", diff --git a/package/nut/0002-configure-ac-typo-in-path-to-test-with-nut_telnetlib-Python-module.patch b/package/nut/0002-configure-ac-typo-in-path-to-test-with-nut_telnetlib-Python-module.patch new file mode 100644 index 0000000000..14fa74902b --- /dev/null +++ b/package/nut/0002-configure-ac-typo-in-path-to-test-with-nut_telnetlib-Python-module.patch @@ -0,0 +1,37 @@ +From d0a234ac4fe9304c4178256584712e4359541060 Mon Sep 17 00:00:00 2001 +From: Jim Klimov +Date: Wed, 3 Jul 2024 09:51:07 +0200 +Subject: [PATCH] configure.ac: typo in path to test with nut_telnetlib Python + module [#2183] + +Signed-off-by: Jim Klimov + +Upstream: https://github.com/networkupstools/nut/commit/d0a234ac4fe9304c4178256584712e4359541060 + +Signed-off-by: Bernd Kuhls +--- + configure.ac | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/configure.ac b/configure.ac +index 935e73d368..e542baacd2 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -2474,7 +2474,7 @@ if test x"${nut_with_pynut}" != xno \ + dnl this line essentially checks for presence of + dnl a usable interpreter implementation compatible + dnl with Python 3.x syntax. +- if (cd script/python/module && ${PYTHON3} -c "import nut_telnetlib as telnetlib") \ ++ if (cd scripts/python/module && ${PYTHON3} -c "import nut_telnetlib as telnetlib") \ + ; then + nut_with_pynut_py3="yes" + fi +@@ -2491,7 +2491,7 @@ if test x"${nut_with_pynut}" != xno \ + nut_with_pynut_py="yes" + else + dnl See comments above +- if (cd script/python/module && ${PYTHON} -c "import nut_telnetlib as telnetlib") \ ++ if (cd scripts/python/module && ${PYTHON} -c "import nut_telnetlib as telnetlib") \ + ; then + nut_with_pynut_py="yes" + fi diff --git a/package/nut/0003-configure.ac-be-sure-to-probe-nut_telnetlib-module-from-source-dir.patch b/package/nut/0003-configure.ac-be-sure-to-probe-nut_telnetlib-module-from-source-dir.patch new file mode 100644 index 0000000000..8410bceecc --- /dev/null +++ b/package/nut/0003-configure.ac-be-sure-to-probe-nut_telnetlib-module-from-source-dir.patch @@ -0,0 +1,39 @@ +From 2898a45a83d7f9af62fe0844fcb5358337d905d3 Mon Sep 17 00:00:00 2001 +From: Jim Klimov +Date: Wed, 3 Jul 2024 10:22:24 +0200 +Subject: [PATCH] configure.ac: be sure to probe nut_telnetlib module from + source dir [#2183] + +Avoid confusion for out-of-tree builds + +Signed-off-by: Jim Klimov + +Upstream: https://github.com/networkupstools/nut/commit/2898a45a83d7f9af62fe0844fcb5358337d905d3 + +Signed-off-by: Bernd Kuhls +--- + configure.ac | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/configure.ac b/configure.ac +index 5865ad5fe3..9d622c34fc 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -2474,7 +2474,7 @@ if test x"${nut_with_pynut}" != xno \ + dnl this line essentially checks for presence of + dnl a usable interpreter implementation compatible + dnl with Python 3.x syntax. +- if (cd scripts/python/module && ${PYTHON3} -c "import nut_telnetlib as telnetlib") \ ++ if (cd "${srcdir}"/scripts/python/module && ${PYTHON3} -c "import nut_telnetlib as telnetlib") \ + ; then + nut_with_pynut_py3="yes" + fi +@@ -2491,7 +2491,7 @@ if test x"${nut_with_pynut}" != xno \ + nut_with_pynut_py="yes" + else + dnl See comments above +- if (cd scripts/python/module && ${PYTHON} -c "import nut_telnetlib as telnetlib") \ ++ if (cd "${srcdir}"/scripts/python/module && ${PYTHON} -c "import nut_telnetlib as telnetlib") \ + ; then + nut_with_pynut_py="yes" + fi diff --git a/package/nut/nut.mk b/package/nut/nut.mk index 93522d017e..55937f3818 100644 --- a/package/nut/nut.mk +++ b/package/nut/nut.mk @@ -11,6 +11,10 @@ NUT_LICENSE_FILES = COPYING LICENSE-GPL2 LICENSE-GPL3 NUT_SELINUX_MODULES = apache nut NUT_INSTALL_STAGING = YES NUT_DEPENDENCIES = host-pkgconf +# 0001-Provide-a-private-copy-of-telnetlib-for-PyNUTClient.patch +# 0002-configure-ac-typo-in-path-to-test-with-nut_telnetlib-Python-module.patch +# 0003-configure.ac-be-sure-to-probe-nut_telnetlib-module-from-source-dir.patch +NUT_AUTORECONF = YES # Put the PID files in a read-write place (/var/run is a tmpfs) # since the default location (/var/state/ups) maybe readonly.