Init
This commit is contained in:
commit
67ad0252b1
176
.gitignore
vendored
Normal file
176
.gitignore
vendored
Normal file
@ -0,0 +1,176 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
config.env
|
466
PyNUT.py
Normal file
466
PyNUT.py
Normal file
@ -0,0 +1,466 @@
|
||||
#!@PYTHON@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2008 David Goncalves <david@lestat.st>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# 2008-01-14 David Goncalves
|
||||
# PyNUT is an abstraction class to access NUT (Network UPS Tools) server.
|
||||
#
|
||||
# 2008-06-09 David Goncalves
|
||||
# Added 'GetRWVars' and 'SetRWVar' commands.
|
||||
#
|
||||
# 2009-02-19 David Goncalves
|
||||
# Changed class PyNUT to PyNUTClient
|
||||
#
|
||||
# 2010-07-23 David Goncalves - Version 1.2
|
||||
# Changed GetRWVars function that fails is the UPS is not
|
||||
# providing such vars.
|
||||
#
|
||||
# 2011-07-05 René Martín Rodríguez <rmrodri@ull.es> - Version 1.2.1
|
||||
# Added support for FSD, HELP and VER commands
|
||||
#
|
||||
# 2012-02-07 René Martín Rodríguez <rmrodri@ull.es> - Version 1.2.2
|
||||
# Added support for LIST CLIENTS command
|
||||
#
|
||||
# 2014-06-03 george2 - Version 1.3.0
|
||||
# Added custom exception class, fixed minor bug, added Python 3 support.
|
||||
#
|
||||
# 2021-09-27 Jim Klimov <jimklimov+nut@gmail.com> - Version 1.4.0
|
||||
# Revise strings used to be byte sequences as required by telnetlib
|
||||
# in Python 3.9, by spelling out b"STR" or str.encode('ascii');
|
||||
# the change was also tested to work with Python 2.7, 3.4, 3.5 and
|
||||
# 3.7 (to the extent of accompanying test_nutclient.py at least).
|
||||
#
|
||||
# 2022-08-12 Jim Klimov <jimklimov+nut@gmail.com> - Version 1.5.0
|
||||
# Fix ListClients() method to actually work with current NUT protocol
|
||||
# Added DeviceLogin() method
|
||||
# Added GetUPSNames() method
|
||||
# Fixed raised PyNUTError() exceptions to carry a Python string
|
||||
# (suitable for Python2 and Python3), not byte array from protocol,
|
||||
# so exception catchers can process them naturally (see test script).
|
||||
#
|
||||
# 2023-01-18 Jim Klimov <jimklimov+nut@gmail.com> - Version 1.6.0
|
||||
# Added CheckUPSAvailable() method originally by Michal Hlavinka
|
||||
# from 2013-01-07 RedHat/Fedora packaging
|
||||
|
||||
import telnetlib
|
||||
|
||||
class PyNUTError( Exception ) :
|
||||
""" Base class for custom exceptions """
|
||||
|
||||
|
||||
class PyNUTClient :
|
||||
""" Abstraction class to access NUT (Network UPS Tools) server """
|
||||
|
||||
__debug = None # Set class to debug mode (prints everything useful for debuging...)
|
||||
__host = None
|
||||
__port = None
|
||||
__login = None
|
||||
__password = None
|
||||
__timeout = None
|
||||
__srv_handler = None
|
||||
|
||||
__version = "1.6.0"
|
||||
__release = "2023-01-18"
|
||||
|
||||
|
||||
def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=5 ) :
|
||||
""" Class initialization method
|
||||
|
||||
host : Host to connect (default to localhost)
|
||||
port : Port where NUT listens for connections (default to 3493)
|
||||
login : Login used to connect to NUT server (default to None for no authentication)
|
||||
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
|
||||
"""
|
||||
self.__debug = debug
|
||||
|
||||
if self.__debug :
|
||||
print( "[DEBUG] Class initialization..." )
|
||||
print( "[DEBUG] -> Host = %s (port %s)" % ( host, port ) )
|
||||
print( "[DEBUG] -> Login = '%s' / '%s'" % ( login, password ) )
|
||||
|
||||
self.__host = host
|
||||
self.__port = port
|
||||
self.__login = login
|
||||
self.__password = password
|
||||
self.__timeout = 5
|
||||
|
||||
self.__connect()
|
||||
|
||||
# Try to disconnect cleanly when class is deleted ;)
|
||||
def __del__( self ) :
|
||||
""" Class destructor method """
|
||||
try :
|
||||
self.__srv_handler.write( b"LOGOUT\n" )
|
||||
except :
|
||||
pass
|
||||
|
||||
def __connect( self ) :
|
||||
""" Connects to the defined server
|
||||
|
||||
If login/pass was specified, the class tries to authenticate. An error is raised
|
||||
if something goes wrong.
|
||||
"""
|
||||
if self.__debug :
|
||||
print( "[DEBUG] Connecting to host" )
|
||||
|
||||
self.__srv_handler = telnetlib.Telnet( 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 )
|
||||
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 )
|
||||
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 )
|
||||
if result[:2] != b"OK" :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
else:
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
def GetUPSList( self ) :
|
||||
""" Returns the list of available UPS from the NUT server
|
||||
|
||||
The result is a dictionary containing 'key->val' pairs of 'UPSName' and 'UPS Description'
|
||||
|
||||
Note that fields here are byte sequences (not locale-aware strings)
|
||||
which is of little concern for Python2 but is important in Python3
|
||||
(e.g. when we use "str" type `ups` variables or check their "validity").
|
||||
"""
|
||||
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" :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
result = self.__srv_handler.read_until( b"END LIST UPS\n" )
|
||||
ups_list = {}
|
||||
|
||||
for line in result.split( b"\n" ) :
|
||||
if line[:3] == b"UPS" :
|
||||
ups, desc = line[4:-1].split( b'"' )
|
||||
ups_list[ ups.replace( b" ", b"" ) ] = desc
|
||||
|
||||
return( ups_list )
|
||||
|
||||
def GetUPSNames( self ) :
|
||||
""" Returns the list of available UPS names from the NUT server as strings
|
||||
|
||||
The result is a set of str objects (comparable with ups="somename" and
|
||||
useful as arguments to other methods). Helps work around Python2/Python3
|
||||
string API changes.
|
||||
"""
|
||||
if self.__debug :
|
||||
print( "[DEBUG] GetUPSNames from server" )
|
||||
|
||||
self_ups_list = []
|
||||
for b in self.GetUPSList():
|
||||
self_ups_list.append(b.decode('ascii'))
|
||||
|
||||
return self_ups_list
|
||||
|
||||
def GetUPSVars( self, ups="" ) :
|
||||
""" Get all available vars from the specified UPS
|
||||
|
||||
The result is a dictionary containing 'key->val' pairs of all
|
||||
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" )
|
||||
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') )
|
||||
offset = len( ("VAR %s " % ups ).encode('ascii') )
|
||||
end_offset = 0 - ( len( ("END LIST VAR %s\n" % ups).encode('ascii') ) + 1 )
|
||||
|
||||
for current in result[:end_offset].split( b"\n" ) :
|
||||
var = current[ offset: ].split( b'"' )[0].replace( b" ", b"" )
|
||||
data = current[ offset: ].split( b'"' )[1]
|
||||
ups_vars[ var ] = data
|
||||
|
||||
return( ups_vars )
|
||||
|
||||
def CheckUPSAvailable( self, ups="" ) :
|
||||
""" Check whether UPS is reachable
|
||||
|
||||
Just tries to contact UPS with a safe command.
|
||||
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" )
|
||||
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') )
|
||||
return True
|
||||
|
||||
def GetUPSCommands( self, ups="" ) :
|
||||
""" Get all available commands for the specified UPS
|
||||
|
||||
The result is a dict object with command name as key and a description
|
||||
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" )
|
||||
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') )
|
||||
offset = len( ("CMD %s " % ups).encode('ascii') )
|
||||
end_offset = 0 - ( len( ("END LIST CMD %s\n" % ups).encode('ascii') ) + 1 )
|
||||
|
||||
for current in result[:end_offset].split( b"\n" ) :
|
||||
var = current[ offset: ].split( b'"' )[0].replace( b" ", b"" )
|
||||
|
||||
# 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" )
|
||||
if temp[:7] != b"CMDDESC" :
|
||||
raise PyNUTError
|
||||
else :
|
||||
off = len( ("CMDDESC %s %s " % ( ups, var )).encode('ascii') )
|
||||
desc = temp[off:-1].split(b'"')[1]
|
||||
except :
|
||||
desc = var
|
||||
|
||||
ups_cmds[ var ] = desc
|
||||
|
||||
return( ups_cmds )
|
||||
|
||||
def GetRWVars( self, ups="" ) :
|
||||
""" Get a list of all writable vars from the selected UPS
|
||||
|
||||
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" )
|
||||
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') )
|
||||
offset = len( ("VAR %s" % ups).encode('ascii') )
|
||||
end_offset = 0 - ( len( ("END LIST RW %s\n" % ups).encode('ascii') ) + 1 )
|
||||
rw_vars = {}
|
||||
|
||||
try :
|
||||
for current in result[:end_offset].split( b"\n" ) :
|
||||
var = current[ offset: ].split( b'"' )[0].replace( b" ", b"" )
|
||||
data = current[ offset: ].split( b'"' )[1]
|
||||
rw_vars[ var ] = data
|
||||
|
||||
except :
|
||||
pass
|
||||
|
||||
return( rw_vars )
|
||||
|
||||
def SetRWVar( self, ups="", var="", value="" ):
|
||||
""" 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).
|
||||
"""
|
||||
|
||||
self.__srv_handler.write( ("SET VAR %s %s %s\n" % ( ups, var, value )).encode('ascii') )
|
||||
result = self.__srv_handler.read_until( b"\n" )
|
||||
if ( result == b"OK\n" ) :
|
||||
return( "OK" )
|
||||
else :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
def RunUPSCommand( self, ups="", command="" ) :
|
||||
""" Send a command to the specified UPS
|
||||
|
||||
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" )
|
||||
if ( result == b"OK\n" ) :
|
||||
return( "OK" )
|
||||
else :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
def DeviceLogin( self, ups="") :
|
||||
""" Establish a login session with a device (like upsmon does)
|
||||
|
||||
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
|
||||
of connection.
|
||||
"""
|
||||
|
||||
if self.__debug :
|
||||
print( "[DEBUG] DeviceLogin called..." )
|
||||
|
||||
if ups is None or (ups not in self.GetUPSNames()):
|
||||
if self.__debug :
|
||||
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" )
|
||||
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" )
|
||||
if ( result == b"OK\n" ) :
|
||||
return( "OK" )
|
||||
else :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
def FSD( self, ups="") :
|
||||
""" 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.write( ("PRIMARY %s\n" % ups).encode('ascii') )
|
||||
result = self.__srv_handler.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" )
|
||||
if ( result != b"OK MASTER-GRANTED\n" ) :
|
||||
if self.__debug :
|
||||
print( "[DEBUG] Primary level functions are not available" )
|
||||
raise PyNUTError( "ERR ACCESS-DENIED" )
|
||||
|
||||
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" )
|
||||
if ( result == b"OK FSD-SET\n" ) :
|
||||
return( "OK" )
|
||||
else :
|
||||
raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') )
|
||||
|
||||
def help(self) :
|
||||
""" Send HELP command
|
||||
"""
|
||||
|
||||
if self.__debug :
|
||||
print( "[DEBUG] HELP called..." )
|
||||
|
||||
self.__srv_handler.write( b"HELP\n" )
|
||||
return self.__srv_handler.read_until( b"\n" )
|
||||
|
||||
def ver(self) :
|
||||
""" Send VER command
|
||||
"""
|
||||
|
||||
if self.__debug :
|
||||
print( "[DEBUG] VER called..." )
|
||||
|
||||
self.__srv_handler.write( b"VER\n" )
|
||||
return self.__srv_handler.read_until( b"\n" )
|
||||
|
||||
def ListClients( self, ups = None ) :
|
||||
""" Returns the list of connected clients from the NUT server
|
||||
|
||||
The result is a dictionary containing 'key->val' pairs of 'UPSName' and a list of clients
|
||||
"""
|
||||
if self.__debug :
|
||||
print( "[DEBUG] ListClients from server: %s" % ups )
|
||||
|
||||
# If (!ups) we use this list below to recurse:
|
||||
self_ups_list = self.GetUPSNames()
|
||||
if ups and (ups not in self_ups_list):
|
||||
if self.__debug :
|
||||
print( "[DEBUG] ListClients: %s is not a valid UPS" % ups )
|
||||
raise PyNUTError( "ERR UNKNOWN-UPS" )
|
||||
|
||||
if ups:
|
||||
self.__srv_handler.write( ("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" )
|
||||
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
|
||||
if self.__debug :
|
||||
print( "[DEBUG] Recurse ListClients() because it did not specify one UPS to query" )
|
||||
ups_list = {}
|
||||
for ups in self_ups_list :
|
||||
# Update "ups_list" dict with contents of recursive call return
|
||||
ups_list.update(self.ListClients(ups))
|
||||
return( ups_list )
|
||||
|
||||
# had a seemingly valid arg, but no success:
|
||||
if self.__debug :
|
||||
print( "[DEBUG] ListClients from server got unexpected result: %s" % result )
|
||||
|
||||
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') )
|
||||
else:
|
||||
# Should not get here with current NUT:
|
||||
result = self.__srv_handler.read_until( b"END LIST CLIENT\n" )
|
||||
ups_list = {}
|
||||
|
||||
for line in result.split( b"\n" ):
|
||||
###print( "[DEBUG] ListClients line: '%s'" % line )
|
||||
if line[:6] == b"CLIENT" :
|
||||
ups, host = line[7:].split(b' ')
|
||||
ups.replace(b' ', b'')
|
||||
host.replace(b' ', b'')
|
||||
if not ups in ups_list:
|
||||
ups_list[ups] = []
|
||||
ups_list[ups].append(host)
|
||||
|
||||
return( ups_list )
|
90
log_nut_ups.py
Executable file
90
log_nut_ups.py
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from dataclasses import dataclass, field, fields, asdict
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
from psycopg.sql import SQL, Identifier, Placeholder
|
||||
|
||||
import PyNUT
|
||||
import psycopg
|
||||
|
||||
def utcnow():
|
||||
return datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
|
||||
@dataclass
|
||||
class UPS():
|
||||
ups_id: int
|
||||
timestamp: datetime.datetime = field(default_factory=utcnow)
|
||||
battery_charge: float | None = None
|
||||
battery_runtime: float | None = None
|
||||
battery_temperature: float | None = None
|
||||
battery_voltage: float | None = None
|
||||
input_voltage: float | None = None
|
||||
output_current: float | None = None
|
||||
output_voltage: float | None = None
|
||||
output_frequency: float | None = None
|
||||
ups_load: float | None = None
|
||||
ups_status: str | None = None
|
||||
|
||||
|
||||
class LogNutUps:
|
||||
def __init__(self):
|
||||
self.conninfo = os.environ['DB_CONNINFO']
|
||||
|
||||
def connect(self) -> psycopg.Connection:
|
||||
return psycopg.connect(conninfo=self.conninfo)
|
||||
|
||||
def get_ups(self, ups="ups") -> UPS:
|
||||
nut_client = PyNUT.PyNUTClient(host='192.168.122.10')
|
||||
result = nut_client.GetUPSVars(ups=ups)
|
||||
return UPS(
|
||||
ups_id=1,
|
||||
battery_charge=float(result.get(b'battery.charge', 0.0)),
|
||||
battery_runtime=float(result.get(b'battery.runtime', 0.0)),
|
||||
battery_temperature=float(result.get(b'battery.temperature', 0.0)),
|
||||
battery_voltage=float(result.get(b'battery.voltage', 0.0)),
|
||||
input_voltage=float(result.get(b'input.voltage', 0.0)),
|
||||
output_current=float(result.get(b'output.current', 0.0)),
|
||||
output_frequency=float(result.get(b'output.frequency', 0.0)),
|
||||
output_voltage=float(result.get(b'output.voltage', 0.0)),
|
||||
ups_load=float(result.get(b'ups.load', 0.0)),
|
||||
ups_status=result.get(b'ups.status', b'').decode('utf-8', errors='ignore')
|
||||
)
|
||||
def insert_measurement(self, ups: UPS):
|
||||
with self.connect() as conn, conn.cursor() as cursor:
|
||||
columns = []
|
||||
placeholders = []
|
||||
data = asdict(ups)
|
||||
for key in data:
|
||||
if key in {}:
|
||||
continue
|
||||
columns.append(Identifier(key))
|
||||
placeholders.append(Placeholder(key))
|
||||
columns = SQL(", ").join(columns)
|
||||
placeholders = SQL(", ").join(placeholders)
|
||||
query = SQL(
|
||||
"insert into public.nut ({columns}) values ({placeholders})"
|
||||
).format(columns=columns, placeholders=placeholders)
|
||||
cursor.execute(query, data)
|
||||
|
||||
def main():
|
||||
print("Running Log NUT UPS")
|
||||
ups = LogNutUps()
|
||||
while True:
|
||||
data = ups.get_ups()
|
||||
ups.insert_measurement(data)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
while True:
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
time.sleep(1)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
psycopg[binary]
|
||||
PyNUTClient
|
680
telnetlib.py
Normal file
680
telnetlib.py
Normal file
@ -0,0 +1,680 @@
|
||||
r"""TELNET client class.
|
||||
|
||||
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 <Dec 2 11:10> 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
|
||||
import warnings
|
||||
|
||||
#warnings._deprecated(__name__, remove=(3, 13))
|
||||
|
||||
__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.
|
||||
|
||||
"""
|
||||
sock = None # for __del__()
|
||||
|
||||
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
|
||||
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()
|
Loading…
x
Reference in New Issue
Block a user