297 lines
9.8 KiB
Python
297 lines
9.8 KiB
Python
# Copyright 2016 Internap
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import logging
|
|
import threading
|
|
|
|
from pysnmp.carrier.asynsock.dgram import udp
|
|
from pysnmp import debug
|
|
from pysnmp.entity import config
|
|
from pysnmp.entity import engine
|
|
from pysnmp.entity.rfc3413 import cmdrsp
|
|
from pysnmp.entity.rfc3413 import context
|
|
from pysnmp.proto.api import v2c
|
|
|
|
# pysnmp is distributed under the BSD license.
|
|
|
|
from virtualpdu.pdu import TraversableOidMapping
|
|
|
|
|
|
auth_protocols = {
|
|
'MD5': config.usmHMACMD5AuthProtocol,
|
|
'SHA': config.usmHMACSHAAuthProtocol,
|
|
'NONE': config.usmNoAuthProtocol
|
|
}
|
|
|
|
# Some auth protocols may not be available in older pysnmp versions
|
|
|
|
try:
|
|
auth_protocols['SHA224'] = config.usmHMAC128SHA224AuthProtocol
|
|
auth_protocols['SHA256'] = config.usmHMAC192SHA256AuthProtocol
|
|
auth_protocols['SHA384'] = config.usmHMAC256SHA384AuthProtocol
|
|
auth_protocols['SHA512'] = config.usmHMAC384SHA512AuthProtocol
|
|
|
|
except AttributeError:
|
|
pass
|
|
|
|
priv_protocols = {
|
|
'DES': config.usmDESPrivProtocol,
|
|
'3DES': config.usm3DESEDEPrivProtocol,
|
|
'AES': config.usmAesCfb128Protocol,
|
|
'AES128': config.usmAesCfb128Protocol,
|
|
'AES192': config.usmAesCfb192Protocol,
|
|
'AES256': config.usmAesCfb256Protocol,
|
|
'NONE': config.usmNoPrivProtocol
|
|
}
|
|
|
|
# Some privacy protocols may not be available in older pysnmp versions
|
|
|
|
try:
|
|
priv_protocols['AES192BLMT'] = config.usmAesBlumenthalCfb192Protocol
|
|
priv_protocols['AES256BLMT'] = config.usmAesBlumenthalCfb256Protocol
|
|
|
|
except AttributeError:
|
|
pass
|
|
|
|
|
|
class GetCommandResponder(cmdrsp.GetCommandResponder):
|
|
|
|
def __init__(self, snmpEngine, snmpContext, context_name, power_unit):
|
|
super(GetCommandResponder, self).__init__(snmpEngine, snmpContext)
|
|
self.__context_name = v2c.OctetString(context_name)
|
|
self.__power_unit = power_unit
|
|
|
|
def handleMgmtOperation(self, snmpEngine, stateReference,
|
|
contextName, req_pdu, acInfo):
|
|
|
|
if self.__context_name == contextName:
|
|
|
|
var_binds = []
|
|
|
|
for oid, val in v2c.apiPDU.getVarBinds(req_pdu):
|
|
var_binds.append(
|
|
(oid, (self.__power_unit.oid_mapping[oid].value
|
|
if oid in self.__power_unit.oid_mapping
|
|
else v2c.NoSuchInstance('')))
|
|
)
|
|
|
|
self.sendRsp(snmpEngine, stateReference, 0, 0, var_binds)
|
|
|
|
self.releaseStateInformation(stateReference)
|
|
|
|
|
|
class NextCommandResponder(cmdrsp.NextCommandResponder):
|
|
|
|
def __init__(self, snmpEngine, snmpContext, context_name, power_unit):
|
|
super(NextCommandResponder, self).__init__(snmpEngine, snmpContext)
|
|
self.__context_name = v2c.OctetString(context_name)
|
|
self.__power_unit = power_unit
|
|
|
|
def handleMgmtOperation(self, snmpEngine, stateReference,
|
|
contextName, req_pdu, acInfo):
|
|
|
|
if self.__context_name == contextName:
|
|
|
|
oid_map = TraversableOidMapping(self.__power_unit.oid_mapping)
|
|
|
|
var_binds = []
|
|
|
|
for oid, val in v2c.apiPDU.getVarBinds(req_pdu):
|
|
|
|
try:
|
|
oid = oid_map.next(to=oid)
|
|
val = self.__power_unit.oid_mapping[oid].value
|
|
|
|
except (KeyError, IndexError):
|
|
val = v2c.NoSuchInstance('')
|
|
|
|
var_binds.append((oid, val))
|
|
|
|
self.sendRsp(snmpEngine, stateReference, 0, 0, var_binds)
|
|
|
|
self.releaseStateInformation(stateReference)
|
|
|
|
|
|
class SetCommandResponder(cmdrsp.SetCommandResponder):
|
|
|
|
def __init__(self, snmpEngine, snmpContext, context_name, power_unit):
|
|
super(SetCommandResponder, self).__init__(snmpEngine, snmpContext)
|
|
self.__context_name = v2c.OctetString(context_name)
|
|
self.__power_unit = power_unit
|
|
|
|
self.__logger = logging.getLogger(__name__)
|
|
|
|
def handleMgmtOperation(self, snmpEngine, stateReference,
|
|
contextName, req_pdu, acInfo):
|
|
|
|
if self.__context_name == contextName:
|
|
|
|
var_binds = []
|
|
|
|
for oid, val in v2c.apiPDU.getVarBinds(req_pdu):
|
|
if oid in self.__power_unit.oid_mapping:
|
|
try:
|
|
self.__power_unit.oid_mapping[oid].value = val
|
|
|
|
except Exception as ex:
|
|
self.__logger.info(
|
|
'Set value {} on power unit {} failed: {}'.format(
|
|
val, self.__power_unit.name, ex
|
|
)
|
|
)
|
|
val = v2c.NoSuchInstance('')
|
|
else:
|
|
val = v2c.NoSuchInstance('')
|
|
|
|
var_binds.append((oid, val))
|
|
|
|
self.sendRsp(snmpEngine, stateReference, 0, 0, var_binds)
|
|
|
|
self.releaseStateInformation(stateReference)
|
|
|
|
|
|
def create_snmp_engine(power_unit,
|
|
listen_address, listen_port,
|
|
**snmp_options):
|
|
|
|
snmp_versions = snmp_options.get('snmp_versions', [])
|
|
community = snmp_options.get('community')
|
|
engine_id = snmp_options.get('engine_id')
|
|
if engine_id:
|
|
engine_id = v2c.OctetString(hexValue=engine_id)
|
|
context_engine_id = snmp_options.get('context_engine_id')
|
|
if context_engine_id:
|
|
context_engine_id = v2c.OctetString(hexValue=context_engine_id)
|
|
context_name = snmp_options.get('context_name', '')
|
|
user = snmp_options.get('user')
|
|
auth_key = snmp_options.get('auth_key')
|
|
auth_protocol = auth_protocols[snmp_options.get('auth_protocol') or 'NONE']
|
|
priv_key = snmp_options.get('priv_key')
|
|
priv_protocol = priv_protocols[snmp_options.get('priv_protocol') or 'NONE']
|
|
|
|
snmp_engine = engine.SnmpEngine(snmpEngineID=engine_id)
|
|
|
|
config.addSocketTransport(
|
|
snmp_engine,
|
|
udp.domainName,
|
|
udp.UdpTransport().openServerMode((listen_address, listen_port))
|
|
)
|
|
|
|
# SNMPv1
|
|
if '1' in snmp_versions:
|
|
config.addV1System(snmp_engine, community, community)
|
|
|
|
# Allow read MIB access for this user / securityModels at SNMP VACM
|
|
config.addVacmUser(snmp_engine, 1,
|
|
community, 'noAuthNoPriv', (1,), (1,))
|
|
|
|
# SNMPv1
|
|
if '2c' in snmp_versions:
|
|
config.addV1System(snmp_engine, community, community)
|
|
|
|
# Allow read MIB access for this user / securityModels at SNMP VACM
|
|
config.addVacmUser(snmp_engine, 2,
|
|
community, 'noAuthNoPriv', (1,), (1,))
|
|
|
|
# SNMPv3/USM setup
|
|
|
|
if '3' in snmp_versions:
|
|
config.addV3User(
|
|
snmp_engine, user,
|
|
auth_protocol, auth_key,
|
|
priv_protocol, priv_key
|
|
)
|
|
|
|
if (auth_protocol != config.usmNoAuthProtocol
|
|
and priv_protocol != config.usmNoPrivProtocol):
|
|
sec_level = 'authPriv'
|
|
elif priv_protocol != config.usmNoAuthProtocol:
|
|
sec_level = 'authNoPriv'
|
|
else:
|
|
sec_level = 'noAuthNoPriv'
|
|
|
|
config.addVacmUser(snmp_engine, 3,
|
|
user, sec_level, (1,), (1,))
|
|
|
|
# SNMP context name is not actually used because we intercept
|
|
# MIB management calls by overriding `handleMgmtOperation()`
|
|
snmp_context = context.SnmpContext(snmp_engine,
|
|
contextEngineId=context_engine_id)
|
|
|
|
else:
|
|
snmp_context = context.SnmpContext(snmp_engine)
|
|
|
|
# Register SNMP Apps at the SNMP engine for particular SNMP context
|
|
GetCommandResponder(snmp_engine, snmp_context,
|
|
context_name=context_name, power_unit=power_unit)
|
|
NextCommandResponder(snmp_engine, snmp_context,
|
|
context_name=context_name, power_unit=power_unit)
|
|
SetCommandResponder(snmp_engine, snmp_context,
|
|
context_name=context_name, power_unit=power_unit)
|
|
|
|
return snmp_engine
|
|
|
|
|
|
class SNMPPDUHarness(threading.Thread):
|
|
def __init__(self, power_unit,
|
|
listen_address, listen_port,
|
|
**snmp_options):
|
|
|
|
super(SNMPPDUHarness, self).__init__()
|
|
|
|
self._logger = logging.getLogger(__name__)
|
|
|
|
if snmp_options.get('debug_snmp'):
|
|
debug.setLogger(debug.Debug('all'))
|
|
|
|
self.snmp_engine = create_snmp_engine(
|
|
power_unit,
|
|
listen_address, listen_port,
|
|
**snmp_options
|
|
)
|
|
|
|
self.listen_address = listen_address
|
|
self.listen_port = listen_port
|
|
self.power_unit = power_unit
|
|
|
|
self._lock = threading.Lock()
|
|
self._stop_requested = False
|
|
|
|
def run(self):
|
|
with self._lock:
|
|
if self._stop_requested:
|
|
return
|
|
|
|
self._logger.info("Starting SNMP agent at {}:{} serving '{}'"
|
|
.format(self.listen_address, self.listen_port,
|
|
self.power_unit.name))
|
|
|
|
self.snmp_engine.transportDispatcher.jobStarted(1)
|
|
|
|
try:
|
|
# Dispatcher will never finish as job#1 never reaches zero
|
|
self.snmp_engine.transportDispatcher.runDispatcher()
|
|
|
|
except Exception:
|
|
self.snmp_engine.transportDispatcher.closeDispatcher()
|
|
|
|
def stop(self):
|
|
with self._lock:
|
|
self._stop_requested = True
|
|
try:
|
|
self.snmp_engine.transportDispatcher.jobFinished(1)
|
|
|
|
except KeyError:
|
|
pass # The job is not started yet and will not start
|