Adds SNMP power driver
Add a new PowerDriver module for remote control of node power by enabling or disabling sockets on a rack power strip via SNMP. Implements: blueprint ironic-snmp-power-driver Change-Id: Ib16781eaa9737525c4d8a9231ecd9e72d7e2645e Co-authored-by: David Hewson <dhewson@cray.com> Co-authored-by: Mark Goddard <mgoddard@cray.com>
This commit is contained in:
parent
5592b1fa9a
commit
8a6b72030c
@ -11,4 +11,60 @@ DRAC with PXE deploy
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Add ``pxe_drac`` to the list of ``enabled_drivers in`` ``/etc/ironic/ironic.conf``
|
||||
- Install openwsman-python package
|
||||
- Install openwsman-python package
|
||||
|
||||
SNMP
|
||||
----
|
||||
|
||||
The SNMP power driver enables control of power distribution units of the type
|
||||
frequently found in data centre racks. PDUs frequently have a management
|
||||
ethernet interface and SNMP support enabling control of the power outlets.
|
||||
|
||||
The SNMP power driver works with the PXE driver for network deployment and
|
||||
network-configured boot.
|
||||
|
||||
Supported PDUs
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
- American Power Conversion (APC)
|
||||
- CyberPower (implemented according to MIB spec but not tested on hardware)
|
||||
- EatonPower (implemented according to MIB spec but not tested on hardware)
|
||||
- Teltronix
|
||||
|
||||
Software Requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- The PySNMP package must be installed, variously referred to as ``pysnmp``
|
||||
or ``python-pysnmp``
|
||||
|
||||
Enabling the SNMP Power Driver
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Add ``pxe_snmp`` to the list of ``enabled_drivers`` in ``/etc/ironic/ironic.conf``
|
||||
- Ironic Conductor must be restarted for the new driver to be loaded.
|
||||
|
||||
Ironic Node Configuration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Nodes are configured for SNMP control by setting the Ironic node object's
|
||||
``driver`` property to be ``pxe_snmp``. Further configuration values are
|
||||
added to ``driver_info``:
|
||||
|
||||
- ``snmp_address``: the IPv4 address of the PDU controlling this node.
|
||||
- ``snmp_port``: (optional) A non-standard UDP port to use for SNMP operations.
|
||||
If not specified, the default port (161) is used.
|
||||
- ``snmp_outlet``: The power outlet on the PDU (1-based indexing).
|
||||
- ``snmp_protocol``: (optional) SNMP protocol version
|
||||
(permitted values ``1``, ``2c`` or ``3``). If not specified, SNMPv1 is chosen.
|
||||
- ``snmp_community``: (Required for SNMPv1 and SNMPv2c) SNMP community
|
||||
parameter for reads and writes to the PDU.
|
||||
- ``snmp_security``: (Required for SNMPv3) SNMP security string.
|
||||
|
||||
PDU Configuration
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
This version of the SNMP power driver does not support handling
|
||||
PDU authentication credentials. When using SNMPv3, the PDU must be
|
||||
configured for ``NoAuthentication`` and ``NoEncryption``. The
|
||||
security name is used analagously to the SNMP community in early
|
||||
SNMP versions.
|
||||
|
@ -1155,6 +1155,17 @@
|
||||
#action_timeout=10
|
||||
|
||||
|
||||
[snmp]
|
||||
|
||||
#
|
||||
# Options defined in ironic.drivers.modules.snmp
|
||||
#
|
||||
|
||||
# Seconds to wait for power action to be completed (integer
|
||||
# value)
|
||||
#power_timeout=10
|
||||
|
||||
|
||||
[ssh]
|
||||
|
||||
#
|
||||
|
@ -459,3 +459,7 @@ class ImageCreationFailed(IronicException):
|
||||
|
||||
class SwiftOperationError(IronicException):
|
||||
message = _("Swift operation '%(operation)s' failed: %(error)s")
|
||||
|
||||
|
||||
class SNMPFailure(IronicException):
|
||||
message = _("SNMP operation '%(operation)s' failed: %(error)s")
|
||||
|
@ -32,6 +32,7 @@ from ironic.drivers.modules import ipminative
|
||||
from ironic.drivers.modules import ipmitool
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.drivers.modules import seamicro
|
||||
from ironic.drivers.modules import snmp
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.drivers import utils
|
||||
|
||||
@ -145,3 +146,15 @@ class FakeDracDriver(base.BaseDriver):
|
||||
self.power = drac_power.DracPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
self.management = drac_mgmt.DracManagement()
|
||||
|
||||
|
||||
class FakeSNMPDriver(base.BaseDriver):
|
||||
"""Fake SNMP driver."""
|
||||
|
||||
def __init__(self):
|
||||
if not importutils.try_import('pysnmp'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason="Unable to import pysnmp library")
|
||||
self.power = snmp.SNMPPower()
|
||||
self.deploy = fake.FakeDeploy()
|
||||
|
657
ironic/drivers/modules/snmp.py
Normal file
657
ironic/drivers/modules/snmp.py
Normal file
@ -0,0 +1,657 @@
|
||||
# Copyright 2013,2014 Cray Inc
|
||||
#
|
||||
# Authors: David Hewson <dhewson@cray.com>
|
||||
# Stig Telfer <stelfer@cray.com>
|
||||
# Mark Goddard <mgoddard@cray.com>
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Ironic SNMP power manager.
|
||||
|
||||
Provides basic power control using an SNMP-enabled smart power controller.
|
||||
Uses a pluggable driver model to support devices with different SNMP object
|
||||
models.
|
||||
|
||||
"""
|
||||
|
||||
import abc
|
||||
import six
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo.utils import importutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import i18n
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers import base
|
||||
from ironic.openstack.common import log as logging
|
||||
from ironic.openstack.common import loopingcall
|
||||
|
||||
pysnmp = importutils.try_import('pysnmp')
|
||||
if pysnmp:
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
from pysnmp.proto import rfc1902
|
||||
else:
|
||||
cmdgen = None
|
||||
rfc1902 = None
|
||||
|
||||
opts = [
|
||||
cfg.IntOpt('power_timeout',
|
||||
default=10,
|
||||
help='Seconds to wait for power action to be completed')
|
||||
]
|
||||
|
||||
_LW = i18n._LW
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(opts, group='snmp')
|
||||
|
||||
|
||||
SNMP_V1 = '1'
|
||||
SNMP_V2C = '2c'
|
||||
SNMP_V3 = '3'
|
||||
SNMP_PORT = 161
|
||||
|
||||
REQUIRED_PROPERTIES = {
|
||||
'snmp_driver': _("PDU manufacturer driver. Required."),
|
||||
'snmp_address': _("PDU IPv4 address or hostname. Required."),
|
||||
'snmp_outlet': _("PDU power outlet index (1-based). Required."),
|
||||
}
|
||||
OPTIONAL_PROPERTIES = {
|
||||
'snmp_version': _("SNMP protocol version: %(v1)s, %(v2c)s, %(v3)s "
|
||||
"(optional, default %(v1)s)")
|
||||
% {"v1": SNMP_V1, "v2c": SNMP_V2C, "v3": SNMP_V3},
|
||||
'snmp_port': _("SNMP port, default %(port)d") % {"port": SNMP_PORT},
|
||||
'snmp_community': _("SNMP community. Required for versions %(v1)s, "
|
||||
"%(v2c)s")
|
||||
% {"v1": SNMP_V1, "v2c": SNMP_V2C},
|
||||
'snmp_security': _("SNMP security name. Required for version %(v3)s")
|
||||
% {"v3": SNMP_V3},
|
||||
}
|
||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
||||
|
||||
|
||||
class SNMPClient(object):
|
||||
"""SNMP client object.
|
||||
|
||||
Performs low level SNMP get and set operations. Encapsulates all
|
||||
interaction with PySNMP to simplify dynamic importing and unit testing.
|
||||
"""
|
||||
|
||||
def __init__(self, address, port, version, community=None, security=None):
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.version = version
|
||||
if self.version == SNMP_V3:
|
||||
self.security = security
|
||||
else:
|
||||
self.community = community
|
||||
self.cmd_gen = cmdgen.CommandGenerator()
|
||||
|
||||
def _get_auth(self):
|
||||
"""Return the authorization data for an SNMP request.
|
||||
|
||||
:returns: A
|
||||
:class:`pysnmp.entity.rfc3413.oneliner.cmdgen.CommunityData`
|
||||
object.
|
||||
"""
|
||||
if self.version == SNMP_V3:
|
||||
# Handling auth/encryption credentials is not (yet) supported.
|
||||
# This version supports a security name analagous to community.
|
||||
return cmdgen.UsmUserData(self.security)
|
||||
else:
|
||||
mp_model = 1 if self.version == SNMP_V2C else 0
|
||||
return cmdgen.CommunityData(self.community, mpModel=mp_model)
|
||||
|
||||
def _get_transport(self):
|
||||
"""Return the transport target for an SNMP request.
|
||||
|
||||
:returns: A :class:
|
||||
`pysnmp.entity.rfc3413.oneliner.cmdgen.UdpTransportTarget` object.
|
||||
"""
|
||||
# The transport target accepts timeout and retries parameters, which
|
||||
# default to 1 (second) and 5 respectively. These are deemed sensible
|
||||
# enough to allow for an unreliable network or slow device.
|
||||
return cmdgen.UdpTransportTarget((self.address, self.port))
|
||||
|
||||
def get(self, oid):
|
||||
"""Use PySNMP to perform an SNMP GET operation on a single object.
|
||||
|
||||
:param oid: The OID of the object to get.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: The value of the requested object.
|
||||
"""
|
||||
results = self.cmd_gen.getCmd(self._get_auth(), self._get_transport(),
|
||||
oid)
|
||||
error_indication, error_status, error_index, var_binds = results
|
||||
|
||||
if error_indication:
|
||||
# SNMP engine-level error.
|
||||
raise exception.SNMPFailure(operation="GET",
|
||||
error=error_indication)
|
||||
|
||||
if error_status:
|
||||
# SNMP PDU error.
|
||||
raise exception.SNMPFailure(operation="GET",
|
||||
error=error_status.prettyPrint())
|
||||
|
||||
# We only expect a single value back
|
||||
name, val = var_binds[0]
|
||||
return val
|
||||
|
||||
def set(self, oid, value):
|
||||
"""Use PySNMP to perform an SNMP SET operation on a single object.
|
||||
|
||||
:param oid: The OID of the object to set.
|
||||
:param value: The value of the object to set.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
results = self.cmd_gen.setCmd(self._get_auth(), self._get_transport(),
|
||||
(oid, value))
|
||||
error_indication, error_status, error_index, var_binds = results
|
||||
|
||||
if error_indication:
|
||||
# SNMP engine-level error.
|
||||
raise exception.SNMPFailure(operation="SET",
|
||||
error=error_indication)
|
||||
|
||||
if error_status:
|
||||
# SNMP PDU error.
|
||||
raise exception.SNMPFailure(operation="SET",
|
||||
error=error_status.prettyPrint())
|
||||
|
||||
|
||||
def _get_client(snmp_info):
|
||||
"""Create and return an SNMP client object.
|
||||
|
||||
:param snmp_info: SNMP driver info.
|
||||
:returns: A :class:`SNMPClient` object.
|
||||
"""
|
||||
return SNMPClient(snmp_info["address"],
|
||||
snmp_info["port"],
|
||||
snmp_info["version"],
|
||||
snmp_info.get("community"),
|
||||
snmp_info.get("security"))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class SNMPDriverBase(object):
|
||||
"""SNMP power driver base class.
|
||||
|
||||
The SNMPDriver class hierarchy implements manufacturer-specific MIB actions
|
||||
over SNMP to interface with different smart power controller products.
|
||||
"""
|
||||
|
||||
oid_enterprise = (1, 3, 6, 1, 4, 1)
|
||||
retry_interval = 1
|
||||
|
||||
def __init__(self, snmp_info):
|
||||
self.snmp_info = snmp_info
|
||||
self.client = _get_client(snmp_info)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _snmp_power_state(self):
|
||||
"""Perform the SNMP request required to retrieve the current power
|
||||
state.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _snmp_power_on(self):
|
||||
"""Perform the SNMP request required to set the power on.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def _snmp_power_off(self):
|
||||
"""Perform the SNMP request required to set the power off.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
|
||||
def _snmp_wait_for_state(self, goal_state):
|
||||
"""Wait for the power state of the PDU outlet to change.
|
||||
|
||||
:param goal_state: The power state to wait for, one of
|
||||
:class:`ironic.common.states`.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
|
||||
def _poll_for_state(mutable):
|
||||
"""Called at an interval until the node's power is consistent.
|
||||
|
||||
:param mutable: dict object containing "state" and "next_time"
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
mutable["state"] = self._snmp_power_state()
|
||||
if mutable["state"] == goal_state:
|
||||
raise loopingcall.LoopingCallDone()
|
||||
|
||||
mutable["next_time"] += self.retry_interval
|
||||
if mutable["next_time"] >= CONF.snmp.power_timeout:
|
||||
mutable["state"] = states.ERROR
|
||||
raise loopingcall.LoopingCallDone()
|
||||
|
||||
# Pass state to the looped function call in a mutable form.
|
||||
state = {"state": None, "next_time": 0}
|
||||
timer = loopingcall.FixedIntervalLoopingCall(_poll_for_state,
|
||||
state)
|
||||
timer.start(interval=self.retry_interval).wait()
|
||||
LOG.debug("power state '%s'", state["state"])
|
||||
return state["state"]
|
||||
|
||||
def power_state(self):
|
||||
"""Returns a node's current power state.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
return self._snmp_power_state()
|
||||
|
||||
def power_on(self):
|
||||
"""Set the power state to this node to ON.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
self._snmp_power_on()
|
||||
return self._snmp_wait_for_state(states.POWER_ON)
|
||||
|
||||
def power_off(self):
|
||||
"""Set the power state to this node to OFF.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
self._snmp_power_off()
|
||||
return self._snmp_wait_for_state(states.POWER_OFF)
|
||||
|
||||
def power_reset(self):
|
||||
"""Reset the power to this node.
|
||||
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
power_result = self.power_off()
|
||||
if power_result != states.POWER_OFF:
|
||||
return states.ERROR
|
||||
power_result = self.power_on()
|
||||
if power_result != states.POWER_ON:
|
||||
return states.ERROR
|
||||
return power_result
|
||||
|
||||
|
||||
class SNMPDriverSimple(SNMPDriverBase):
|
||||
"""SNMP driver base class for simple PDU devices.
|
||||
|
||||
Here, simple refers to devices which provide a single SNMP object for
|
||||
controlling the power state of an outlet.
|
||||
|
||||
The default OID of the power state object is of the form
|
||||
<enterprise OID>.<device OID>.<outlet ID>. A different OID may be specified
|
||||
by overriding the _snmp_oid method in a subclass.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SNMPDriverSimple, self).__init__(*args, **kwargs)
|
||||
self.oid = self._snmp_oid()
|
||||
|
||||
@abc.abstractproperty
|
||||
def oid_device(self):
|
||||
"""Device dependent portion of the power state object OID."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def value_power_on(self):
|
||||
"""Value representing power on state."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def value_power_off(self):
|
||||
"""Value representing power off state."""
|
||||
|
||||
def _snmp_oid(self):
|
||||
"""Return the OID of the power state object.
|
||||
|
||||
:returns: Power state object OID as a tuple of integers.
|
||||
"""
|
||||
outlet = int(self.snmp_info['outlet'])
|
||||
return self.oid_enterprise + self.oid_device + (outlet,)
|
||||
|
||||
def _snmp_power_state(self):
|
||||
state = self.client.get(self.oid)
|
||||
|
||||
# Translate the state to an Ironic power state.
|
||||
if state == self.value_power_on:
|
||||
power_state = states.POWER_ON
|
||||
elif state == self.value_power_off:
|
||||
power_state = states.POWER_OFF
|
||||
else:
|
||||
LOG.warning(_LW("SNMP PDU %(addr)s outlet %(outlet)s: "
|
||||
"unrecognised power state %(state)s."),
|
||||
{'addr': self.snmp_info['address'],
|
||||
'outlet': self.snmp_info['outlet'],
|
||||
'state': state})
|
||||
power_state = states.ERROR
|
||||
|
||||
return power_state
|
||||
|
||||
def _snmp_power_on(self):
|
||||
value = rfc1902.Integer(self.value_power_on)
|
||||
self.client.set(self.oid, value)
|
||||
|
||||
def _snmp_power_off(self):
|
||||
value = rfc1902.Integer(self.value_power_off)
|
||||
self.client.set(self.oid, value)
|
||||
|
||||
|
||||
class SNMPDriverAPC(SNMPDriverSimple):
|
||||
"""SNMP driver class for APC PDU devices.
|
||||
|
||||
SNMP objects for APC PDU:
|
||||
1.3.6.1.4.1.318.1.1.4.4.2.1.3 sPDUOutletCtl
|
||||
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow]
|
||||
"""
|
||||
|
||||
oid_device = (318, 1, 1, 4, 4, 2, 1, 3)
|
||||
value_power_on = 1
|
||||
value_power_off = 2
|
||||
|
||||
|
||||
class SNMPDriverCyberPower(SNMPDriverSimple):
|
||||
"""SNMP driver class for CyberPower PDU devices.
|
||||
|
||||
SNMP objects for CyberPower PDU:
|
||||
1.3.6.1.4.1.3808.1.1.3.3.3.1.1.4 ePDUOutletControlOutletCommand
|
||||
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow]
|
||||
"""
|
||||
|
||||
# NOTE(mgoddard): This device driver is currently untested, this driver has
|
||||
# been implemented based upon its published MIB
|
||||
# documentation.
|
||||
|
||||
oid_device = (3808, 1, 1, 3, 3, 3, 1, 1, 4)
|
||||
value_power_on = 1
|
||||
value_power_off = 2
|
||||
|
||||
|
||||
class SNMPDriverTeltronix(SNMPDriverSimple):
|
||||
"""SNMP driver class for Teltronix PDU devices.
|
||||
|
||||
SNMP objects for Teltronix PDU:
|
||||
1.3.6.1.4.1.23620.1.2.2.1.4 Outlet Power
|
||||
Values: 1=Off, 2=On
|
||||
"""
|
||||
|
||||
oid_device = (23620, 1, 2, 2, 1, 4)
|
||||
value_power_on = 2
|
||||
value_power_off = 1
|
||||
|
||||
|
||||
class SNMPDriverEatonPower(SNMPDriverBase):
|
||||
"""SNMP driver class for Eaton Power PDU.
|
||||
|
||||
The Eaton power PDU does not follow the model of SNMPDriverSimple as it
|
||||
uses multiple SNMP objects.
|
||||
|
||||
SNMP objects for Eaton Power PDU
|
||||
1.3.6.1.4.1.534.6.6.7.6.6.1.2.<outlet ID> outletControlStatus
|
||||
Read 0=off, 1=on, 2=pending off, 3=pending on
|
||||
1.3.6.1.4.1.534.6.6.7.6.6.1.3.<outlet ID> outletControlOffCmd
|
||||
Write 0 for immediate power off
|
||||
1.3.6.1.4.1.534.6.6.7.6.6.1.4.<outlet ID> outletControlOnCmd
|
||||
Write 0 for immediate power on
|
||||
"""
|
||||
|
||||
# NOTE(mgoddard): This device driver is currently untested, this driver has
|
||||
# been implemented based upon its published MIB
|
||||
# documentation.
|
||||
|
||||
oid_device = (534, 6, 6, 7, 6, 6, 1)
|
||||
oid_status = (2,)
|
||||
oid_poweron = (3,)
|
||||
oid_poweroff = (4,)
|
||||
|
||||
status_off = 0
|
||||
status_on = 1
|
||||
status_pending_off = 2
|
||||
status_pending_on = 3
|
||||
|
||||
value_power_on = 0
|
||||
value_power_off = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SNMPDriverEatonPower, self).__init__(*args, **kwargs)
|
||||
# Due to its use of different OIDs for different actions, we only form
|
||||
# an OID that holds the common substring of the OIDs for power
|
||||
# operations.
|
||||
self.oid_base = self.oid_enterprise + self.oid_device
|
||||
|
||||
def _snmp_oid(self, oid):
|
||||
"""Return the OID for one of the outlet control objects.
|
||||
|
||||
:param oid: The action-dependent portion of the OID, as a tuple of
|
||||
integers.
|
||||
:returns: The full OID as a tuple of integers.
|
||||
"""
|
||||
outlet = int(self.snmp_info['outlet'])
|
||||
return self.oid_base + oid + (outlet,)
|
||||
|
||||
def _snmp_power_state(self):
|
||||
oid = self._snmp_oid(self.oid_status)
|
||||
state = self.client.get(oid)
|
||||
|
||||
# Translate the state to an Ironic power state.
|
||||
if state in (self.status_on, self.status_pending_off):
|
||||
power_state = states.POWER_ON
|
||||
elif state in (self.status_off, self.status_pending_on):
|
||||
power_state = states.POWER_OFF
|
||||
else:
|
||||
LOG.warning(_LW("Eaton Power SNMP PDU %(addr)s outlet %(outlet)s: "
|
||||
"unrecognised power state %(state)s."),
|
||||
{'addr': self.snmp_info['address'],
|
||||
'outlet': self.snmp_info['outlet'],
|
||||
'state': state})
|
||||
power_state = states.ERROR
|
||||
|
||||
return power_state
|
||||
|
||||
def _snmp_power_on(self):
|
||||
oid = self._snmp_oid(self.oid_poweron)
|
||||
value = rfc1902.Integer(self.value_power_on)
|
||||
self.client.set(oid, value)
|
||||
|
||||
def _snmp_power_off(self):
|
||||
oid = self._snmp_oid(self.oid_poweroff)
|
||||
value = rfc1902.Integer(self.value_power_off)
|
||||
self.client.set(oid, value)
|
||||
|
||||
|
||||
# A dictionary of supported drivers keyed by snmp_driver attribute
|
||||
DRIVER_CLASSES = {
|
||||
'apc': SNMPDriverAPC,
|
||||
'cyberpower': SNMPDriverCyberPower,
|
||||
'eatonpower': SNMPDriverEatonPower,
|
||||
'teltronix': SNMPDriverTeltronix
|
||||
}
|
||||
|
||||
|
||||
def _parse_driver_info(node):
|
||||
"""Return a dictionary of validated driver information, usable for
|
||||
SNMPDriver object creation.
|
||||
|
||||
:param node: An Ironic node object.
|
||||
:returns: SNMP driver info.
|
||||
:raises: MissingParameterValue if any required parameters are missing.
|
||||
:raises: InvalidParameterValue if any parameters are invalid.
|
||||
"""
|
||||
info = node.driver_info or {}
|
||||
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
|
||||
if missing_info:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"SNMP driver requires the following to be set: %s.")
|
||||
% missing_info)
|
||||
|
||||
snmp_info = {}
|
||||
|
||||
# Validate PDU driver type
|
||||
snmp_info['driver'] = info.get('snmp_driver')
|
||||
if snmp_info['driver'] not in DRIVER_CLASSES:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"SNMPPowerDriver: unknown driver: '%s'") % snmp_info['driver'])
|
||||
|
||||
# In absence of a version, default to SNMPv1
|
||||
snmp_info['version'] = info.get('snmp_version', SNMP_V1)
|
||||
if snmp_info['version'] not in (SNMP_V1, SNMP_V2C, SNMP_V3):
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"SNMPPowerDriver: unknown SNMP version: '%s'") %
|
||||
snmp_info['version'])
|
||||
|
||||
# In absence of a configured UDP port, default to the standard port
|
||||
port_str = info.get('snmp_port', SNMP_PORT)
|
||||
try:
|
||||
snmp_info['port'] = int(port_str)
|
||||
except ValueError:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"SNMPPowerDriver: SNMP UDP port must be numeric: %s") % port_str)
|
||||
if snmp_info['port'] < 1 or snmp_info['port'] > 65535:
|
||||
raise exception.InvalidParameterValue(_(
|
||||
"SNMPPowerDriver: SNMP UDP port out of range: %d")
|
||||
% snmp_info['port'])
|
||||
|
||||
# Extract version-dependent required parameters
|
||||
if snmp_info['version'] in (SNMP_V1, SNMP_V2C):
|
||||
if 'snmp_community' not in info:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"SNMP driver requires snmp_community to be set for version "
|
||||
"%s.") % snmp_info['version'])
|
||||
snmp_info['community'] = info.get('snmp_community')
|
||||
elif snmp_info['version'] == SNMP_V3:
|
||||
if 'snmp_security' not in info:
|
||||
raise exception.MissingParameterValue(_(
|
||||
"SNMP driver requires snmp_security to be set for version %s.")
|
||||
% (SNMP_V3))
|
||||
snmp_info['security'] = info.get('snmp_security')
|
||||
|
||||
# Target PDU IP address and power outlet identification
|
||||
snmp_info['address'] = info.get('snmp_address')
|
||||
snmp_info['outlet'] = info.get('snmp_outlet')
|
||||
|
||||
return snmp_info
|
||||
|
||||
|
||||
def _get_driver(node):
|
||||
"""Return a new SNMP driver object of the correct type for `node`.
|
||||
|
||||
:param node: Single node object.
|
||||
:raises: InvalidParameterValue if node power config is incomplete or
|
||||
invalid.
|
||||
:returns: SNMP driver object.
|
||||
"""
|
||||
snmp_info = _parse_driver_info(node)
|
||||
cls = DRIVER_CLASSES[snmp_info['driver']]
|
||||
return cls(snmp_info)
|
||||
|
||||
|
||||
class SNMPPower(base.PowerInterface):
|
||||
"""SNMP Power Interface.
|
||||
|
||||
This PowerInterface class provides a mechanism for controlling the power
|
||||
state of a physical device using an SNMP-enabled smart power controller.
|
||||
"""
|
||||
|
||||
def get_properties(self):
|
||||
"""Return the properties of the interface.
|
||||
|
||||
:returns: dictionary of <property name>:<property description> entries.
|
||||
"""
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
def validate(self, task):
|
||||
"""Check that node.driver_info contains the requisite fields.
|
||||
|
||||
:raises: MissingParameterValue if required SNMP parameters are missing.
|
||||
:raises: InvalidParameterValue if SNMP parameters are invalid.
|
||||
"""
|
||||
_parse_driver_info(task.node)
|
||||
|
||||
def get_power_state(self, task):
|
||||
"""Get the current power state.
|
||||
|
||||
Poll the SNMP device for the current power state of the node.
|
||||
|
||||
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||
:raises: MissingParameterValue if required SNMP parameters are missing.
|
||||
:raises: InvalidParameterValue if SNMP parameters are invalid.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
:returns: power state. One of :class:`ironic.common.states`.
|
||||
"""
|
||||
driver = _get_driver(task.node)
|
||||
power_state = driver.power_state()
|
||||
return power_state
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def set_power_state(self, task, pstate):
|
||||
"""Turn the power on or off.
|
||||
|
||||
Set the power state of a node.
|
||||
|
||||
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||
:param pstate: Either POWER_ON or POWER_OFF from :class:
|
||||
`ironic.common.states`.
|
||||
:raises: MissingParameterValue if required SNMP parameters are missing.
|
||||
:raises: InvalidParameterValue if SNMP parameters are invalid or
|
||||
`pstate` is invalid.
|
||||
:raises: PowerStateFailure if the final power state of the node is not
|
||||
as requested after the timeout.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
|
||||
driver = _get_driver(task.node)
|
||||
if pstate == states.POWER_ON:
|
||||
state = driver.power_on()
|
||||
elif pstate == states.POWER_OFF:
|
||||
state = driver.power_off()
|
||||
else:
|
||||
raise exception.InvalidParameterValue(_("set_power_state called "
|
||||
"with invalid power "
|
||||
"state %s.") % str(pstate))
|
||||
if state != pstate:
|
||||
raise exception.PowerStateFailure(pstate=pstate)
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def reboot(self, task):
|
||||
"""Cycles the power to a node.
|
||||
|
||||
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||
:raises: MissingParameterValue if required SNMP parameters are missing.
|
||||
:raises: InvalidParameterValue if SNMP parameters are invalid.
|
||||
:raises: PowerStateFailure if the final power state of the node is not
|
||||
POWER_ON after the timeout.
|
||||
:raises: SNMPFailure if an SNMP request fails.
|
||||
"""
|
||||
|
||||
driver = _get_driver(task.node)
|
||||
state = driver.power_reset()
|
||||
if state != states.POWER_ON:
|
||||
raise exception.PowerStateFailure(pstate=states.POWER_ON)
|
@ -28,6 +28,7 @@ from ironic.drivers.modules import ipminative
|
||||
from ironic.drivers.modules import ipmitool
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.drivers.modules import seamicro
|
||||
from ironic.drivers.modules import snmp
|
||||
from ironic.drivers.modules import ssh
|
||||
from ironic.drivers import utils
|
||||
|
||||
@ -154,3 +155,27 @@ class PXEAndIloDriver(base.BaseDriver):
|
||||
self.power = ilo_power.IloPower()
|
||||
self.deploy = pxe.PXEDeploy()
|
||||
self.vendor = pxe.VendorPassthru()
|
||||
|
||||
|
||||
class PXEAndSNMPDriver(base.BaseDriver):
|
||||
"""PXE + SNMP driver.
|
||||
|
||||
This driver implements the 'core' functionality, combining
|
||||
:class:ironic.drivers.snmp.SNMP for power on/off and reboot with
|
||||
:class:ironic.drivers.pxe.PXE for image deployment. Implentations are in
|
||||
those respective classes; this class is merely the glue between them.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Driver has a runtime dependency on PySNMP, abort load if it is absent
|
||||
if not importutils.try_import('pysnmp'):
|
||||
raise exception.DriverLoadError(
|
||||
driver=self.__class__.__name__,
|
||||
reason=_("Unable to import pysnmp library"))
|
||||
self.power = snmp.SNMPPower()
|
||||
self.deploy = pxe.PXEDeploy()
|
||||
self.vendor = pxe.VendorPassthru()
|
||||
|
||||
# PDUs have no boot device management capability.
|
||||
# Only PXE as a boot device is supported.
|
||||
self.management = None
|
||||
|
@ -2214,6 +2214,11 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
|
||||
'seamicro_api_version']
|
||||
self._check_driver_properties("fake_seamicro", expected)
|
||||
|
||||
def test_driver_properties_fake_snmp(self):
|
||||
expected = ['snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
||||
'snmp_community', 'snmp_security', 'snmp_outlet']
|
||||
self._check_driver_properties("fake_snmp", expected)
|
||||
|
||||
def test_driver_properties_pxe_ipmitool(self):
|
||||
expected = ['ipmi_address', 'ipmi_terminal_port',
|
||||
'ipmi_password', 'ipmi_priv_level',
|
||||
@ -2243,6 +2248,12 @@ class ManagerTestProperties(tests_db_base.DbTestCase):
|
||||
'seamicro_api_version']
|
||||
self._check_driver_properties("pxe_seamicro", expected)
|
||||
|
||||
def test_driver_properties_pxe_snmp(self):
|
||||
expected = ['pxe_deploy_kernel', 'pxe_deploy_ramdisk',
|
||||
'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
||||
'snmp_community', 'snmp_security', 'snmp_outlet']
|
||||
self._check_driver_properties("pxe_snmp", expected)
|
||||
|
||||
def test_driver_properties_fake_ilo(self):
|
||||
expected = ['ilo_address', 'ilo_username', 'ilo_password',
|
||||
'client_port', 'client_timeout']
|
||||
|
@ -124,6 +124,21 @@ def get_test_iboot_info():
|
||||
}
|
||||
|
||||
|
||||
def get_test_snmp_info(**kw):
|
||||
result = {
|
||||
"snmp_driver": kw.get("snmp_driver", "teltronix"),
|
||||
"snmp_address": kw.get("snmp_address", "1.2.3.4"),
|
||||
"snmp_port": kw.get("snmp_port", "161"),
|
||||
"snmp_outlet": kw.get("snmp_outlet", "1"),
|
||||
"snmp_version": kw.get("snmp_version", "1")
|
||||
}
|
||||
if result["snmp_version"] in ("1", "2c"):
|
||||
result["snmp_community"] = kw.get("snmp_community", "public")
|
||||
elif result["snmp_version"] == "3":
|
||||
result["snmp_security"] = kw.get("snmp_security", "public")
|
||||
return result
|
||||
|
||||
|
||||
def get_test_node(**kw):
|
||||
properties = {
|
||||
"cpu_arch": "x86_64",
|
||||
|
1007
ironic/tests/drivers/test_snmp.py
Normal file
1007
ironic/tests/drivers/test_snmp.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@ Current list of mocked libraries:
|
||||
seamicroclient
|
||||
ipminative
|
||||
proliantutils
|
||||
pysnmp
|
||||
"""
|
||||
|
||||
import sys
|
||||
@ -113,3 +114,27 @@ if not iboot:
|
||||
# external library has been mocked
|
||||
if 'ironic.drivers.modules.iboot' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.modules.iboot'])
|
||||
|
||||
|
||||
# attempt to load the external 'pysnmp' library, which is required by
|
||||
# the optional drivers.modules.snmp module
|
||||
pysnmp = importutils.try_import("pysnmp")
|
||||
if not pysnmp:
|
||||
pysnmp = mock.Mock()
|
||||
sys.modules["pysnmp"] = pysnmp
|
||||
sys.modules["pysnmp.entity"] = pysnmp.entity
|
||||
sys.modules["pysnmp.entity.rfc3413"] = pysnmp.entity.rfc3413
|
||||
sys.modules["pysnmp.entity.rfc3413.oneliner"] = (
|
||||
pysnmp.entity.rfc3413.oneliner)
|
||||
sys.modules["pysnmp.entity.rfc3413.oneliner.cmdgen"] = (
|
||||
pysnmp.entity.rfc3413.oneliner.cmdgen)
|
||||
sys.modules["pysnmp.proto"] = pysnmp.proto
|
||||
sys.modules["pysnmp.proto.rfc1902"] = pysnmp.proto.rfc1902
|
||||
# Patch the RFC1902 integer class with a python int
|
||||
pysnmp.proto.rfc1902.Integer = int
|
||||
|
||||
|
||||
# if anything has loaded the snmp driver yet, reload it now that the
|
||||
# external library has been mocked
|
||||
if 'ironic.drivers.modules.snmp' in sys.modules:
|
||||
reload(sys.modules['ironic.drivers.modules.snmp'])
|
||||
|
@ -48,6 +48,7 @@ ironic.drivers =
|
||||
fake_iboot = ironic.drivers.fake:FakeIBootDriver
|
||||
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
||||
fake_drac = ironic.drivers.fake:FakeDracDriver
|
||||
fake_snmp = ironic.drivers.fake:FakeSNMPDriver
|
||||
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
|
||||
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
||||
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
||||
@ -55,6 +56,7 @@ ironic.drivers =
|
||||
pxe_iboot = ironic.drivers.pxe:PXEAndIBootDriver
|
||||
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
||||
pxe_drac = ironic.drivers.drac:PXEDracDriver
|
||||
pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
|
Loading…
Reference in New Issue
Block a user