Merge "Add node provision state change notification"
This commit is contained in:
commit
2b5fdc991a
@ -187,5 +187,73 @@ prior to the correction::
|
|||||||
"publisher_id":"ironic-conductor.cond-hostname02"
|
"publisher_id":"ironic-conductor.cond-hostname02"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baremetal.node.provision_set
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
* ``baremetal.node.provision_set.start`` is emitted by the ironic-conductor
|
||||||
|
service when it begins a provision state transition. It has notification
|
||||||
|
level INFO.
|
||||||
|
|
||||||
|
* ``baremetal.node.provision_set.end`` is emitted when ironic-conductor
|
||||||
|
successfully completes a provision state transition. It has notification
|
||||||
|
level INFO.
|
||||||
|
|
||||||
|
* ``baremetal.node.provision_set.success`` is emitted when ironic-conductor
|
||||||
|
successfully changes provision state instantly, without any intermediate
|
||||||
|
work required (example is AVAILABLE to MANAGEABLE). It has notification level
|
||||||
|
INFO.
|
||||||
|
|
||||||
|
* ``baremetal.node.provision_set.error`` is emitted by ironic-conductor when it
|
||||||
|
changes provision state as result of error event processing. It has
|
||||||
|
notification level ERROR.
|
||||||
|
|
||||||
|
Here is an example payload for a notification with this event type. The
|
||||||
|
"previous_provision_state" and "previous_target_provision_state" payload fields
|
||||||
|
indicate a node's provision states before state change, "event" is the FSM
|
||||||
|
(finite state machine) event that triggered the state change::
|
||||||
|
|
||||||
|
{
|
||||||
|
"priority": "info",
|
||||||
|
"payload":{
|
||||||
|
"ironic_object.namespace":"ironic",
|
||||||
|
"ironic_object.name":"NodeSetProvisionStatePayload",
|
||||||
|
"ironic_object.version":"1.0",
|
||||||
|
"ironic_object.data":{
|
||||||
|
"clean_step": None,
|
||||||
|
"console_enabled": False,
|
||||||
|
"created_at": "2016-01-26T20:41:03+00:00",
|
||||||
|
"driver": "fake",
|
||||||
|
"extra": {},
|
||||||
|
"inspection_finished_at": None,
|
||||||
|
"inspection_started_at": None,
|
||||||
|
"instance_info": {},
|
||||||
|
"instance_uuid": None,
|
||||||
|
"last_error": None,
|
||||||
|
"maintenance": False,
|
||||||
|
"maintenance_reason": None,
|
||||||
|
"network_interface": "flat",
|
||||||
|
"name": None,
|
||||||
|
"power_state": "power off",
|
||||||
|
"properties": {
|
||||||
|
"memory_mb": 4096,
|
||||||
|
"cpu_arch": "x86_64",
|
||||||
|
"local_gb": 10,
|
||||||
|
"cpus": 8},
|
||||||
|
"provision_state": "deploying",
|
||||||
|
"provision_updated_at": "2016-01-27T20:41:03+00:00",
|
||||||
|
"resource_class": None,
|
||||||
|
"target_power_state": None,
|
||||||
|
"target_provision_state": "active",
|
||||||
|
"updated_at": "2016-01-27T20:41:03+00:00",
|
||||||
|
"uuid": "1be26c0b-03f2-4d2e-ae87-c02d7f33c123",
|
||||||
|
"previous_provision_state": "available",
|
||||||
|
"previous_target_provision_state": None,
|
||||||
|
"event": "deploy"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event_type":"baremetal.node.provision_set.start",
|
||||||
|
"publisher_id":"ironic-conductor.hostname01"
|
||||||
|
}
|
||||||
|
|
||||||
.. [1] https://wiki.openstack.org/wiki/LoggingStandards#Log_level_definitions
|
.. [1] https://wiki.openstack.org/wiki/LoggingStandards#Log_level_definitions
|
||||||
.. [2] https://www.rabbitmq.com/documentation.html
|
.. [2] https://www.rabbitmq.com/documentation.html
|
||||||
|
@ -195,6 +195,9 @@ DELETE_ALLOWED_STATES = (AVAILABLE, MANAGEABLE, ENROLL, ADOPTFAIL)
|
|||||||
STABLE_STATES = (ENROLL, MANAGEABLE, AVAILABLE, ACTIVE, ERROR)
|
STABLE_STATES = (ENROLL, MANAGEABLE, AVAILABLE, ACTIVE, ERROR)
|
||||||
"""States that will not transition unless receiving a request."""
|
"""States that will not transition unless receiving a request."""
|
||||||
|
|
||||||
|
UNSTABLE_STATES = (DEPLOYING, DEPLOYWAIT, CLEANING, CLEANWAIT, VERIFYING,
|
||||||
|
DELETING, INSPECTING, ADOPTING)
|
||||||
|
"""States that can be changed without external request."""
|
||||||
|
|
||||||
##############
|
##############
|
||||||
# Power states
|
# Power states
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_messaging import exceptions as oslo_msg_exc
|
from oslo_messaging import exceptions as oslo_msg_exc
|
||||||
|
from oslo_utils import strutils
|
||||||
from oslo_versionedobjects import exception as oslo_vo_exc
|
from oslo_versionedobjects import exception as oslo_vo_exc
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
@ -25,6 +26,17 @@ LOG = log.getLogger(__name__)
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def mask_secrets(payload):
|
||||||
|
"""Remove secrets from payload object."""
|
||||||
|
mask = '******'
|
||||||
|
if hasattr(payload, 'instance_info'):
|
||||||
|
payload.instance_info = strutils.mask_dict_password(
|
||||||
|
payload.instance_info, mask)
|
||||||
|
if 'image_url' in payload.instance_info:
|
||||||
|
payload.instance_info['image_url'] = mask
|
||||||
|
# TODO(yuriyz): add "driver_info" support
|
||||||
|
|
||||||
|
|
||||||
def _emit_conductor_node_notification(task, notification_method,
|
def _emit_conductor_node_notification(task, notification_method,
|
||||||
payload_method, action,
|
payload_method, action,
|
||||||
level, status, **kwargs):
|
level, status, **kwargs):
|
||||||
@ -58,6 +70,7 @@ def _emit_conductor_node_notification(task, notification_method,
|
|||||||
"payload_method %(payload_method)s, error "
|
"payload_method %(payload_method)s, error "
|
||||||
"%(error)s"))
|
"%(error)s"))
|
||||||
payload = payload_method(task.node, **kwargs)
|
payload = payload_method(task.node, **kwargs)
|
||||||
|
mask_secrets(payload)
|
||||||
notification_method(
|
notification_method(
|
||||||
publisher=notification.NotificationPublisher(
|
publisher=notification.NotificationPublisher(
|
||||||
service='ironic-conductor', host=CONF.host),
|
service='ironic-conductor', host=CONF.host),
|
||||||
@ -129,3 +142,25 @@ def emit_power_state_corrected_notification(task, from_power):
|
|||||||
fields.NotificationStatus.SUCCESS,
|
fields.NotificationStatus.SUCCESS,
|
||||||
from_power=from_power
|
from_power=from_power
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def emit_provision_set_notification(task, level, status, prev_state,
|
||||||
|
prev_target, event):
|
||||||
|
"""Helper for conductor sending a set provision state notification.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
:param level: One of fields.NotificationLevel.
|
||||||
|
:param status: One of fields.NotificationStatus.
|
||||||
|
:param prev_state: Previous provision state.
|
||||||
|
:param prev_target: Previous target provision state.
|
||||||
|
:param event: FSM event that triggered provision state change.
|
||||||
|
"""
|
||||||
|
_emit_conductor_node_notification(
|
||||||
|
task,
|
||||||
|
node_objects.NodeSetProvisionStateNotification,
|
||||||
|
node_objects.NodeSetProvisionStatePayload,
|
||||||
|
'provision_set', level, status,
|
||||||
|
prev_state=prev_state,
|
||||||
|
prev_target=prev_target,
|
||||||
|
event=event
|
||||||
|
)
|
||||||
|
@ -94,6 +94,8 @@ raised in the background thread.):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import futurist
|
import futurist
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -106,7 +108,9 @@ from ironic.common import driver_factory
|
|||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.i18n import _, _LE, _LI, _LW
|
from ironic.common.i18n import _, _LE, _LI, _LW
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
|
from ironic.conductor import notification_utils as notify
|
||||||
from ironic import objects
|
from ironic import objects
|
||||||
|
from ironic.objects import fields
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -200,6 +204,12 @@ class TaskManager(object):
|
|||||||
self._purpose = purpose
|
self._purpose = purpose
|
||||||
self._debug_timer = timeutils.StopWatch()
|
self._debug_timer = timeutils.StopWatch()
|
||||||
|
|
||||||
|
# states and event for notification
|
||||||
|
self._prev_provision_state = None
|
||||||
|
self._prev_target_provision_state = None
|
||||||
|
self._event = None
|
||||||
|
self._saved_node = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
node = objects.Node.get(context, node_id)
|
node = objects.Node.get(context, node_id)
|
||||||
LOG.debug("Attempting to get %(type)s lock on node %(node)s (for "
|
LOG.debug("Attempting to get %(type)s lock on node %(node)s (for "
|
||||||
@ -358,6 +368,44 @@ class TaskManager(object):
|
|||||||
except exception.NodeNotFound:
|
except exception.NodeNotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _notify_provision_state_change(self):
|
||||||
|
"""Emit notification about change of the node provision state."""
|
||||||
|
if self._event is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.node is None:
|
||||||
|
# Rare case if resource released before notification
|
||||||
|
task = copy.copy(self)
|
||||||
|
task.fsm = states.machine.copy()
|
||||||
|
task.node = self._saved_node
|
||||||
|
else:
|
||||||
|
task = self
|
||||||
|
|
||||||
|
node = task.node
|
||||||
|
|
||||||
|
state = node.provision_state
|
||||||
|
prev_state = self._prev_provision_state
|
||||||
|
new_unstable = state in states.UNSTABLE_STATES
|
||||||
|
prev_unstable = prev_state in states.UNSTABLE_STATES
|
||||||
|
level = fields.NotificationLevel.INFO
|
||||||
|
|
||||||
|
if self._event in ('fail', 'error'):
|
||||||
|
status = fields.NotificationStatus.ERROR
|
||||||
|
level = fields.NotificationLevel.ERROR
|
||||||
|
elif (prev_unstable, new_unstable) == (False, True):
|
||||||
|
status = fields.NotificationStatus.START
|
||||||
|
elif (prev_unstable, new_unstable) == (True, False):
|
||||||
|
status = fields.NotificationStatus.END
|
||||||
|
else:
|
||||||
|
status = fields.NotificationStatus.SUCCESS
|
||||||
|
|
||||||
|
notify.emit_provision_set_notification(
|
||||||
|
task, level, status, self._prev_provision_state,
|
||||||
|
self._prev_target_provision_state, self._event)
|
||||||
|
|
||||||
|
# reset saved event, avoiding duplicate notification
|
||||||
|
self._event = None
|
||||||
|
|
||||||
def _thread_release_resources(self, fut):
|
def _thread_release_resources(self, fut):
|
||||||
"""Thread callback to release resources."""
|
"""Thread callback to release resources."""
|
||||||
try:
|
try:
|
||||||
@ -382,6 +430,11 @@ class TaskManager(object):
|
|||||||
:raises: InvalidState if the event is not allowed by the associated
|
:raises: InvalidState if the event is not allowed by the associated
|
||||||
state machine
|
state machine
|
||||||
"""
|
"""
|
||||||
|
# save previous states and event
|
||||||
|
self._prev_provision_state = self.node.provision_state
|
||||||
|
self._prev_target_provision_state = self.node.target_provision_state
|
||||||
|
self._event = event
|
||||||
|
|
||||||
# Advance the state model for the given event. Note that this doesn't
|
# Advance the state model for the given event. Note that this doesn't
|
||||||
# alter the node in any way. This may raise InvalidState, if this event
|
# alter the node in any way. This may raise InvalidState, if this event
|
||||||
# is not allowed in the current state.
|
# is not allowed in the current state.
|
||||||
@ -394,7 +447,6 @@ class TaskManager(object):
|
|||||||
self.node.provision_state,
|
self.node.provision_state,
|
||||||
self.node.target_provision_state)
|
self.node.target_provision_state)
|
||||||
|
|
||||||
previous_state = self.node.provision_state
|
|
||||||
self.node.provision_state = self.fsm.current_state
|
self.node.provision_state = self.fsm.current_state
|
||||||
|
|
||||||
# NOTE(lucasagomes): If there's no extra processing
|
# NOTE(lucasagomes): If there's no extra processing
|
||||||
@ -422,7 +474,14 @@ class TaskManager(object):
|
|||||||
'"%(target)s"'),
|
'"%(target)s"'),
|
||||||
{'node': self.node.uuid, 'state': self.node.provision_state,
|
{'node': self.node.uuid, 'state': self.node.provision_state,
|
||||||
'target': self.node.target_provision_state,
|
'target': self.node.target_provision_state,
|
||||||
'previous': previous_state})
|
'previous': self._prev_provision_state})
|
||||||
|
|
||||||
|
if callback is None:
|
||||||
|
self._notify_provision_state_change()
|
||||||
|
else:
|
||||||
|
# save the node, in case it is released before a notification is
|
||||||
|
# emitted at __exit__().
|
||||||
|
self._saved_node = self.node
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@ -450,6 +509,13 @@ class TaskManager(object):
|
|||||||
fut.add_done_callback(self._thread_release_resources)
|
fut.add_done_callback(self._thread_release_resources)
|
||||||
# Don't unlock! The unlock will occur when the
|
# Don't unlock! The unlock will occur when the
|
||||||
# thread finishes.
|
# thread finishes.
|
||||||
|
# NOTE(yuriyz): A race condition with process_event()
|
||||||
|
# in callback is possible here if eventlet changes behavior.
|
||||||
|
# E.g., if the execution of the new thread (that handles the
|
||||||
|
# event processing) finishes before we get here, that new
|
||||||
|
# thread may emit the "end" notification before we emit the
|
||||||
|
# following "start" notification.
|
||||||
|
self._notify_provision_state_change()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
|
@ -530,3 +530,37 @@ class NodeCorrectedPowerStatePayload(NodePayload):
|
|||||||
def __init__(self, node, from_power):
|
def __init__(self, node, from_power):
|
||||||
super(NodeCorrectedPowerStatePayload, self).__init__(
|
super(NodeCorrectedPowerStatePayload, self).__init__(
|
||||||
node, from_power=from_power)
|
node, from_power=from_power)
|
||||||
|
|
||||||
|
|
||||||
|
@base.IronicObjectRegistry.register
|
||||||
|
class NodeSetProvisionStateNotification(notification.NotificationBase):
|
||||||
|
"""Notification emitted when ironic changes a node provision state."""
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'payload': object_fields.ObjectField('NodeSetProvisionStatePayload')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@base.IronicObjectRegistry.register
|
||||||
|
class NodeSetProvisionStatePayload(NodePayload):
|
||||||
|
"""Payload schema for when ironic changes a node provision state."""
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
SCHEMA = dict(NodePayload.SCHEMA,
|
||||||
|
**{'instance_info': ('node', 'instance_info')})
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'instance_info': object_fields.FlexibleDictField(nullable=True),
|
||||||
|
'event': object_fields.StringField(nullable=True),
|
||||||
|
'previous_provision_state': object_fields.StringField(nullable=True),
|
||||||
|
'previous_target_provision_state':
|
||||||
|
object_fields.StringField(nullable=True)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, node, prev_state, prev_target, event):
|
||||||
|
super(NodeSetProvisionStatePayload, self).__init__(
|
||||||
|
node, event=event, previous_provision_state=prev_state,
|
||||||
|
previous_target_provision_state=prev_target)
|
||||||
|
@ -21,8 +21,10 @@ from oslo_versionedobjects.exception import VersionedObjectsException
|
|||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
from ironic.conductor import notification_utils as notif_utils
|
from ironic.conductor import notification_utils as notif_utils
|
||||||
|
from ironic.conductor import task_manager
|
||||||
from ironic.objects import fields
|
from ironic.objects import fields
|
||||||
from ironic.objects import node as node_objects
|
from ironic.objects import node as node_objects
|
||||||
|
from ironic.tests import base as tests_base
|
||||||
from ironic.tests.unit.db import base
|
from ironic.tests.unit.db import base
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
|
|
||||||
@ -67,7 +69,8 @@ class TestNotificationUtils(base.DbTestCase):
|
|||||||
to_power=states.POWER_ON
|
to_power=states.POWER_ON
|
||||||
)
|
)
|
||||||
|
|
||||||
def test__emit_conductor_node_notification(self):
|
@mock.patch.object(notif_utils, 'mask_secrets')
|
||||||
|
def test__emit_conductor_node_notification(self, mock_secrets):
|
||||||
mock_notify_method = mock.Mock()
|
mock_notify_method = mock.Mock()
|
||||||
# Required for exception handling
|
# Required for exception handling
|
||||||
mock_notify_method.__name__ = 'MockNotificationConstructor'
|
mock_notify_method.__name__ = 'MockNotificationConstructor'
|
||||||
@ -88,6 +91,7 @@ class TestNotificationUtils(base.DbTestCase):
|
|||||||
|
|
||||||
mock_payload_method.assert_called_once_with(
|
mock_payload_method.assert_called_once_with(
|
||||||
self.task.node, **mock_kwargs)
|
self.task.node, **mock_kwargs)
|
||||||
|
mock_secrets.assert_called_once_with(mock_payload_method.return_value)
|
||||||
mock_notify_method.assert_called_once_with(
|
mock_notify_method.assert_called_once_with(
|
||||||
publisher=mock.ANY,
|
publisher=mock.ANY,
|
||||||
event_type=mock.ANY,
|
event_type=mock.ANY,
|
||||||
@ -120,7 +124,9 @@ class TestNotificationUtils(base.DbTestCase):
|
|||||||
|
|
||||||
self.assertFalse(mock_notify_method.called)
|
self.assertFalse(mock_notify_method.called)
|
||||||
|
|
||||||
def test__emit_conductor_node_notification_known_notify_exc(self):
|
@mock.patch.object(notif_utils, 'mask_secrets')
|
||||||
|
def test__emit_conductor_node_notification_known_notify_exc(self,
|
||||||
|
mock_secrets):
|
||||||
"""Test exception caught for a known notification exception."""
|
"""Test exception caught for a known notification exception."""
|
||||||
mock_notify_method = mock.Mock()
|
mock_notify_method = mock.Mock()
|
||||||
# Required for exception handling
|
# Required for exception handling
|
||||||
@ -142,3 +148,49 @@ class TestNotificationUtils(base.DbTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(mock_notify_method.return_value.emit.called)
|
self.assertFalse(mock_notify_method.return_value.emit.called)
|
||||||
|
|
||||||
|
|
||||||
|
class ProvisionNotifyTestCase(tests_base.TestCase):
|
||||||
|
@mock.patch('ironic.objects.node.NodeSetProvisionStateNotification')
|
||||||
|
def test_emit_notification(self, provision_mock):
|
||||||
|
provision_mock.__name__ = 'NodeSetProvisionStateNotification'
|
||||||
|
self.config(host='fake-host')
|
||||||
|
node = obj_utils.get_test_node(self.context,
|
||||||
|
provision_state='fake state',
|
||||||
|
target_provision_state='fake target',
|
||||||
|
instance_info={'foo': 'baz'})
|
||||||
|
task = mock.Mock(spec=task_manager.TaskManager)
|
||||||
|
task.node = node
|
||||||
|
test_level = fields.NotificationLevel.INFO
|
||||||
|
test_status = fields.NotificationStatus.SUCCESS
|
||||||
|
notif_utils.emit_provision_set_notification(
|
||||||
|
task, test_level, test_status, 'fake_old',
|
||||||
|
'fake_old_target', 'event')
|
||||||
|
init_kwargs = provision_mock.call_args[1]
|
||||||
|
publisher = init_kwargs['publisher']
|
||||||
|
event_type = init_kwargs['event_type']
|
||||||
|
level = init_kwargs['level']
|
||||||
|
payload = init_kwargs['payload']
|
||||||
|
self.assertEqual('fake-host', publisher.host)
|
||||||
|
self.assertEqual('ironic-conductor', publisher.service)
|
||||||
|
self.assertEqual('node', event_type.object)
|
||||||
|
self.assertEqual('provision_set', event_type.action)
|
||||||
|
self.assertEqual(test_status, event_type.status)
|
||||||
|
self.assertEqual(test_level, level)
|
||||||
|
self.assertEqual(node.uuid, payload.uuid)
|
||||||
|
self.assertEqual('fake state', payload.provision_state)
|
||||||
|
self.assertEqual('fake target', payload.target_provision_state)
|
||||||
|
self.assertEqual('fake_old', payload.previous_provision_state)
|
||||||
|
self.assertEqual('fake_old_target',
|
||||||
|
payload.previous_target_provision_state)
|
||||||
|
self.assertEqual({'foo': 'baz'}, payload.instance_info)
|
||||||
|
|
||||||
|
def test_mask_secrets(self):
|
||||||
|
test_info = {'configdrive': 'fake_drive', 'image_url': 'fake-url',
|
||||||
|
'some_value': 'fake-value'}
|
||||||
|
node = obj_utils.get_test_node(self.context,
|
||||||
|
instance_info=test_info)
|
||||||
|
notif_utils.mask_secrets(node)
|
||||||
|
self.assertEqual('******', node.instance_info['configdrive'])
|
||||||
|
self.assertEqual('******', node.instance_info['image_url'])
|
||||||
|
self.assertEqual('fake-value', node.instance_info['some_value'])
|
||||||
|
@ -25,8 +25,10 @@ from ironic.common import driver_factory
|
|||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common import fsm
|
from ironic.common import fsm
|
||||||
from ironic.common import states
|
from ironic.common import states
|
||||||
|
from ironic.conductor import notification_utils
|
||||||
from ironic.conductor import task_manager
|
from ironic.conductor import task_manager
|
||||||
from ironic import objects
|
from ironic import objects
|
||||||
|
from ironic.objects import fields
|
||||||
from ironic.tests import base as tests_base
|
from ironic.tests import base as tests_base
|
||||||
from ironic.tests.unit.db import base as tests_db_base
|
from ironic.tests.unit.db import base as tests_db_base
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
@ -418,9 +420,11 @@ class TaskManagerTestCase(tests_db_base.DbTestCase):
|
|||||||
task1.process_event('provide')
|
task1.process_event('provide')
|
||||||
self.assertEqual(states.CLEANING, task1.node.provision_state)
|
self.assertEqual(states.CLEANING, task1.node.provision_state)
|
||||||
|
|
||||||
|
@mock.patch.object(task_manager.TaskManager,
|
||||||
|
'_notify_provision_state_change', autospec=True)
|
||||||
def test_spawn_after(
|
def test_spawn_after(
|
||||||
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
self, notify_mock, get_portgroups_mock, get_ports_mock,
|
||||||
reserve_mock, release_mock, node_get_mock):
|
build_driver_mock, reserve_mock, release_mock, node_get_mock):
|
||||||
spawn_mock = mock.Mock(return_value=self.future_mock)
|
spawn_mock = mock.Mock(return_value=self.future_mock)
|
||||||
task_release_mock = mock.Mock()
|
task_release_mock = mock.Mock()
|
||||||
reserve_mock.return_value = self.node
|
reserve_mock.return_value = self.node
|
||||||
@ -437,6 +441,7 @@ class TaskManagerTestCase(tests_db_base.DbTestCase):
|
|||||||
# release resources pending the finishing of the background
|
# release resources pending the finishing of the background
|
||||||
# thread
|
# thread
|
||||||
self.assertFalse(task_release_mock.called)
|
self.assertFalse(task_release_mock.called)
|
||||||
|
notify_mock.assert_called_once_with(task)
|
||||||
|
|
||||||
def test_spawn_after_exception_while_yielded(
|
def test_spawn_after_exception_while_yielded(
|
||||||
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
||||||
@ -455,9 +460,11 @@ class TaskManagerTestCase(tests_db_base.DbTestCase):
|
|||||||
self.assertFalse(spawn_mock.called)
|
self.assertFalse(spawn_mock.called)
|
||||||
task_release_mock.assert_called_once_with()
|
task_release_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(task_manager.TaskManager,
|
||||||
|
'_notify_provision_state_change', autospec=True)
|
||||||
def test_spawn_after_spawn_fails(
|
def test_spawn_after_spawn_fails(
|
||||||
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
self, notify_mock, get_portgroups_mock, get_ports_mock,
|
||||||
reserve_mock, release_mock, node_get_mock):
|
build_driver_mock, reserve_mock, release_mock, node_get_mock):
|
||||||
spawn_mock = mock.Mock(side_effect=exception.IronicException('foo'))
|
spawn_mock = mock.Mock(side_effect=exception.IronicException('foo'))
|
||||||
task_release_mock = mock.Mock()
|
task_release_mock = mock.Mock()
|
||||||
reserve_mock.return_value = self.node
|
reserve_mock.return_value = self.node
|
||||||
@ -471,6 +478,7 @@ class TaskManagerTestCase(tests_db_base.DbTestCase):
|
|||||||
|
|
||||||
spawn_mock.assert_called_once_with(1, 2, foo='bar', cat='meow')
|
spawn_mock.assert_called_once_with(1, 2, foo='bar', cat='meow')
|
||||||
task_release_mock.assert_called_once_with()
|
task_release_mock.assert_called_once_with()
|
||||||
|
self.assertFalse(notify_mock.called)
|
||||||
|
|
||||||
def test_spawn_after_link_fails(
|
def test_spawn_after_link_fails(
|
||||||
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
self, get_portgroups_mock, get_ports_mock, build_driver_mock,
|
||||||
@ -672,6 +680,11 @@ class TaskManagerStateModelTestCases(tests_base.TestCase):
|
|||||||
self.assertEqual(states.NOSTATE,
|
self.assertEqual(states.NOSTATE,
|
||||||
self.task.node.target_provision_state)
|
self.task.node.target_provision_state)
|
||||||
|
|
||||||
|
def test_process_event_no_callback_notify(self):
|
||||||
|
self.task.process_event = task_manager.TaskManager.process_event
|
||||||
|
self.task.process_event(self.task, 'fake')
|
||||||
|
self.task._notify_provision_state_change.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
@task_manager.require_exclusive_lock
|
||||||
def _req_excl_lock_method(*args, **kwargs):
|
def _req_excl_lock_method(*args, **kwargs):
|
||||||
@ -762,3 +775,98 @@ class ThreadExceptionTestCase(tests_base.TestCase):
|
|||||||
self.future_mock.exception.assert_called_once_with()
|
self.future_mock.exception.assert_called_once_with()
|
||||||
self.assertIsNone(self.node.last_error)
|
self.assertIsNone(self.node.last_error)
|
||||||
self.assertTrue(log_mock.called)
|
self.assertTrue(log_mock.called)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(notification_utils, 'emit_provision_set_notification',
|
||||||
|
autospec=True)
|
||||||
|
class ProvisionNotifyTestCase(tests_base.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ProvisionNotifyTestCase, self).setUp()
|
||||||
|
self.node = mock.Mock(spec=objects.Node)
|
||||||
|
self.task = mock.Mock(spec=task_manager.TaskManager)
|
||||||
|
self.task.node = self.node
|
||||||
|
notifier = task_manager.TaskManager._notify_provision_state_change
|
||||||
|
self.task.notifier = notifier
|
||||||
|
self.task._prev_target_provision_state = 'oldtarget'
|
||||||
|
self.task._event = 'event'
|
||||||
|
|
||||||
|
def test_notify_no_state_change(self, emit_mock):
|
||||||
|
self.task._event = None
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
self.assertFalse(emit_mock.called)
|
||||||
|
|
||||||
|
def test_notify_error_state(self, emit_mock):
|
||||||
|
self.task._event = 'fail'
|
||||||
|
self.task._prev_provision_state = 'fake'
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
emit_mock.assert_called_once_with(self.task,
|
||||||
|
fields.NotificationLevel.ERROR,
|
||||||
|
fields.NotificationStatus.ERROR,
|
||||||
|
'fake', 'oldtarget', 'fail')
|
||||||
|
self.assertIsNone(self.task._event)
|
||||||
|
|
||||||
|
def test_notify_unstable_to_unstable(self, emit_mock):
|
||||||
|
self.node.provision_state = states.DEPLOYING
|
||||||
|
self.task._prev_provision_state = states.DEPLOYWAIT
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
emit_mock.assert_called_once_with(self.task,
|
||||||
|
fields.NotificationLevel.INFO,
|
||||||
|
fields.NotificationStatus.SUCCESS,
|
||||||
|
states.DEPLOYWAIT,
|
||||||
|
'oldtarget', 'event')
|
||||||
|
|
||||||
|
def test_notify_stable_to_unstable(self, emit_mock):
|
||||||
|
self.node.provision_state = states.DEPLOYING
|
||||||
|
self.task._prev_provision_state = states.AVAILABLE
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
emit_mock.assert_called_once_with(self.task,
|
||||||
|
fields.NotificationLevel.INFO,
|
||||||
|
fields.NotificationStatus.START,
|
||||||
|
states.AVAILABLE,
|
||||||
|
'oldtarget', 'event')
|
||||||
|
|
||||||
|
def test_notify_unstable_to_stable(self, emit_mock):
|
||||||
|
self.node.provision_state = states.ACTIVE
|
||||||
|
self.task._prev_provision_state = states.DEPLOYING
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
emit_mock.assert_called_once_with(self.task,
|
||||||
|
fields.NotificationLevel.INFO,
|
||||||
|
fields.NotificationStatus.END,
|
||||||
|
states.DEPLOYING,
|
||||||
|
'oldtarget', 'event')
|
||||||
|
|
||||||
|
def test_notify_stable_to_stable(self, emit_mock):
|
||||||
|
self.node.provision_state = states.MANAGEABLE
|
||||||
|
self.task._prev_provision_state = states.AVAILABLE
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
emit_mock.assert_called_once_with(self.task,
|
||||||
|
fields.NotificationLevel.INFO,
|
||||||
|
fields.NotificationStatus.SUCCESS,
|
||||||
|
states.AVAILABLE,
|
||||||
|
'oldtarget', 'event')
|
||||||
|
|
||||||
|
def test_notify_resource_released(self, emit_mock):
|
||||||
|
node = mock.Mock(spec=objects.Node)
|
||||||
|
node.provision_state = states.DEPLOYING
|
||||||
|
node.target_provision_state = states.ACTIVE
|
||||||
|
task = mock.Mock(spec=task_manager.TaskManager)
|
||||||
|
task._prev_provision_state = states.AVAILABLE
|
||||||
|
task._prev_target_provision_state = states.NOSTATE
|
||||||
|
task._event = 'event'
|
||||||
|
task.node = None
|
||||||
|
task._saved_node = node
|
||||||
|
notifier = task_manager.TaskManager._notify_provision_state_change
|
||||||
|
task.notifier = notifier
|
||||||
|
task.notifier(task)
|
||||||
|
task_arg = emit_mock.call_args[0][0]
|
||||||
|
self.assertEqual(node, task_arg.node)
|
||||||
|
self.assertIsNot(task, task_arg)
|
||||||
|
|
||||||
|
def test_notify_only_once(self, emit_mock):
|
||||||
|
self.node.provision_state = states.DEPLOYING
|
||||||
|
self.task._prev_provision_state = states.AVAILABLE
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
self.assertIsNone(self.task._event)
|
||||||
|
self.task.notifier(self.task)
|
||||||
|
self.assertEqual(1, emit_mock.call_count)
|
||||||
|
self.assertIsNone(self.task._event)
|
||||||
|
@ -418,7 +418,9 @@ expected_object_fingerprints = {
|
|||||||
'NodeCorrectedPowerStateNotification': '1.0-59acc533c11d306f149846f922739'
|
'NodeCorrectedPowerStateNotification': '1.0-59acc533c11d306f149846f922739'
|
||||||
'c15',
|
'c15',
|
||||||
'NodeCorrectedPowerStatePayload': '1.0-2a484d7c342caa9fe488de16dc5f1f1e',
|
'NodeCorrectedPowerStatePayload': '1.0-2a484d7c342caa9fe488de16dc5f1f1e',
|
||||||
|
'NodeSetProvisionStateNotification':
|
||||||
|
'1.0-59acc533c11d306f149846f922739c15',
|
||||||
|
'NodeSetProvisionStatePayload': '1.0-91be7439b9b6b04931c9b99b8e1ea87a'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Adds notifications for node's provision state changes, event types are
|
||||||
|
"baremetal.node.provision_set.{start, end, success, error}".
|
||||||
|
For more details, see
|
||||||
|
http://docs.openstack.org/developer/ironic/dev/notifications.html.
|
Loading…
Reference in New Issue
Block a user