Generic power interface for soft reboot and soft power off
This patch updates the generic power interface to support SOFT_REBOOT and SOFT_POWER_OFF. And also it introduces "timeout" optional parameter for all power operations. Partial-Bug: #1526226 Change-Id: I1c9bbd1f11f6a8565607c874b3c99aa10eeb62a5
This commit is contained in:
parent
480d5be023
commit
f15d5b9a37
@ -2,6 +2,12 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
**1.27** (Ocata)
|
||||
|
||||
Add ``soft rebooting`` and ``soft power off`` as possible values
|
||||
for the ``target`` field of the power state change payload, and
|
||||
also add ``timeout`` field to it.
|
||||
|
||||
**1.26** (Ocata)
|
||||
|
||||
Add portgroup ``mode`` and ``properties`` fields.
|
||||
|
@ -1013,6 +1013,12 @@
|
||||
# disable timeout. (integer value)
|
||||
#clean_callback_timeout = 1800
|
||||
|
||||
# Timeout (in seconds) of soft reboot and soft power off
|
||||
# operation. This value always has to be positive. (integer
|
||||
# value)
|
||||
# Minimum value: 1
|
||||
#soft_power_off_timeout = 600
|
||||
|
||||
|
||||
[console]
|
||||
|
||||
|
@ -102,6 +102,12 @@ PROVISION_ACTION_STATES = (ir_states.VERBS['manage'],
|
||||
|
||||
_NODES_CONTROLLER_RESERVED_WORDS = None
|
||||
|
||||
ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
|
||||
ir_states.POWER_OFF,
|
||||
ir_states.REBOOT,
|
||||
ir_states.SOFT_REBOOT,
|
||||
ir_states.SOFT_POWER_OFF)
|
||||
|
||||
|
||||
def get_nodes_controller_reserved_names():
|
||||
global _NODES_CONTROLLER_RESERVED_WORDS
|
||||
@ -434,16 +440,22 @@ class NodeStatesController(rest.RestController):
|
||||
|
||||
@METRICS.timer('NodeStatesController.power')
|
||||
@expose.expose(None, types.uuid_or_name, wtypes.text,
|
||||
wtypes.IntegerType(minimum=1),
|
||||
status_code=http_client.ACCEPTED)
|
||||
def power(self, node_ident, target):
|
||||
def power(self, node_ident, target, timeout=None):
|
||||
"""Set the power state of the node.
|
||||
|
||||
:param node_ident: the UUID or logical name of a node.
|
||||
:param target: The desired power state of the node.
|
||||
:param timeout: timeout (in seconds) positive integer (> 0) for any
|
||||
power state. ``None`` indicates to use default timeout.
|
||||
:raises: ClientSideError (HTTP 409) if a power operation is
|
||||
already in progress.
|
||||
:raises: InvalidStateRequested (HTTP 400) if the requested target
|
||||
state is not valid or if the node is in CLEANING state.
|
||||
:raises: NotAcceptable (HTTP 406) for soft reboot, soft power off or
|
||||
timeout parameter, if requested version of the API is less than 1.27.
|
||||
:raises: Invalid (HTTP 400) if timeout value is less than 1.
|
||||
|
||||
"""
|
||||
cdict = pecan.request.context.to_policy_values()
|
||||
@ -454,9 +466,16 @@ class NodeStatesController(rest.RestController):
|
||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||
|
||||
if target not in [ir_states.POWER_ON,
|
||||
ir_states.POWER_OFF,
|
||||
ir_states.REBOOT]:
|
||||
if ((target in [ir_states.SOFT_REBOOT, ir_states.SOFT_POWER_OFF] or
|
||||
timeout) and not api_utils.allow_soft_power_off()):
|
||||
raise exception.NotAcceptable()
|
||||
# FIXME(naohirot): This check is workaround because
|
||||
# wtypes.IntegerType(minimum=1) is not effective
|
||||
if timeout is not None and timeout < 1:
|
||||
raise exception.Invalid(
|
||||
_("timeout has to be positive integer"))
|
||||
|
||||
if target not in ALLOWED_TARGET_POWER_STATES:
|
||||
raise exception.InvalidStateRequested(
|
||||
action=target, node=node_ident,
|
||||
state=rpc_node.power_state)
|
||||
@ -470,7 +489,8 @@ class NodeStatesController(rest.RestController):
|
||||
|
||||
pecan.request.rpcapi.change_node_power_state(pecan.request.context,
|
||||
rpc_node.uuid, target,
|
||||
topic)
|
||||
timeout=timeout,
|
||||
topic=topic)
|
||||
# Set the HTTP Location Header
|
||||
url_args = '/'.join([node_ident, 'states'])
|
||||
pecan.response.location = link.build_url('nodes', url_args)
|
||||
|
@ -369,6 +369,15 @@ def allow_raid_config():
|
||||
return pecan.request.version.minor >= versions.MINOR_12_RAID_CONFIG
|
||||
|
||||
|
||||
def allow_soft_power_off():
|
||||
"""Check if Soft Power Off is allowed for the node.
|
||||
|
||||
Version 1.27 of the API allows Soft Power Off, including Soft Reboot, for
|
||||
the node.
|
||||
"""
|
||||
return pecan.request.version.minor >= versions.MINOR_27_SOFT_POWER_OFF
|
||||
|
||||
|
||||
def allow_links_node_states_and_driver_properties():
|
||||
"""Check if links are displayable.
|
||||
|
||||
|
@ -57,6 +57,7 @@ BASE_VERSION = 1
|
||||
# Add port.portgroup_uuid field.
|
||||
# v1.25: Add possibility to unset chassis_uuid from node.
|
||||
# v1.26: Add portgroup.mode and portgroup.properties.
|
||||
# v1.27: Add soft reboot, soft power off and timeout.
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -85,11 +86,12 @@ MINOR_23_PORTGROUPS = 23
|
||||
MINOR_24_PORTGROUPS_SUBCONTROLLERS = 24
|
||||
MINOR_25_UNSET_CHASSIS_UUID = 25
|
||||
MINOR_26_PORTGROUP_MODE_PROPERTIES = 26
|
||||
MINOR_27_SOFT_POWER_OFF = 27
|
||||
|
||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
||||
# what the version has changed.
|
||||
MINOR_MAX_VERSION = MINOR_26_PORTGROUP_MODE_PROPERTIES
|
||||
MINOR_MAX_VERSION = MINOR_27_SOFT_POWER_OFF
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -212,6 +212,12 @@ POWER_OFF = 'power off'
|
||||
REBOOT = 'rebooting'
|
||||
""" Node is rebooting. """
|
||||
|
||||
SOFT_REBOOT = 'soft rebooting'
|
||||
""" Node is rebooting gracefully. """
|
||||
|
||||
SOFT_POWER_OFF = 'soft power off'
|
||||
""" Node is in the process of soft power off. """
|
||||
|
||||
|
||||
#####################
|
||||
# State machine model
|
||||
|
@ -83,7 +83,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
"""Ironic Conductor manager main class."""
|
||||
|
||||
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
|
||||
RPC_API_VERSION = '1.38'
|
||||
RPC_API_VERSION = '1.39'
|
||||
|
||||
target = messaging.Target(version=RPC_API_VERSION)
|
||||
|
||||
@ -179,7 +179,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
@messaging.expected_exceptions(exception.InvalidParameterValue,
|
||||
exception.NoFreeConductorWorker,
|
||||
exception.NodeLocked)
|
||||
def change_node_power_state(self, context, node_id, new_state):
|
||||
def change_node_power_state(self, context, node_id, new_state,
|
||||
timeout=None):
|
||||
"""RPC method to encapsulate changes to a node's state.
|
||||
|
||||
Perform actions such as power on, power off. The validation is
|
||||
@ -191,8 +192,12 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
:param context: an admin context.
|
||||
:param node_id: the id or uuid of a node.
|
||||
:param new_state: the desired power state of the node.
|
||||
:param timeout: timeout (in seconds) positive integer (> 0) for any
|
||||
power state. ``None`` indicates to use default timeout.
|
||||
:raises: NoFreeConductorWorker when there is no free worker to start
|
||||
async task.
|
||||
:raises: InvalidParameterValue
|
||||
:raises: MissingParameterValue
|
||||
|
||||
"""
|
||||
LOG.debug("RPC change_node_power_state called for node %(node)s. "
|
||||
@ -202,19 +207,38 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
with task_manager.acquire(context, node_id, shared=False,
|
||||
purpose='changing node power state') as task:
|
||||
task.driver.power.validate(task)
|
||||
|
||||
if (new_state not in
|
||||
task.driver.power.get_supported_power_states(task)):
|
||||
# FIXME(naohirot):
|
||||
# After driver composition, we should print power interface
|
||||
# name here instead of driver.
|
||||
raise exception.InvalidParameterValue(
|
||||
_('The driver %(driver)s does not support the power state,'
|
||||
' %(state)s') %
|
||||
{'driver': task.node.driver, 'state': new_state})
|
||||
|
||||
if new_state in (states.SOFT_REBOOT, states.SOFT_POWER_OFF):
|
||||
power_timeout = (timeout or
|
||||
CONF.conductor.soft_power_off_timeout)
|
||||
else:
|
||||
power_timeout = timeout
|
||||
|
||||
# Set the target_power_state and clear any last_error, since we're
|
||||
# starting a new operation. This will expose to other processes
|
||||
# and clients that work is in progress.
|
||||
if new_state == states.REBOOT:
|
||||
if new_state in (states.POWER_ON, states.REBOOT,
|
||||
states.SOFT_REBOOT):
|
||||
task.node.target_power_state = states.POWER_ON
|
||||
else:
|
||||
task.node.target_power_state = new_state
|
||||
task.node.target_power_state = states.POWER_OFF
|
||||
|
||||
task.node.last_error = None
|
||||
task.node.save()
|
||||
task.set_spawn_error_hook(utils.power_state_error_handler,
|
||||
task.node, task.node.power_state)
|
||||
task.spawn_after(self._spawn_worker, utils.node_power_action,
|
||||
task, new_state)
|
||||
task, new_state, timeout=power_timeout)
|
||||
|
||||
@METRICS.timer('ConductorManager.vendor_passthru')
|
||||
@messaging.expected_exceptions(exception.NoFreeConductorWorker,
|
||||
|
@ -85,11 +85,12 @@ class ConductorAPI(object):
|
||||
| 1.36 - Added create_node
|
||||
| 1.37 - Added destroy_volume_target and update_volume_target
|
||||
| 1.38 - Added vif_attach, vif_detach, vif_list
|
||||
| 1.39 - Added timeout optional parameter to change_node_power_state
|
||||
|
||||
"""
|
||||
|
||||
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
|
||||
RPC_API_VERSION = '1.38'
|
||||
RPC_API_VERSION = '1.39'
|
||||
|
||||
def __init__(self, topic=None):
|
||||
super(ConductorAPI, self).__init__()
|
||||
@ -179,7 +180,8 @@ class ConductorAPI(object):
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.1')
|
||||
return cctxt.call(context, 'update_node', node_obj=node_obj)
|
||||
|
||||
def change_node_power_state(self, context, node_id, new_state, topic=None):
|
||||
def change_node_power_state(self, context, node_id, new_state,
|
||||
topic=None, timeout=None):
|
||||
"""Change a node's power state.
|
||||
|
||||
Synchronously, acquire lock and start the conductor background task
|
||||
@ -188,14 +190,16 @@ class ConductorAPI(object):
|
||||
:param context: request context.
|
||||
:param node_id: node id or uuid.
|
||||
:param new_state: one of ironic.common.states power state values
|
||||
:param timeout: timeout (in seconds) positive integer (> 0) for any
|
||||
power state. ``None`` indicates to use default timeout.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NoFreeConductorWorker when there is no free worker to start
|
||||
async task.
|
||||
|
||||
"""
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.6')
|
||||
cctxt = self.client.prepare(topic=topic or self.topic, version='1.39')
|
||||
return cctxt.call(context, 'change_node_power_state', node_id=node_id,
|
||||
new_state=new_state)
|
||||
new_state=new_state, timeout=timeout)
|
||||
|
||||
def vendor_passthru(self, context, node_id, driver_method, http_method,
|
||||
info, topic=None):
|
||||
|
@ -15,6 +15,7 @@
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import reflection
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _, _LE, _LI, _LW
|
||||
@ -67,15 +68,15 @@ def node_set_boot_device(task, device, persistent=False):
|
||||
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def node_power_action(task, new_state):
|
||||
def node_power_action(task, new_state, timeout=None):
|
||||
"""Change power state or reset for a node.
|
||||
|
||||
Perform the requested power action if the transition is required.
|
||||
|
||||
:param task: a TaskManager instance containing the node to act on.
|
||||
:param new_state: Any power state from ironic.common.states. If the
|
||||
state is 'REBOOT' then a reboot will be attempted, otherwise
|
||||
the node power state is directly set to 'state'.
|
||||
:param new_state: Any power state from ironic.common.states.
|
||||
:param timeout: timeout (in seconds) positive integer (> 0) for any
|
||||
power state. ``None`` indicates to use default timeout.
|
||||
:raises: InvalidParameterValue when the wrong state is specified
|
||||
or the wrong driver info is specified.
|
||||
:raises: other exceptions by the node's power driver if something
|
||||
@ -86,50 +87,63 @@ def node_power_action(task, new_state):
|
||||
task, fields.NotificationLevel.INFO, fields.NotificationStatus.START,
|
||||
new_state)
|
||||
node = task.node
|
||||
target_state = states.POWER_ON if new_state == states.REBOOT else new_state
|
||||
|
||||
if new_state != states.REBOOT:
|
||||
try:
|
||||
curr_state = task.driver.power.get_power_state(task)
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
node['last_error'] = _(
|
||||
"Failed to change power state to '%(target)s'. "
|
||||
"Error: %(error)s") % {'target': new_state, 'error': e}
|
||||
node['target_power_state'] = states.NOSTATE
|
||||
node.save()
|
||||
notify_utils.emit_power_set_notification(
|
||||
task, fields.NotificationLevel.ERROR,
|
||||
fields.NotificationStatus.ERROR, new_state)
|
||||
if new_state in (states.POWER_ON, states.REBOOT, states.SOFT_REBOOT):
|
||||
target_state = states.POWER_ON
|
||||
elif new_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
|
||||
target_state = states.POWER_OFF
|
||||
else:
|
||||
target_state = None
|
||||
|
||||
if curr_state == new_state:
|
||||
# Neither the ironic service nor the hardware has erred. The
|
||||
# node is, for some reason, already in the requested state,
|
||||
# though we don't know why. eg, perhaps the user previously
|
||||
# requested the node POWER_ON, the network delayed those IPMI
|
||||
# packets, and they are trying again -- but the node finally
|
||||
# responds to the first request, and so the second request
|
||||
# gets to this check and stops.
|
||||
# This isn't an error, so we'll clear last_error field
|
||||
# (from previous operation), log a warning, and return.
|
||||
node['last_error'] = None
|
||||
# NOTE(dtantsur): under rare conditions we can get out of sync here
|
||||
node['power_state'] = new_state
|
||||
def _not_going_to_change():
|
||||
# Neither the ironic service nor the hardware has erred. The
|
||||
# node is, for some reason, already in the requested state,
|
||||
# though we don't know why. eg, perhaps the user previously
|
||||
# requested the node POWER_ON, the network delayed those IPMI
|
||||
# packets, and they are trying again -- but the node finally
|
||||
# responds to the first request, and so the second request
|
||||
# gets to this check and stops.
|
||||
# This isn't an error, so we'll clear last_error field
|
||||
# (from previous operation), log a warning, and return.
|
||||
node['last_error'] = None
|
||||
# NOTE(dtantsur): under rare conditions we can get out of sync here
|
||||
node['power_state'] = curr_state
|
||||
node['target_power_state'] = states.NOSTATE
|
||||
node.save()
|
||||
notify_utils.emit_power_set_notification(
|
||||
task, fields.NotificationLevel.INFO,
|
||||
fields.NotificationStatus.END, new_state)
|
||||
LOG.warning(_LW("Not going to change node %(node)s power "
|
||||
"state because current state = requested state "
|
||||
"= '%(state)s'."),
|
||||
{'node': node.uuid, 'state': curr_state})
|
||||
|
||||
try:
|
||||
curr_state = task.driver.power.get_power_state(task)
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
node['last_error'] = _(
|
||||
"Failed to change power state to '%(target)s'. "
|
||||
"Error: %(error)s") % {'target': new_state, 'error': e}
|
||||
node['target_power_state'] = states.NOSTATE
|
||||
node.save()
|
||||
notify_utils.emit_power_set_notification(
|
||||
task, fields.NotificationLevel.INFO,
|
||||
fields.NotificationStatus.END, new_state)
|
||||
LOG.warning(_LW("Not going to change node %(node)s power "
|
||||
"state because current state = requested state "
|
||||
"= '%(state)s'."),
|
||||
{'node': node.uuid, 'state': curr_state})
|
||||
return
|
||||
task, fields.NotificationLevel.ERROR,
|
||||
fields.NotificationStatus.ERROR, new_state)
|
||||
|
||||
if curr_state == states.ERROR:
|
||||
# be optimistic and continue action
|
||||
LOG.warning(_LW("Driver returns ERROR power state for node %s."),
|
||||
node.uuid)
|
||||
if curr_state == states.POWER_ON:
|
||||
if new_state == states.POWER_ON:
|
||||
_not_going_to_change()
|
||||
return
|
||||
elif curr_state == states.POWER_OFF:
|
||||
if new_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
|
||||
_not_going_to_change()
|
||||
return
|
||||
else:
|
||||
# if curr_state == states.ERROR:
|
||||
# be optimistic and continue action
|
||||
LOG.warning(_LW("Driver returns ERROR power state for node %s."),
|
||||
node.uuid)
|
||||
|
||||
# Set the target_power_state and clear any last_error, if we're
|
||||
# starting a new operation. This will expose to other processes
|
||||
@ -142,15 +156,37 @@ def node_power_action(task, new_state):
|
||||
# take power action
|
||||
try:
|
||||
if new_state != states.REBOOT:
|
||||
task.driver.power.set_power_state(task, new_state)
|
||||
if ('timeout' in reflection.get_signature(
|
||||
task.driver.power.set_power_state).parameters):
|
||||
task.driver.power.set_power_state(task, new_state,
|
||||
timeout=timeout)
|
||||
else:
|
||||
# FIXME(naohirot):
|
||||
# After driver composition, we should print power interface
|
||||
# name here instead of driver.
|
||||
LOG.warning(
|
||||
_LW("The set_power_state method of %s(driver_name)s "
|
||||
"doesn't support 'timeout' parameter."),
|
||||
{'driver_name': node.driver})
|
||||
task.driver.power.set_power_state(task, new_state)
|
||||
else:
|
||||
task.driver.power.reboot(task)
|
||||
if ('timeout' in reflection.get_signature(
|
||||
task.driver.power.reboot).parameters):
|
||||
task.driver.power.reboot(task, timeout=timeout)
|
||||
else:
|
||||
LOG.warning(_LW("The reboot method of %s(driver_name)s "
|
||||
"doesn't support 'timeout' parameter."),
|
||||
{'driver_name': node.driver})
|
||||
task.driver.power.reboot(task)
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
node['target_power_state'] = states.NOSTATE
|
||||
node['last_error'] = _(
|
||||
"Failed to change power state to '%(target)s'. "
|
||||
"Error: %(error)s") % {'target': target_state, 'error': e}
|
||||
"Failed to change power state to '%(target_state)s' "
|
||||
"by '%(new_state)s'. Error: %(error)s") % {
|
||||
'target_state': target_state,
|
||||
'new_state': new_state,
|
||||
'error': e}
|
||||
node.save()
|
||||
notify_utils.emit_power_set_notification(
|
||||
task, fields.NotificationLevel.ERROR,
|
||||
@ -164,8 +200,10 @@ def node_power_action(task, new_state):
|
||||
task, fields.NotificationLevel.INFO, fields.NotificationStatus.END,
|
||||
new_state)
|
||||
LOG.info(_LI('Successfully set node %(node)s power state to '
|
||||
'%(state)s.'),
|
||||
{'node': node.uuid, 'state': target_state})
|
||||
'%(target_state)s by %(new_state)s.'),
|
||||
{'node': node.uuid,
|
||||
'target_state': target_state,
|
||||
'new_state': new_state})
|
||||
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
|
@ -131,6 +131,11 @@ opts = [
|
||||
'ramdisk doing the cleaning. If the timeout is reached '
|
||||
'the node will be put in the "clean failed" provision '
|
||||
'state. Set to 0 to disable timeout.')),
|
||||
cfg.IntOpt('soft_power_off_timeout',
|
||||
default=600,
|
||||
min=1,
|
||||
help=_('Timeout (in seconds) of soft reboot and soft power '
|
||||
'off operation. This value always has to be positive.')),
|
||||
]
|
||||
|
||||
|
||||
|
@ -31,6 +31,7 @@ import six
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _, _LE, _LW
|
||||
from ironic.common import raid
|
||||
from ironic.common import states
|
||||
from ironic.drivers.modules.network import common as net_common
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -493,25 +494,38 @@ class PowerInterface(BaseInterface):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
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 to use default timeout.
|
||||
:raises: MissingParameterValue if a required parameter is missing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reboot(self, task):
|
||||
def reboot(self, task, timeout=None):
|
||||
"""Perform a hard reboot of the task's node.
|
||||
|
||||
Drivers are expected to properly handle case when node is powered off
|
||||
by powering it on.
|
||||
|
||||
: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 to use default timeout.
|
||||
:raises: MissingParameterValue if a required parameter is missing.
|
||||
"""
|
||||
|
||||
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.
|
||||
:returns: A list with the supported power states defined
|
||||
in :mod:`ironic.common.states`.
|
||||
"""
|
||||
return [states.POWER_ON, states.POWER_OFF, states.REBOOT]
|
||||
|
||||
|
||||
class ConsoleInterface(BaseInterface):
|
||||
"""Interface for console-related actions."""
|
||||
|
@ -78,6 +78,14 @@ class FakeDriver(base.BaseDriver):
|
||||
self.raid = fake.FakeRAID()
|
||||
|
||||
|
||||
class FakeSoftPowerDriver(FakeDriver):
|
||||
"""Example implementation of a Driver."""
|
||||
|
||||
def __init__(self):
|
||||
super(FakeSoftPowerDriver, self).__init__()
|
||||
self.power = fake.FakeSoftPower()
|
||||
|
||||
|
||||
class FakeIPMIToolDriver(base.BaseDriver):
|
||||
"""Example implementation of a Driver."""
|
||||
|
||||
|
@ -46,7 +46,7 @@ class FakePower(base.PowerInterface):
|
||||
def set_power_state(self, task, power_state):
|
||||
if power_state not in [states.POWER_ON, states.POWER_OFF]:
|
||||
raise exception.InvalidParameterValue(
|
||||
_("set_power_state called with an invalid power"
|
||||
_("set_power_state called with an invalid power "
|
||||
"state: %s.") % power_state)
|
||||
task.node.power_state = power_state
|
||||
|
||||
@ -54,6 +54,25 @@ class FakePower(base.PowerInterface):
|
||||
pass
|
||||
|
||||
|
||||
class FakeSoftPower(FakePower):
|
||||
"""Example implementation of a simple soft power operations."""
|
||||
|
||||
def set_power_state(self, task, power_state, timeout=None):
|
||||
if power_state not in [states.POWER_ON, states.POWER_OFF,
|
||||
states.SOFT_REBOOT, states.SOFT_POWER_OFF]:
|
||||
raise exception.InvalidParameterValue(
|
||||
_("set_power_state called with an invalid power "
|
||||
"state: %s.") % power_state)
|
||||
task.node.power_state = power_state
|
||||
|
||||
def reboot(self, task, timeout=None):
|
||||
pass
|
||||
|
||||
def get_supported_power_states(self, task):
|
||||
return [states.POWER_ON, states.POWER_OFF, states.REBOOT,
|
||||
states.SOFT_REBOOT, states.SOFT_POWER_OFF]
|
||||
|
||||
|
||||
class FakeBoot(base.BootInterface):
|
||||
"""Example implementation of a simple boot interface."""
|
||||
|
||||
|
@ -2345,21 +2345,120 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
self.mock_dnih = p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
def test_power_state(self):
|
||||
response = self.put_json('/nodes/%s/states/power' % self.node.uuid,
|
||||
{'target': states.POWER_ON})
|
||||
def _test_power_state_success(self, target_state, timeout, api_version):
|
||||
if timeout is None:
|
||||
body = {'target': target_state}
|
||||
else:
|
||||
body = {'target': target_state, 'timeout': timeout}
|
||||
|
||||
if api_version is None:
|
||||
response = self.put_json(
|
||||
'/nodes/%s/states/power' % self.node.uuid, body)
|
||||
else:
|
||||
response = self.put_json(
|
||||
'/nodes/%s/states/power' % self.node.uuid, body,
|
||||
headers={api_base.Version.string: api_version})
|
||||
|
||||
self.assertEqual(http_client.ACCEPTED, response.status_code)
|
||||
self.assertEqual(b'', response.body)
|
||||
self.mock_cnps.assert_called_once_with(mock.ANY,
|
||||
self.node.uuid,
|
||||
states.POWER_ON,
|
||||
'test-topic')
|
||||
target_state,
|
||||
timeout=timeout,
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(response.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.uuid
|
||||
self.assertEqual(urlparse.urlparse(response.location).path,
|
||||
expected_location)
|
||||
|
||||
def _test_power_state_failure(self, target_state, http_status_code,
|
||||
timeout, api_version):
|
||||
if timeout is None:
|
||||
body = {'target': target_state}
|
||||
else:
|
||||
body = {'target': target_state, 'timeout': timeout}
|
||||
|
||||
if api_version is None:
|
||||
response = self.put_json(
|
||||
'/nodes/%s/states/power' % self.node.uuid, body,
|
||||
expect_errors=True)
|
||||
else:
|
||||
response = self.put_json(
|
||||
'/nodes/%s/states/power' % self.node.uuid, body,
|
||||
headers={api_base.Version.string: api_version},
|
||||
expect_errors=True)
|
||||
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(http_status_code, response.status_code)
|
||||
self.assertTrue(response.json['error_message'])
|
||||
|
||||
def test_power_state_power_on_no_timeout_no_ver(self):
|
||||
self._test_power_state_success(states.POWER_ON, None, None)
|
||||
|
||||
def test_power_state_power_on_no_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_success(states.POWER_ON, None, "1.27")
|
||||
|
||||
def test_power_state_power_on_no_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_success(states.POWER_ON, None, "1.26")
|
||||
|
||||
def test_power_state_power_on_valid_timeout_no_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.POWER_ON, http_client.NOT_ACCEPTABLE, 2, None)
|
||||
|
||||
def test_power_state_power_on_valid_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_success(states.POWER_ON, 2, "1.27")
|
||||
|
||||
def test_power_state_power_on_valid_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.POWER_ON, http_client.NOT_ACCEPTABLE, 2, "1.26")
|
||||
|
||||
def test_power_state_power_on_invalid_timeout_no_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.POWER_ON, http_client.BAD_REQUEST, 0, None)
|
||||
|
||||
def test_power_state_power_on_invalid_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.POWER_ON, http_client.BAD_REQUEST, 0, "1.27")
|
||||
|
||||
def test_power_state_power_on_invalid_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.POWER_ON, http_client.BAD_REQUEST, 0, "1.26")
|
||||
|
||||
def test_power_state_soft_power_off_no_timeout_no_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, None, None)
|
||||
|
||||
def test_power_state_soft_power_off_no_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_success(states.SOFT_POWER_OFF, None, "1.27")
|
||||
|
||||
def test_power_state_soft_power_off_no_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, None, "1.26")
|
||||
|
||||
def test_power_state_soft_power_off_valid_timeout_no_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 2, None)
|
||||
|
||||
def test_power_state_soft_power_off_valid_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_success(states.SOFT_POWER_OFF, 2, "1.27")
|
||||
|
||||
def test_power_state_soft_power_off_valid_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 2, "1.26")
|
||||
|
||||
def test_power_state_soft_power_off_invalid_timeout_no_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 0, None)
|
||||
|
||||
def test_power_state_soft_power_off_invalid_timeout_valid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.BAD_REQUEST, 0, "1.27")
|
||||
|
||||
def test_power_state_soft_power_off_invalid_timeout_invalid_soft_ver(self):
|
||||
self._test_power_state_failure(
|
||||
states.SOFT_POWER_OFF, http_client.NOT_ACCEPTABLE, 0, "1.26")
|
||||
|
||||
def test_power_state_by_name_unsupported(self):
|
||||
response = self.put_json('/nodes/%s/states/power' % self.node.name,
|
||||
{'target': states.POWER_ON},
|
||||
@ -2375,7 +2474,8 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
self.mock_cnps.assert_called_once_with(mock.ANY,
|
||||
self.node.uuid,
|
||||
states.POWER_ON,
|
||||
'test-topic')
|
||||
timeout=None,
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(response.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.name
|
||||
|
@ -85,6 +85,36 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
# background task's link callback.
|
||||
self.assertIsNone(node.reservation)
|
||||
|
||||
def test_change_node_power_state_soft_power_off_timeout(self):
|
||||
# Test change_node_power_state with timeout optional parameter
|
||||
# including integration with conductor.utils.node_power_action and
|
||||
# lower.
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_soft_power")
|
||||
self.driver = driver_factory.get_driver("fake_soft_power")
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
driver='fake_soft_power',
|
||||
power_state=states.POWER_ON)
|
||||
self._start_service()
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_power_state') as get_power_mock:
|
||||
get_power_mock.return_value = states.POWER_ON
|
||||
|
||||
self.service.change_node_power_state(self.context,
|
||||
node.uuid,
|
||||
states.SOFT_POWER_OFF,
|
||||
timeout=2)
|
||||
self._stop_service()
|
||||
|
||||
get_power_mock.assert_called_once_with(mock.ANY)
|
||||
node.refresh()
|
||||
self.assertEqual(states.POWER_OFF, node.power_state)
|
||||
self.assertIsNone(node.target_power_state)
|
||||
self.assertIsNone(node.last_error)
|
||||
# Verify the reservation has been cleared by
|
||||
# background task's link callback.
|
||||
self.assertIsNone(node.reservation)
|
||||
|
||||
@mock.patch.object(conductor_utils, 'node_power_action')
|
||||
def test_change_node_power_state_node_already_locked(self,
|
||||
pwr_act_mock):
|
||||
@ -135,7 +165,7 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
self.assertEqual(exception.NoFreeConductorWorker, exc.exc_info[0])
|
||||
|
||||
spawn_mock.assert_called_once_with(mock.ANY, mock.ANY,
|
||||
mock.ANY)
|
||||
mock.ANY, timeout=mock.ANY)
|
||||
node.refresh()
|
||||
self.assertEqual(initial_state, node.power_state)
|
||||
self.assertIsNone(node.target_power_state)
|
||||
@ -343,7 +373,8 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
states.POWER_ON)
|
||||
|
||||
spawn_mock.assert_called_once_with(
|
||||
conductor_utils.node_power_action, mock.ANY, states.POWER_ON)
|
||||
conductor_utils.node_power_action, mock.ANY, states.POWER_ON,
|
||||
timeout=None)
|
||||
self.assertFalse(mock_notif.called)
|
||||
|
||||
@mock.patch('ironic.objects.node.NodeSetPowerStateNotification')
|
||||
@ -381,6 +412,33 @@ class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
'baremetal.node.power_set.end',
|
||||
obj_fields.NotificationLevel.INFO)
|
||||
|
||||
def test_change_node_power_state_unsupported_state(self):
|
||||
# Test change_node_power_state where unsupported power state raises
|
||||
# an exception
|
||||
initial_state = states.POWER_ON
|
||||
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||
power_state=initial_state)
|
||||
self._start_service()
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_supported_power_states') as supported_mock:
|
||||
supported_mock.return_value = [
|
||||
states.POWER_ON, states.POWER_OFF, states.REBOOT]
|
||||
|
||||
exc = self.assertRaises(messaging.rpc.ExpectedException,
|
||||
self.service.change_node_power_state,
|
||||
self.context,
|
||||
node.uuid,
|
||||
states.SOFT_POWER_OFF)
|
||||
|
||||
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
|
||||
|
||||
node.refresh()
|
||||
supported_mock.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual(states.POWER_ON, node.power_state)
|
||||
self.assertIsNone(node.target_power_state)
|
||||
self.assertIsNone(node.last_error)
|
||||
|
||||
|
||||
@mgr_utils.mock_record_keepalive
|
||||
class CreateNodeTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
|
@ -188,7 +188,7 @@ class RPCAPITestCase(base.DbTestCase):
|
||||
def test_change_node_power_state(self):
|
||||
self._test_rpcapi('change_node_power_state',
|
||||
'call',
|
||||
version='1.6',
|
||||
version='1.39',
|
||||
node_id=self.fake_node['uuid'],
|
||||
new_state=states.POWER_ON)
|
||||
|
||||
|
@ -495,6 +495,96 @@ class NodePowerActionTestCase(base.DbTestCase):
|
||||
obj_fields.NotificationLevel.ERROR)
|
||||
|
||||
|
||||
class NodeSoftPowerActionTestCase(base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NodeSoftPowerActionTestCase, self).setUp()
|
||||
mgr_utils.mock_the_extension_manager(driver="fake_soft_power")
|
||||
self.driver = driver_factory.get_driver("fake_soft_power")
|
||||
|
||||
def test_node_power_action_power_soft_reboot(self):
|
||||
"""Test for soft reboot a node."""
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake_soft_power',
|
||||
power_state=states.POWER_ON)
|
||||
task = task_manager.TaskManager(self.context, node.uuid)
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_power_state') as get_power_mock:
|
||||
get_power_mock.return_value = states.POWER_ON
|
||||
|
||||
conductor_utils.node_power_action(task, states.SOFT_REBOOT)
|
||||
|
||||
node.refresh()
|
||||
get_power_mock.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual(states.POWER_ON, node['power_state'])
|
||||
self.assertIsNone(node['target_power_state'])
|
||||
self.assertIsNone(node['last_error'])
|
||||
|
||||
def test_node_power_action_power_soft_reboot_timeout(self):
|
||||
"""Test for soft reboot a node."""
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake_soft_power',
|
||||
power_state=states.POWER_ON)
|
||||
task = task_manager.TaskManager(self.context, node.uuid)
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_power_state') as get_power_mock:
|
||||
get_power_mock.return_value = states.POWER_ON
|
||||
|
||||
conductor_utils.node_power_action(task, states.SOFT_REBOOT,
|
||||
timeout=2)
|
||||
|
||||
node.refresh()
|
||||
get_power_mock.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual(states.POWER_ON, node['power_state'])
|
||||
self.assertIsNone(node['target_power_state'])
|
||||
self.assertIsNone(node['last_error'])
|
||||
|
||||
def test_node_power_action_soft_power_off(self):
|
||||
"""Test node_power_action to turn node soft power off."""
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake_soft_power',
|
||||
power_state=states.POWER_ON)
|
||||
task = task_manager.TaskManager(self.context, node.uuid)
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_power_state') as get_power_mock:
|
||||
get_power_mock.return_value = states.POWER_ON
|
||||
|
||||
conductor_utils.node_power_action(task, states.SOFT_POWER_OFF)
|
||||
|
||||
node.refresh()
|
||||
get_power_mock.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual(states.POWER_OFF, node['power_state'])
|
||||
self.assertIsNone(node['target_power_state'])
|
||||
self.assertIsNone(node['last_error'])
|
||||
|
||||
def test_node_power_action_soft_power_off_timeout(self):
|
||||
"""Test node_power_action to turn node soft power off."""
|
||||
node = obj_utils.create_test_node(self.context,
|
||||
uuid=uuidutils.generate_uuid(),
|
||||
driver='fake_soft_power',
|
||||
power_state=states.POWER_ON)
|
||||
task = task_manager.TaskManager(self.context, node.uuid)
|
||||
|
||||
with mock.patch.object(self.driver.power,
|
||||
'get_power_state') as get_power_mock:
|
||||
get_power_mock.return_value = states.POWER_ON
|
||||
|
||||
conductor_utils.node_power_action(task, states.SOFT_POWER_OFF,
|
||||
timeout=2)
|
||||
|
||||
node.refresh()
|
||||
get_power_mock.assert_called_once_with(mock.ANY)
|
||||
self.assertEqual(states.POWER_OFF, node['power_state'])
|
||||
self.assertIsNone(node['target_power_state'])
|
||||
self.assertIsNone(node['last_error'])
|
||||
|
||||
|
||||
class CleanupAfterTimeoutTestCase(tests_base.TestCase):
|
||||
def setUp(self):
|
||||
super(CleanupAfterTimeoutTestCase, self).setUp()
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- Support ``soft rebooting`` and ``soft power off`` requests to
|
||||
change node's power state with API version 1.27, and also
|
||||
introduce ``timeout`` optional parameter and
|
||||
``[conductor]/soft_power_off_timeout`` configuration option.
|
||||
Custom power drivers may be enhanced to support this feature.
|
@ -52,6 +52,7 @@ ironic.drivers =
|
||||
agent_vbox = ironic.drivers.agent:AgentAndVirtualBoxDriver
|
||||
agent_ucs = ironic.drivers.agent:AgentAndUcsDriver
|
||||
fake = ironic.drivers.fake:FakeDriver
|
||||
fake_soft_power = ironic.drivers.fake:FakeSoftPowerDriver
|
||||
fake_agent = ironic.drivers.fake:FakeAgentDriver
|
||||
fake_inspector = ironic.drivers.fake:FakeIPMIToolInspectorDriver
|
||||
fake_ipmitool = ironic.drivers.fake:FakeIPMIToolDriver
|
||||
|
Loading…
Reference in New Issue
Block a user