iRMC power driver for soft reboot and soft power off

This patch enhances iRMC power driver to support SOFT_REBOOT_SOFT and
SOFT_POWER_OFF.

Partial-Bug: #1526226
Change-Id: I8c69904063ac0a9e042f54158a20347f0c2325e1
This commit is contained in:
Naohiro Tamura 2015-08-25 18:54:51 +09:00
parent bbd0e96c4b
commit d2a4dca36d
6 changed files with 399 additions and 28 deletions

View File

@ -1831,6 +1831,9 @@
# SNMP security name. Required for version "v3" (string value)
#snmp_security = <None>
# SNMP polling interval in seconds (integer value)
#snmp_polling_interval = 10
[ironic_lib]

View File

@ -66,6 +66,9 @@ opts = [
help=_('SNMP community. Required for versions "v1" and "v2c"')),
cfg.StrOpt('snmp_security',
help=_('SNMP security name. Required for version "v3"')),
cfg.IntOpt('snmp_polling_interval',
default=10,
help='SNMP polling interval in seconds'),
]

View File

@ -15,20 +15,23 @@
"""
iRMC Power Driver using the Base Server Profile
"""
from ironic_lib import metrics_utils
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _, _LE
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LI
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conf import CONF
from ironic.drivers import base
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules.irmc import boot as irmc_boot
from ironic.drivers.modules.irmc import common as irmc_common
from ironic.drivers.modules import snmp
scci = importutils.try_import('scciclient.irmc.scci')
@ -36,37 +39,158 @@ LOG = logging.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__)
"""
SC2.mib: sc2srvCurrentBootStatus returns status of the current boot
"""
BOOT_STATUS_OID = "1.3.6.1.4.1.231.2.10.2.2.10.4.1.1.4.1"
BOOT_STATUS_VALUE = {
'error': 0,
'unknown': 1,
'off': 2,
'no-boot-cpu': 3,
'self-test': 4,
'setup': 5,
'os-boot': 6,
'diagnostic-boot': 7,
'os-running': 8,
'diagnostic-running': 9,
'os-shutdown': 10,
'diagnostic-shutdown': 11,
'reset': 12
}
BOOT_STATUS = {v: k for k, v in BOOT_STATUS_VALUE.items()}
if scci:
STATES_MAP = {states.POWER_OFF: scci.POWER_OFF,
states.POWER_ON: scci.POWER_ON,
states.REBOOT: scci.POWER_RESET}
states.REBOOT: scci.POWER_RESET,
states.SOFT_REBOOT: scci.POWER_SOFT_CYCLE,
states.SOFT_POWER_OFF: scci.POWER_SOFT_OFF}
def _set_power_state(task, target_state):
"""Turns the server power on/off or do a reboot.
def _is_expected_power_state(target_state, boot_status_value):
"""Predicate if target power state and boot status values match.
:param target_state: Target power state.
:param boot_status_value: SNMP BOOT_STATUS_VALUE.
:returns: True if expected power state, otherwise Flase.
"""
if (target_state == states.SOFT_POWER_OFF and
boot_status_value in (BOOT_STATUS_VALUE['unknown'],
BOOT_STATUS_VALUE['off'])):
return True
elif (target_state == states.SOFT_REBOOT and
boot_status_value == BOOT_STATUS_VALUE['os-running']):
return True
return False
def _wait_power_state(task, target_state, timeout=None):
"""Wait for having changed to the target power state.
:param task: A TaskManager instance containing the node to act on.
:raises: IRMCOperationError if the target state acknowledge failure
or SNMP failure.
"""
node = task.node
d_info = irmc_common.parse_driver_info(node)
snmp_client = snmp.SNMPClient(d_info['irmc_address'],
d_info['irmc_snmp_port'],
d_info['irmc_snmp_version'],
d_info['irmc_snmp_community'],
d_info['irmc_snmp_security'])
interval = CONF.irmc.snmp_polling_interval
retry_timeout_soft = timeout or CONF.conductor.soft_power_off_timeout
max_retry = int(retry_timeout_soft / interval)
def _wait(mutable):
mutable['boot_status_value'] = snmp_client.get(BOOT_STATUS_OID)
LOG.debug("iRMC SNMP agent of %(node_id)s returned "
"boot status value %(bootstatus)s on attempt %(times)s.",
{'node_id': node.uuid,
'bootstatus': BOOT_STATUS[mutable['boot_status_value']],
'times': mutable['times']})
if _is_expected_power_state(target_state,
mutable['boot_status_value']):
mutable['state'] = target_state
raise loopingcall.LoopingCallDone()
mutable['times'] += 1
if mutable['times'] > max_retry:
mutable['state'] = states.ERROR
raise loopingcall.LoopingCallDone()
store = {'state': None, 'times': 0, 'boot_status_value': None}
timer = loopingcall.FixedIntervalLoopingCall(_wait, store)
timer.start(interval=interval).wait()
if store['state'] == target_state:
# iRMC acknowledged the target state
node.last_error = None
node.power_state = (states.POWER_OFF
if target_state == states.SOFT_POWER_OFF
else states.POWER_ON)
node.target_power_state = states.NOSTATE
node.save()
LOG.info(_LI('iRMC successfully set node %(node_id)s '
'power state to %(bootstatus)s.'),
{'node_id': node.uuid,
'bootstatus': BOOT_STATUS[store['boot_status_value']]})
else:
# iRMC failed to acknowledge the target state
last_error = (_('iRMC returned unexpected boot status value %s') %
BOOT_STATUS[store['boot_status_value']])
node.last_error = last_error
node.power_state = states.ERROR
node.target_power_state = states.NOSTATE
node.save()
LOG.error(_LE('iRMC failed to acknowledge the target state for node '
'%(node_id)s. Error: %(last_error)s'),
{'node_id': node.uuid, 'last_error': last_error})
error = _('unexpected boot status value')
raise exception.IRMCOperationError(operation=target_state,
error=error)
def _set_power_state(task, target_state, timeout=None):
"""Turn the server power on/off or do a reboot.
:param task: a TaskManager instance containing the node to act on.
:param target_state: target state of the node.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: MissingParameterValue if some mandatory information
is missing on the node
:raises: IRMCOperationError on an error from SCCI
is missing on the node
:raises: IRMCOperationError on an error from SCCI or SNMP
"""
node = task.node
irmc_client = irmc_common.get_irmc_client(node)
if target_state in (states.POWER_ON, states.REBOOT):
if target_state in (states.POWER_ON, states.REBOOT, states.SOFT_REBOOT):
irmc_boot.attach_boot_iso_if_needed(task)
try:
irmc_client(STATES_MAP[target_state])
if target_state in (states.SOFT_REBOOT, states.SOFT_POWER_OFF):
_wait_power_state(task, target_state, timeout=timeout)
except KeyError:
msg = _("_set_power_state called with invalid power state "
"'%s'") % target_state
raise exception.InvalidParameterValue(msg)
except exception.SNMPFailure as snmp_exception:
LOG.error(_LE("iRMC failed to acknowledge the target state "
"for node %(node_id)s. Error: %(error)s"),
{'node_id': node.uuid, 'error': snmp_exception})
raise exception.IRMCOperationError(operation=target_state,
error=snmp_exception)
except scci.SCCIClientError as irmc_exception:
LOG.error(_LE("iRMC set_power_state failed to set state to %(tstate)s "
" for node %(node_id)s with error: %(error)s"),
@ -119,29 +243,45 @@ class IRMCPower(base.PowerInterface):
@METRICS.timer('IRMCPower.set_power_state')
@task_manager.require_exclusive_lock
def set_power_state(self, task, power_state):
def set_power_state(self, task, power_state, timeout=None):
"""Set the power state of the task's node.
:param task: a TaskManager instance containing the node to act on.
:param power_state: Any power state from :mod:`ironic.common.states`.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: MissingParameterValue if some mandatory information
is missing on the node
is missing on the node
:raises: IRMCOperationError if failed to set the power state.
"""
_set_power_state(task, power_state)
_set_power_state(task, power_state, timeout=timeout)
@METRICS.timer('IRMCPower.reboot')
@task_manager.require_exclusive_lock
def reboot(self, task):
def reboot(self, task, timeout=None):
"""Perform a hard reboot of the task's node.
:param task: a TaskManager instance containing the node to act on.
:param timeout: timeout (in seconds) positive integer (> 0) for any
power state. ``None`` indicates default timeout.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: IRMCOperationError if failed to set the power state.
"""
current_pstate = self.get_power_state(task)
if current_pstate == states.POWER_ON:
_set_power_state(task, states.REBOOT)
_set_power_state(task, states.REBOOT, timeout=timeout)
elif current_pstate == states.POWER_OFF:
_set_power_state(task, states.POWER_ON)
_set_power_state(task, states.POWER_ON, timeout=timeout)
@METRICS.timer('IRMCPower.get_supported_power_states')
def get_supported_power_states(self, task):
"""Get a list of the supported power states.
:param task: A TaskManager instance containing the node to act on.
currently not used.
:returns: A list with the supported power states defined
in :mod:`ironic.common.states`.
"""
return [states.POWER_ON, states.POWER_OFF, states.REBOOT,
states.SOFT_REBOOT, states.SOFT_POWER_OFF]

View File

@ -16,6 +16,8 @@
Test class for iRMC Power Driver
"""
import time
import mock
from oslo_utils import uuidutils
@ -34,8 +36,6 @@ from ironic.tests.unit.objects import utils as obj_utils
INFO_DICT = db_utils.get_test_irmc_info()
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase):
def setUp(self):
@ -47,11 +47,98 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase):
driver_info=driver_info,
instance_uuid=uuidutils.generate_uuid())
def test__is_expected_power_state(self):
target_state = states.SOFT_POWER_OFF
boot_status_value = irmc_power.BOOT_STATUS_VALUE['unknown']
self.assertTrue(irmc_power._is_expected_power_state(
target_state, boot_status_value))
target_state = states.SOFT_POWER_OFF
boot_status_value = irmc_power.BOOT_STATUS_VALUE['off']
self.assertTrue(irmc_power._is_expected_power_state(
target_state, boot_status_value))
target_state = states.SOFT_REBOOT
boot_status_value = irmc_power.BOOT_STATUS_VALUE['os-running']
self.assertTrue(irmc_power._is_expected_power_state(
target_state, boot_status_value))
target_state = states.SOFT_POWER_OFF
boot_status_value = irmc_power.BOOT_STATUS_VALUE['os-running']
self.assertFalse(irmc_power._is_expected_power_state(
target_state, boot_status_value))
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient',
spec_set=True, autospec=True)
def test__wait_power_state_soft_power_off(self, snmpclient_mock):
target_state = states.SOFT_POWER_OFF
self.config(snmp_polling_interval=1, group='irmc')
self.config(soft_power_off_timeout=3, group='conductor')
snmpclient_mock.return_value = mock.Mock(
**{'get.side_effect': [8, 8, 2]})
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
irmc_power._wait_power_state(task, target_state)
task.node.refresh()
self.assertIsNone(task.node.last_error)
self.assertEqual(states.POWER_OFF, task.node.power_state)
self.assertEqual(states.NOSTATE, task.node.target_power_state)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient',
spec_set=True, autospec=True)
def test__wait_power_state_soft_reboot(self, snmpclient_mock):
target_state = states.SOFT_REBOOT
self.config(snmp_polling_interval=1, group='irmc')
self.config(soft_power_off_timeout=3, group='conductor')
snmpclient_mock.return_value = mock.Mock(
**{'get.side_effect': [10, 6, 8]})
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
irmc_power._wait_power_state(task, target_state)
task.node.refresh()
self.assertIsNone(task.node.last_error)
self.assertEqual(states.POWER_ON, task.node.power_state)
self.assertEqual(states.NOSTATE, task.node.target_power_state)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch('ironic.drivers.modules.irmc.power.snmp.SNMPClient',
spec_set=True, autospec=True)
def test__wait_power_state_timeout(self, snmpclient_mock):
target_state = states.SOFT_POWER_OFF
self.config(snmp_polling_interval=1, group='irmc')
self.config(soft_power_off_timeout=2, group='conductor')
snmpclient_mock.return_value = mock.Mock(
**{'get.side_effect': [8, 8, 8]})
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.IRMCOperationError,
irmc_power._wait_power_state,
task,
target_state,
timeout=None)
task.node.refresh()
self.assertIsNotNone(task.node.last_error)
self.assertEqual(states.ERROR, task.node.power_state)
self.assertEqual(states.NOSTATE, task.node.target_power_state)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_power_on_ok(
self,
attach_boot_iso_if_needed_mock,
get_irmc_client_mock):
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
target_state = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid,
@ -59,21 +146,33 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase):
irmc_power._set_power_state(task, target_state)
attach_boot_iso_if_needed_mock.assert_called_once_with(task)
irmc_client.assert_called_once_with(irmc_power.scci.POWER_ON)
self.assertFalse(_wait_power_state_mock.called)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
def test__set_power_state_power_off_ok(self,
get_irmc_client_mock):
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
target_state = states.POWER_OFF
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
irmc_power._set_power_state(task, target_state)
irmc_client.assert_called_once_with(irmc_power.scci.POWER_OFF)
self.assertFalse(_wait_power_state_mock.called)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_power_reboot_ok(
def test__set_power_state_reboot_ok(
self,
attach_boot_iso_if_needed_mock,
get_irmc_client_mock):
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
target_state = states.REBOOT
with task_manager.acquire(self.context, self.node.uuid,
@ -81,18 +180,72 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase):
irmc_power._set_power_state(task, target_state)
attach_boot_iso_if_needed_mock.assert_called_once_with(task)
irmc_client.assert_called_once_with(irmc_power.scci.POWER_RESET)
self.assertFalse(_wait_power_state_mock.called)
def test__set_power_state_invalid_target_state(self,
get_irmc_client_mock):
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_soft_reboot_ok(
self,
attach_boot_iso_if_needed_mock,
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
target_state = states.SOFT_REBOOT
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
irmc_power._set_power_state(task, target_state)
attach_boot_iso_if_needed_mock.assert_called_once_with(task)
irmc_client.assert_called_once_with(irmc_power.scci.POWER_SOFT_CYCLE)
_wait_power_state_mock.assert_called_once_with(task, target_state,
timeout=None)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_soft_power_off_ok(self,
attach_boot_iso_if_needed_mock,
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
target_state = states.SOFT_POWER_OFF
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
irmc_power._set_power_state(task, target_state)
self.assertFalse(attach_boot_iso_if_needed_mock.called)
irmc_client.assert_called_once_with(irmc_power.scci.POWER_SOFT_OFF)
_wait_power_state_mock.assert_called_once_with(task, target_state,
timeout=None)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_invalid_target_state(
self,
attach_boot_iso_if_needed_mock,
_wait_power_state_mock):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.InvalidParameterValue,
irmc_power._set_power_state,
task,
states.ERROR)
self.assertFalse(attach_boot_iso_if_needed_mock.called)
self.assertFalse(_wait_power_state_mock.called)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_common, 'get_irmc_client', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_scci_exception(self,
get_irmc_client_mock):
attach_boot_iso_if_needed_mock,
get_irmc_client_mock,
_wait_power_state_mock):
irmc_client = get_irmc_client_mock.return_value
irmc_client.side_effect = Exception()
irmc_power.scci.SCCIClientError = Exception
@ -103,6 +256,30 @@ class IRMCPowerInternalMethodsTestCase(db_base.DbTestCase):
irmc_power._set_power_state,
task,
states.POWER_ON)
attach_boot_iso_if_needed_mock.assert_called_once_with(
task)
self.assertFalse(_wait_power_state_mock.called)
@mock.patch.object(irmc_power, '_wait_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_boot, 'attach_boot_iso_if_needed')
def test__set_power_state_snmp_exception(self,
attach_boot_iso_if_needed_mock,
_wait_power_state_mock):
target_state = states.SOFT_REBOOT
_wait_power_state_mock.side_effect = exception.SNMPFailure(
"fake exception")
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.IRMCOperationError,
irmc_power._set_power_state,
task,
target_state)
attach_boot_iso_if_needed_mock.assert_called_once_with(
task)
_wait_power_state_mock.assert_called_once_with(
task, target_state, timeout=None)
class IRMCPowerTestCase(db_base.DbTestCase):
@ -158,7 +335,19 @@ class IRMCPowerTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.set_power_state(task, states.POWER_ON)
mock_set_power.assert_called_once_with(task, states.POWER_ON)
mock_set_power.assert_called_once_with(task, states.POWER_ON,
timeout=None)
@mock.patch.object(irmc_power, '_set_power_state', spec_set=True,
autospec=True)
def test_set_power_state_timeout(self, mock_set_power):
mock_set_power.return_value = states.POWER_ON
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.set_power_state(task, states.POWER_ON,
timeout=2)
mock_set_power.assert_called_once_with(task, states.POWER_ON,
timeout=2)
@mock.patch.object(irmc_power, '_set_power_state', spec_set=True,
autospec=True)
@ -171,7 +360,22 @@ class IRMCPowerTestCase(db_base.DbTestCase):
task.driver.power.reboot(task)
mock_get_power.assert_called_once_with(
task.driver.power, task)
mock_set_power.assert_called_once_with(task, states.REBOOT)
mock_set_power.assert_called_once_with(task, states.REBOOT,
timeout=None)
@mock.patch.object(irmc_power, '_set_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True,
autospec=True)
def test_reboot_reboot_timeout(self, mock_get_power, mock_set_power):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
mock_get_power.return_value = states.POWER_ON
task.driver.power.reboot(task, timeout=2)
mock_get_power.assert_called_once_with(
task.driver.power, task)
mock_set_power.assert_called_once_with(task, states.REBOOT,
timeout=2)
@mock.patch.object(irmc_power, '_set_power_state', spec_set=True,
autospec=True)
@ -184,4 +388,19 @@ class IRMCPowerTestCase(db_base.DbTestCase):
task.driver.power.reboot(task)
mock_get_power.assert_called_once_with(
task.driver.power, task)
mock_set_power.assert_called_once_with(task, states.POWER_ON)
mock_set_power.assert_called_once_with(task, states.POWER_ON,
timeout=None)
@mock.patch.object(irmc_power, '_set_power_state', spec_set=True,
autospec=True)
@mock.patch.object(irmc_power.IRMCPower, 'get_power_state', spec_set=True,
autospec=True)
def test_reboot_power_on_timeout(self, mock_get_power, mock_set_power):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
mock_get_power.return_value = states.POWER_OFF
task.driver.power.reboot(task, timeout=2)
mock_get_power.assert_called_once_with(
task.driver.power, task)
mock_set_power.assert_called_once_with(task, states.POWER_ON,
timeout=2)

View File

@ -113,6 +113,8 @@ SCCICLIENT_IRMC_SCCI_SPEC = (
'POWER_OFF',
'POWER_ON',
'POWER_RESET',
'POWER_SOFT_CYCLE',
'POWER_SOFT_OFF',
'MOUNT_CD',
'UNMOUNT_CD',
'MOUNT_FD',

View File

@ -0,0 +1,4 @@
---
features:
- Adds support for ``soft reboot`` and ``soft power off`` to
iRMC driver.