diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..319cddc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "nut2" +version = "2.1.2a1" +description = "A Python abstraction class to access NUT servers." +readme = "README.rst" +requires-python = ">=3" +license = "GPL-3.0-or-later" +license-files = ["LICENSE"] +keywords = ['nut', 'ups'] +authors = [ + {email = "python@rshipp.com", name = "Ryan Shipp"} +] +classifiers = [ + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Power (UPS)", + "Topic :: System :: Systems Administration", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 9a8fb80..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -from setuptools import setup - -from nut2 import __version__ - -README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() - -# allow setup.py to be run from any path -os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) - -setup( - name='nut2', - version=__version__, - py_modules=['nut2'], - include_package_data=True, - install_requires=[], - license='GPL3', - description='A Python abstraction class to access NUT servers.', - long_description=README, - url='https://github.com/rshipp/python-nut2', - author='Ryan Shipp', - author_email='python@rshipp.com', - classifiers=[ - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Libraries', - 'Topic :: System :: Power (UPS)', - 'Topic :: System :: Systems Administration', - ], -) diff --git a/nut2.py b/src/nut2/__init__.py similarity index 64% rename from nut2.py rename to src/nut2/__init__.py index 99ea8a0..6e54522 100644 --- a/nut2.py +++ b/src/nut2/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +# SPDX-License-Identifier: GPL-3.0-or-later """A Python module for dealing with NUT (Network UPS Tools) servers. * PyNUTError: Base class for custom exceptions. @@ -22,34 +22,41 @@ along with this program. If not, see . """ -import telnetlib import logging +from socket import create_connection - -__version__ = '2.1.1' +__version__ = '2.1.2a1' __all__ = ['PyNUTError', 'PyNUTClient'] -logging.basicConfig(level=logging.WARNING, format="[%(levelname)s] %(message)s") +logging.basicConfig(level=logging.WARNING, + format="[%(levelname)s] %(message)s") class PyNUTError(Exception): """Base class for custom exceptions.""" + class PyNUTClient(object): """Access NUT (Network UPS Tools) servers.""" - def __init__(self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=5, connect=True): + def __init__(self, + host="localhost", + port=3493, + login=None, + password=None, + debug=False, + timeout=5, + connect=True): """Class initialization method. - host : Host to connect (defaults to 127.0.0.1). + host : Host to connect (defaults to localhost). port : Port where NUT listens for connections (defaults to 3493). login : Login used to connect to NUT server (defaults to None for no authentication). password : Password used when using authentication (defaults to None). debug : Boolean, put class in debug mode (prints everything on console, defaults to False). - timeout : Timeout used to wait for network response (defaults - to 5 seconds). + timeout : Socket timeout (defaults to 5 seconds). """ if debug: # Print DEBUG messages to the console. @@ -65,6 +72,7 @@ def __init__(self, host="127.0.0.1", port=3493, login=None, password=None, debug self._password = password self._timeout = timeout self._srv_handler = None + self._buf = bytearray() if connect: self._connect() @@ -73,7 +81,7 @@ def __del__(self): # Try to disconnect cleanly when class is deleted. if self._srv_handler: try: - self._srv_handler.write(b"LOGOUT\n") + self._srv_handler.sendall(b"LOGOUT\n") self._srv_handler.close() except (telnetlib.socket.error, AttributeError): # The socket is already disconnected. @@ -85,6 +93,35 @@ def __enter__(self): def __exit__(self, exc_t, exc_v, trace): self.__del__() + def _read_until(self, match): + """Read until a given string is encountered. + + If no match is found after timeout, return whatever + is available instead, possibly the empty string. + Raise EOFError if the connection is closed. + """ + ret = '' + try: + while True: + rb = self._srv_handler.recv(2048) + #logging.debug("RECV: %r", rb) + if not rb: + raise EOFError('connection closed') + + self._buf.extend(rb) + idx = self._buf.find(match) + if idx >= 0: + end = idx + len(match) + ret = bytes(self._buf[0:end]) + del (self._buf[0:end]) + break + + except TimeoutError: + ret = bytes(self._buf) + self._buf.clear() + + return ret + def _connect(self): """Connects to the defined server. @@ -94,18 +131,20 @@ def _connect(self): logging.debug("Connecting to host") try: - self._srv_handler = telnetlib.Telnet(self._host, self._port, - timeout=self._timeout) + self._srv_handler = create_connection((self._host, self._port), + timeout=self._timeout) if self._login is not None: - self._srv_handler.write(b"USERNAME %s\n" % self._login.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"USERNAME %s\n" % + self._login.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if not result == "OK\n": raise PyNUTError(result.replace("\n", "")) if self._password is not None: - self._srv_handler.write(b"PASSWORD %s\n" % self._password.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"PASSWORD %s\n" % + self._password.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if not result == "OK\n": raise PyNUTError(result.replace("\n", "")) except telnetlib.socket.error: @@ -115,8 +154,8 @@ def description(self, ups): """Returns the description for a given UPS.""" logging.debug("description called...") - self._srv_handler.write(b"GET UPSDESC %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"GET UPSDESC %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') try: return result.split('"')[1].strip() except IndexError: @@ -130,13 +169,12 @@ def list_ups(self): """ logging.debug("list_ups from server") - self._srv_handler.write(b"LIST UPS\n") - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST UPS\n") + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST UPS\n": raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST UPS\n", - self._timeout).decode('utf-8') + result = self._read_until(b"END LIST UPS\n").decode('utf-8') ups_dict = {} for line in result.split("\n"): @@ -154,13 +192,13 @@ def list_vars(self, ups): """ logging.debug("list_vars called...") - self._srv_handler.write(b"LIST VAR %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST VAR %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST VAR %s\n" % ups: raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST VAR %s\n" % ups.encode('utf-8'), - self._timeout).decode('utf-8') + result = self._read_until(b"END LIST VAR %s\n" % + ups.encode('utf-8')).decode('utf-8') offset = len("VAR %s " % ups) end_offset = 0 - (len("END LIST VAR %s\n" % ups) + 1) @@ -179,13 +217,13 @@ def list_commands(self, ups): """ logging.debug("list_commands called...") - self._srv_handler.write(b"LIST CMD %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST CMD %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST CMD %s\n" % ups: raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST CMD %s\n" % ups.encode('utf-8'), - self._timeout).decode('utf-8') + result = self._read_until(b"END LIST CMD %s\n" % + ups.encode('utf-8')).decode('utf-8') offset = len("CMD %s " % ups) end_offset = 0 - (len("END LIST CMD %s\n" % ups) + 1) @@ -195,8 +233,10 @@ def list_commands(self, ups): # For each var we try to get the available description try: - self._srv_handler.write(b"GET CMDDESC %s %s\n" % (ups.encode('utf-8'), command.encode('utf-8'))) - temp = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall( + b"GET CMDDESC %s %s\n" % + (ups.encode('utf-8'), command.encode('utf-8'))) + temp = self._read_until(b"\n").decode('utf-8') if temp.startswith("CMDDESC"): desc_offset = len("CMDDESC %s %s " % (ups, command)) commands[command] = temp[desc_offset:-1].split('"')[1] @@ -219,15 +259,15 @@ def list_clients(self, ups=None): raise PyNUTError("%s is not a valid UPS" % ups) if ups: - self._srv_handler.write(b"LIST CLIENTS %s\n" % ups.encode('utf-8')) + self._srv_handler.sendall(b"LIST CLIENTS %s\n" % + ups.encode('utf-8')) else: - self._srv_handler.write(b"LIST CLIENTS\n") - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST CLIENTS\n") + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST CLIENTS\n": raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST CLIENTS\n", - self._timeout).decode('utf-8') + result = self._read_until(b"END LIST CLIENTS\n").decode('utf-8') clients = {} for line in result.split("\n"): @@ -247,13 +287,13 @@ def list_rw_vars(self, ups): """ logging.debug("list_vars from '%s'...", ups) - self._srv_handler.write(b"LIST RW %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST RW %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST RW %s\n" % ups: raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST RW %s\n" % ups.encode('utf-8'), - self._timeout).decode('utf-8') + result = self._read_until(b"END LIST RW %s\n" % + ups.encode('utf-8')).decode('utf-8') offset = len("VAR %s" % ups) end_offset = 0 - (len("END LIST RW %s\n" % ups) + 1) @@ -271,19 +311,23 @@ def list_enum(self, ups, var): """ logging.debug("list_enum from '%s'...", ups) - self._srv_handler.write(b"LIST ENUM %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST ENUM %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST ENUM %s %s\n" % (ups, var): raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST ENUM %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8')), - self._timeout).decode('utf-8') + result = self._read_until( + b"END LIST ENUM %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))).decode('utf-8') offset = len("ENUM %s %s" % (ups, var)) end_offset = 0 - (len("END LIST ENUM %s %s\n" % (ups, var)) + 1) try: - return [ c[offset:].split('"')[1].strip() - for c in result[:end_offset].split("\n") ] + return [ + c[offset:].split('"')[1].strip() + for c in result[:end_offset].split("\n") + ] except IndexError: raise PyNUTError(result.replace("\n", "")) @@ -294,19 +338,23 @@ def list_range(self, ups, var): """ logging.debug("list_range from '%s'...", ups) - self._srv_handler.write(b"LIST RANGE %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"LIST RANGE %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') if result != "BEGIN LIST RANGE %s %s\n" % (ups, var): raise PyNUTError(result.replace("\n", "")) - result = self._srv_handler.read_until(b"END LIST RANGE %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8')), - self._timeout).decode('utf-8') + result = self._read_until( + b"END LIST RANGE %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))).decode('utf-8') offset = len("RANGE %s %s" % (ups, var)) end_offset = 0 - (len("END LIST RANGE %s %s\n" % (ups, var)) + 1) try: - return [ c[offset:].split('"')[1].strip() - for c in result[:end_offset].split("\n") ] + return [ + c[offset:].split('"')[1].strip() + for c in result[:end_offset].split("\n") + ] except IndexError: raise PyNUTError(result.replace("\n", "")) @@ -318,8 +366,10 @@ def set_var(self, ups, var, value): """ logging.debug("set_var '%s' from '%s' to '%s'", var, ups, value) - self._srv_handler.write(b"SET VAR %s %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'), value.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall( + b"SET VAR %s %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'), value.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') if result != "OK\n": raise PyNUTError(result.replace("\n", "")) @@ -327,8 +377,9 @@ def get_var(self, ups, var): """Get the value of a variable.""" logging.debug("get_var called...") - self._srv_handler.write(b"GET VAR %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"GET VAR %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') try: # result = 'VAR %s %s "%s"\n' % (ups, var, value) return result.split('"')[1].strip() @@ -344,8 +395,9 @@ def var_description(self, ups, var): """Get a variable's description.""" logging.debug("var_description called...") - self._srv_handler.write(b"GET DESC %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"GET DESC %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') try: # result = 'DESC %s %s "%s"\n' % (ups, var, description) return result.split('"')[1].strip() @@ -356,14 +408,15 @@ def var_type(self, ups, var): """Get a variable's type.""" logging.debug("var_type called...") - self._srv_handler.write(b"GET TYPE %s %s\n" % (ups.encode('utf-8'), var.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"GET TYPE %s %s\n" % + (ups.encode('utf-8'), var.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') try: # result = 'TYPE %s %s %s\n' % (ups, var, type) type_ = ' '.join(result.split(' ')[3:]).strip() # Ensure the response was valid. - assert(len(type_) > 0) - assert(result.startswith("TYPE")) + assert (len(type_) > 0) + assert (result.startswith("TYPE")) return type_ except AssertionError: raise PyNUTError(result.replace("\n", "")) @@ -372,8 +425,10 @@ def command_description(self, ups, command): """Get a command's description.""" logging.debug("command_description called...") - self._srv_handler.write(b"GET CMDDESC %s %s\n" % (ups.encode('utf-8'), command.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall( + b"GET CMDDESC %s %s\n" % + (ups.encode('utf-8'), command.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') try: # result = 'CMDDESC %s %s "%s"' % (ups, command, description) return result.split('"')[1].strip() @@ -384,8 +439,10 @@ def run_command(self, ups, command): """Send a command to the specified UPS.""" logging.debug("run_command called...") - self._srv_handler.write(b"INSTCMD %s %s\n" % (ups.encode('utf-8'), command.encode('utf-8'))) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall( + b"INSTCMD %s %s\n" % + (ups.encode('utf-8'), command.encode('utf-8'))) + result = self._read_until(b"\n").decode('utf-8') if result != "OK\n": raise PyNUTError(result.replace("\n", "")) @@ -393,14 +450,14 @@ def fsd(self, ups): """Send MASTER and FSD commands.""" logging.debug("MASTER called...") - self._srv_handler.write(b"MASTER %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"MASTER %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if result != "OK MASTER-GRANTED\n": raise PyNUTError(("Master level function are not available", "")) logging.debug("FSD called...") - self._srv_handler.write(b"FSD %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"FSD %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') if result != "OK FSD-SET\n": raise PyNUTError(result.replace("\n", "")) @@ -410,8 +467,8 @@ def num_logins(self, ups): """ logging.debug("num_logins called on '%s'...", ups) - self._srv_handler.write(b"GET NUMLOGINS %s\n" % ups.encode('utf-8')) - result = self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"GET NUMLOGINS %s\n" % ups.encode('utf-8')) + result = self._read_until(b"\n").decode('utf-8') try: # result = "NUMLOGINS %s %s\n" % (ups, int(numlogins)) return int(result.split(' ')[2].strip()) @@ -422,12 +479,12 @@ def help(self): """Send HELP command.""" logging.debug("HELP called...") - self._srv_handler.write(b"HELP\n") - return self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"HELP\n") + return self._read_until(b"\n").decode('utf-8') def ver(self): """Send VER command.""" logging.debug("VER called...") - self._srv_handler.write(b"VER\n") - return self._srv_handler.read_until(b"\n", self._timeout).decode('utf-8') + self._srv_handler.sendall(b"VER\n") + return self._read_until(b"\n").decode('utf-8')