Implements karbor events notifications

Getting the messages from Karbor like:
  - Plan create/update/delete
  - Checkpoint create/update/available
  - Restore create
  - Scheduled Operations create/delete
  - Triggers create/update/delete

This should add and send the desired messages to RabbitMQ with the
start and end identifiers for each message
(e.g. karbor.plan_create.start, karbor.plan_create.end). The
notification code was done in a similar fashion than the
notifications from the Trove project.

Implements:  blueprint karbor-event-notifications
Closes-Bug: 1797462
Change-Id: I0d7ffaa0873d192aeb24c17191683d705044644c
This commit is contained in:
genevieve-nantel 2019-05-10 14:58:12 -04:00
parent 8632a6cfcd
commit e96ec2726c
7 changed files with 608 additions and 23 deletions

View File

@ -25,6 +25,8 @@ from karbor.api.openstack import wsgi
from karbor.api.schemas import plans as plan_schema from karbor.api.schemas import plans as plan_schema
from karbor.api import validation from karbor.api import validation
from karbor.common import constants from karbor.common import constants
from karbor.common import notification
from karbor.common.notification import StartNotification
from karbor import exception from karbor import exception
from karbor.i18n import _ from karbor.i18n import _
@ -147,7 +149,8 @@ class PlansController(wsgi.Controller):
context = req.environ['karbor.context'] context = req.environ['karbor.context']
LOG.info("Delete plan with id: %s", id, context=context) LOG.info("Delete plan with id: %s", id, context=context)
context.notification = notification.KarborPlanDelete(
context, request=req)
try: try:
plan = self._plan_get(context, id) plan = self._plan_get(context, id)
except exception.PlanNotFound as error: except exception.PlanNotFound as error:
@ -157,7 +160,8 @@ class PlansController(wsgi.Controller):
project_id = plan.project_id project_id = plan.project_id
try: try:
plan.destroy() with StartNotification(context, id=id):
plan.destroy()
except Exception: except Exception:
msg = _("Failed to destroy a plan.") msg = _("Failed to destroy a plan.")
raise exc.HTTPServerError(reason=msg) raise exc.HTTPServerError(reason=msg)
@ -245,6 +249,8 @@ class PlansController(wsgi.Controller):
context.can(plan_policy.CREATE_POLICY) context.can(plan_policy.CREATE_POLICY)
plan = body['plan'] plan = body['plan']
LOG.debug('Create plan request plan: %s', plan) LOG.debug('Create plan request plan: %s', plan)
context.notification = notification.KarborPlanCreate(
context, request=req)
parameters = plan.get("parameters", None) parameters = plan.get("parameters", None)
@ -277,7 +283,9 @@ class PlansController(wsgi.Controller):
resource='plans') resource='plans')
try: try:
plan = objects.Plan(context=context, **plan_properties) plan = objects.Plan(context=context, **plan_properties)
plan.create() with StartNotification(
context, name=plan.get('name', None)):
plan.create()
QUOTAS.commit(context, reservations) QUOTAS.commit(context, reservations)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
@ -295,6 +303,8 @@ class PlansController(wsgi.Controller):
def update(self, req, id, body): def update(self, req, id, body):
"""Update a plan.""" """Update a plan."""
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.notification = notification.KarborPlanUpdate(
context, request=req)
plan = body['plan'] plan = body['plan']
update_dict = {} update_dict = {}
@ -327,9 +337,9 @@ class PlansController(wsgi.Controller):
except exception.PlanNotFound as error: except exception.PlanNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg) raise exc.HTTPNotFound(explanation=error.msg)
self._plan_update(context, plan, update_dict) with StartNotification(context, id=id):
self._plan_update(context, plan, update_dict)
plan.update(update_dict) plan.update(update_dict)
retval = self._view_builder.detail(req, plan) retval = self._view_builder.detail(req, plan)
return retval return retval

View File

@ -24,6 +24,8 @@ from karbor.api.openstack import wsgi
from karbor.api.schemas import checkpoints as checkpoint_schema from karbor.api.schemas import checkpoints as checkpoint_schema
from karbor.api import validation from karbor.api import validation
from karbor.common import constants from karbor.common import constants
from karbor.common import notification
from karbor.common.notification import StartNotification
from karbor import exception from karbor import exception
from karbor.i18n import _ from karbor.i18n import _
@ -337,6 +339,8 @@ class ProvidersController(wsgi.Controller):
"""Creates a new checkpoint.""" """Creates a new checkpoint."""
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.notification = notification.KarborCheckpointCreate(
context, request=req)
LOG.debug('Create checkpoint request ' LOG.debug('Create checkpoint request '
'body: %s provider_id:%s', body, provider_id) 'body: %s provider_id:%s', body, provider_id)
@ -404,9 +408,11 @@ class ProvidersController(wsgi.Controller):
else: else:
checkpoint_id = None checkpoint_id = None
try: try:
checkpoint_id = self.protection_api.protect( with StartNotification(
context, plan, checkpoint_properties) context, checkpoint_properties=checkpoint_properties):
QUOTAS.commit(context, reservations) checkpoint_id = self.protection_api.protect(
context, plan, checkpoint_properties)
QUOTAS.commit(context, reservations)
except Exception as error: except Exception as error:
if not checkpoint_id: if not checkpoint_id:
QUOTAS.rollback(context, reservations) QUOTAS.rollback(context, reservations)
@ -471,6 +477,8 @@ class ProvidersController(wsgi.Controller):
"""Delete a checkpoint.""" """Delete a checkpoint."""
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.can(provider_policy.CHECKPOINT_DELETE_POLICY) context.can(provider_policy.CHECKPOINT_DELETE_POLICY)
context.notification = notification.KarborCheckpointDelete(
context, request=req)
LOG.info("Delete checkpoint with id: %s.", checkpoint_id) LOG.info("Delete checkpoint with id: %s.", checkpoint_id)
LOG.info("provider_id: %s.", provider_id) LOG.info("provider_id: %s.", provider_id)
@ -484,7 +492,8 @@ class ProvidersController(wsgi.Controller):
project_id = checkpoint.get('project_id') project_id = checkpoint.get('project_id')
try: try:
self.protection_api.delete(context, provider_id, checkpoint_id) with StartNotification(context, checkpoint_id=checkpoint_id):
self.protection_api.delete(context, provider_id, checkpoint_id)
except exception.DeleteCheckpointNotAllowed as error: except exception.DeleteCheckpointNotAllowed as error:
raise exc.HTTPForbidden(explantion=error.msg) raise exc.HTTPForbidden(explantion=error.msg)
@ -518,6 +527,8 @@ class ProvidersController(wsgi.Controller):
def checkpoints_update(self, req, provider_id, checkpoint_id, body): def checkpoints_update(self, req, provider_id, checkpoint_id, body):
"""Reset a checkpoint's state""" """Reset a checkpoint's state"""
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.notification = notification.KarborCheckpointUpdate(
context, request=req)
LOG.info("Reset checkpoint state with id: %s", checkpoint_id) LOG.info("Reset checkpoint state with id: %s", checkpoint_id)
LOG.info("provider_id: %s.", provider_id) LOG.info("provider_id: %s.", provider_id)
@ -531,10 +542,12 @@ class ProvidersController(wsgi.Controller):
raise exc.HTTPBadRequest(explanation=msg) raise exc.HTTPBadRequest(explanation=msg)
context.can(provider_policy.CHECKPOINT_UPDATE_POLICY) context.can(provider_policy.CHECKPOINT_UPDATE_POLICY)
if body.get("os-resetState"): if body.get("os-resetState"):
state = body["os-resetState"]["state"] with StartNotification(context, checkpoint_id=checkpoint_id):
return self._checkpoint_reset_state( state = body["os-resetState"]["state"]
context, provider_id, checkpoint_id, state) return self._checkpoint_reset_state(
context, provider_id, checkpoint_id, state)
else: else:
msg = _("Invalid input.") msg = _("Invalid input.")
raise exc.HTTPBadRequest(explanation=msg) raise exc.HTTPBadRequest(explanation=msg)

View File

@ -23,6 +23,8 @@ from karbor.api.openstack import wsgi
from karbor.api.schemas import restores as restore_schema from karbor.api.schemas import restores as restore_schema
from karbor.api import validation from karbor.api import validation
from karbor.common import constants from karbor.common import constants
from karbor.common import notification
from karbor.common.notification import StartNotification
from karbor import exception from karbor import exception
from karbor.i18n import _ from karbor.i18n import _
@ -200,6 +202,8 @@ class RestoresController(wsgi.Controller):
LOG.debug('Create restore request body: %s', body) LOG.debug('Create restore request body: %s', body)
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.can(restore_policy.CREATE_POLICY) context.can(restore_policy.CREATE_POLICY)
context.notification = notification.KarborRestoreCreate(
context, request=req)
restore = body['restore'] restore = body['restore']
LOG.debug('Create restore request : %s', restore) LOG.debug('Create restore request : %s', restore)
@ -221,7 +225,8 @@ class RestoresController(wsgi.Controller):
# call restore rpc API of protection service # call restore rpc API of protection service
try: try:
self.protection_api.restore(context, restoreobj, restore_auth) with StartNotification(context, parameters=parameters):
self.protection_api.restore(context, restoreobj, restore_auth)
except exception.AccessCheckpointNotAllowed as error: except exception.AccessCheckpointNotAllowed as error:
raise exc.HTTPForbidden(explanation=error.msg) raise exc.HTTPForbidden(explanation=error.msg)
except Exception: except Exception:

View File

@ -21,6 +21,8 @@ from karbor.api.openstack import wsgi
from karbor.api.schemas import scheduled_operations as \ from karbor.api.schemas import scheduled_operations as \
scheduled_operation_schema scheduled_operation_schema
from karbor.api import validation from karbor.api import validation
from karbor.common import notification
from karbor.common.notification import StartNotification
from karbor import exception from karbor import exception
from karbor.i18n import _ from karbor.i18n import _
from karbor import objects from karbor import objects
@ -90,6 +92,8 @@ class ScheduledOperationController(wsgi.Controller):
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.can(scheduled_operation_policy.CREATE_POLICY) context.can(scheduled_operation_policy.CREATE_POLICY)
context.notification = notification.KarborScheduledOpsCreate(
context, request=req)
operation_info = body['scheduled_operation'] operation_info = body['scheduled_operation']
name = operation_info.get("name", None) name = operation_info.get("name", None)
@ -123,7 +127,8 @@ class ScheduledOperationController(wsgi.Controller):
self._raise_unknown_exception(ex) self._raise_unknown_exception(ex)
try: try:
self._create_scheduled_operation(context, operation) with StartNotification(context, operation_obj=operation_obj):
self._create_scheduled_operation(context, operation)
except Exception: except Exception:
try: try:
operation.destroy() operation.destroy()
@ -140,14 +145,17 @@ class ScheduledOperationController(wsgi.Controller):
LOG.debug('Delete scheduled operation(%s) start', id) LOG.debug('Delete scheduled operation(%s) start', id)
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.notification = notification.KarborScheduledOpsDelete(
context, request=req)
operation = self._get_operation_by_id(context, id, ['trigger']) operation = self._get_operation_by_id(context, id, ['trigger'])
trigger = operation.trigger trigger = operation.trigger
context.can(scheduled_operation_policy.DELETE_POLICY, operation) context.can(scheduled_operation_policy.DELETE_POLICY, operation)
try: try:
self.operationengine_api.delete_scheduled_operation( with StartNotification(context, id=id):
context, id, trigger.id) self.operationengine_api.delete_scheduled_operation(
context, id, trigger.id)
except (exception.ScheduledOperationStateNotFound, except (exception.ScheduledOperationStateNotFound,
exception.TriggerNotFound, exception.TriggerNotFound,

View File

@ -21,6 +21,8 @@ from karbor.api import common
from karbor.api.openstack import wsgi from karbor.api.openstack import wsgi
from karbor.api.schemas import triggers as trigger_schema from karbor.api.schemas import triggers as trigger_schema
from karbor.api import validation from karbor.api import validation
from karbor.common import notification
from karbor.common.notification import StartNotification
from karbor import exception from karbor import exception
from karbor.i18n import _ from karbor.i18n import _
from karbor import objects from karbor import objects
@ -88,6 +90,8 @@ class TriggersController(wsgi.Controller):
context = req.environ['karbor.context'] context = req.environ['karbor.context']
context.can(trigger_policy.CREATE_POLICY) context.can(trigger_policy.CREATE_POLICY)
trigger_info = body['trigger_info'] trigger_info = body['trigger_info']
context.notification = notification.KarborTriggerCreate(
context, request=req)
trigger_name = trigger_info.get("name", None) trigger_name = trigger_info.get("name", None)
trigger_type = trigger_info.get("type", None) trigger_type = trigger_info.get("type", None)
@ -103,10 +107,13 @@ class TriggersController(wsgi.Controller):
'properties': trigger_property, 'properties': trigger_property,
} }
try: try:
trigger = objects.Trigger(context=context, **trigger_definition) with StartNotification(
self.operationengine_api.verify_trigger(context, trigger) context, name=trigger_name):
self.operationengine_api.create_trigger(context, trigger) trigger = objects.Trigger(
trigger.create() context=context, **trigger_definition)
self.operationengine_api.verify_trigger(context, trigger)
self.operationengine_api.create_trigger(context, trigger)
trigger.create()
except exception.Invalid as ex: except exception.Invalid as ex:
raise exc.HTTPBadRequest(explanation=ex.msg) raise exc.HTTPBadRequest(explanation=ex.msg)
except Exception as ex: except Exception as ex:
@ -121,6 +128,8 @@ class TriggersController(wsgi.Controller):
context = req.environ['karbor.context'] context = req.environ['karbor.context']
trigger = self._get_trigger_by_id(context, id) trigger = self._get_trigger_by_id(context, id)
context.notification = notification.KarborTriggerDelete(
context, request=req)
context.can(trigger_policy.DELETE_POLICY, trigger) context.can(trigger_policy.DELETE_POLICY, trigger)
@ -135,7 +144,8 @@ class TriggersController(wsgi.Controller):
raise exc.HTTPFailedDependency(explanation=msg) raise exc.HTTPFailedDependency(explanation=msg)
try: try:
self.operationengine_api.delete_trigger(context, id) with StartNotification(context, id=id):
self.operationengine_api.delete_trigger(context, id)
except exception.TriggerNotFound as ex: except exception.TriggerNotFound as ex:
pass pass
except (exception.DeleteTriggerNotAllowed, except (exception.DeleteTriggerNotAllowed,
@ -152,6 +162,8 @@ class TriggersController(wsgi.Controller):
context = req.environ['karbor.context'] context = req.environ['karbor.context']
trigger = self._get_trigger_by_id(context, id) trigger = self._get_trigger_by_id(context, id)
context.notification = notification.KarborTriggerUpdate(
context, request=req)
context.can(trigger_policy.UPDATE_POLICY, trigger) context.can(trigger_policy.UPDATE_POLICY, trigger)
@ -177,7 +189,8 @@ class TriggersController(wsgi.Controller):
except (exception.TriggerNotFound, Exception) as ex: except (exception.TriggerNotFound, Exception) as ex:
self._raise_unknown_exception(ex) self._raise_unknown_exception(ex)
try: try:
trigger.save() with StartNotification(context, id=id):
trigger.save()
except Exception as ex: except Exception as ex:
self._raise_unknown_exception(ex) self._raise_unknown_exception(ex)

View File

@ -0,0 +1,349 @@
# 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.
"""The notification module."""
import abc
import copy
import traceback
from karbor import exception
from karbor.i18n import _
from karbor import rpc
from oslo_config import cfg
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class EndNotification(object):
@property
def _notifier(self):
"""Returns the notification for Karbor API."""
return (self.context.notification)
def __init__(self, context, **kwargs):
self.context = context
self.context.notification.payload.update(kwargs)
def __enter__(self):
return self.context.notification
def __exit__(self, etype, value, tb):
if etype:
message = str(value)
exception = traceback.format_exception(etype, value, tb)
self._notifier.notify_exc_info(message, exception)
else:
self._notifier.notify_end()
class StartNotification(EndNotification):
def __enter__(self):
self.context.notification.notify_start()
return super(StartNotification, self).__enter__()
class KaborAPINotification(object):
"""The traits of karbor.* notifications."""
event_type_format = 'karbor.%s.%s'
notify_callback = None
@classmethod
def register_notify_callback(cls, callback):
"""Callback when a notification is sent out."""
cls.notify_callback = callback
@abc.abstractmethod
def event_type(self):
'Returns the event type (like "create" for karbor.create.start)'
pass
@abc.abstractmethod
def required_start_traits(self):
'Returns list of required traits for start notification'
pass
def optional_start_traits(self):
'Returns list of optional traits for start notification'
return []
def required_end_traits(self):
'Returns list of required traits for end notification'
return []
def optional_end_traits(self):
'Returns list of optional traits for end notification'
return []
def required_error_traits(self):
'Returns list of required traits for error notification'
return ['message', 'exception']
def optional_error_traits(self):
'Returns list of optional traits for error notification'
return ['id']
def required_base_traits(self):
return ['tenant_id', 'client_ip', 'request_id']
@property
def request_id(self):
return self.payload['request_id']
def __init__(self, context, **kwargs):
self.context = context
self.needs_end_notification = True
self.payload = {}
if 'request' in kwargs:
request = kwargs.pop('request')
self.payload.update({
'request_id': context.request_id,
'client_ip': request.remote_addr,
'tenant_id': context.tenant,
})
elif 'request_id' not in kwargs:
raise exception.InvalidInput(
reason="Notification must include 'request' property")
self.payload.update(kwargs)
def serialize(self, context):
return self.payload
def validate(self, required_traits):
required_keys = set(required_traits)
provided_keys = set(self.payload.keys())
if not required_keys.issubset(provided_keys):
msg = (_("The following required keys not defined for"
" notification %(name)s: %(keys)s")
% {'name': self.__class__.__name__,
'keys': list(required_keys - provided_keys)})
raise exception.InvalidInput(reason=msg)
def _notify(self, event_qualifier, required_traits, optional_traits,
**kwargs):
self.payload.update(kwargs)
self.validate(self.required_base_traits() + required_traits)
available_values = self.serialize(self.context)
payload = {k: available_values[k]
for k in self.required_base_traits() + required_traits}
for k in optional_traits:
if k in available_values:
payload[k] = available_values[k]
qualified_event_type = (KaborAPINotification.event_type_format
% (self.event_type(), event_qualifier))
LOG.debug('Sending event: %(event_type)s, %(payload)s',
{'event_type': qualified_event_type, 'payload': payload})
context = copy.copy(self.context)
del context.notification
notifier = rpc.get_notifier()
notifier.info(context, qualified_event_type, self.payload)
if self.notify_callback:
self.notify_callback(event_qualifier)
def notify_start(self, **kwargs):
self._notify('start', self.required_start_traits(),
self.optional_start_traits(), **kwargs)
def notify_end(self, **kwargs):
if self.needs_end_notification:
self._notify('end', self.required_end_traits(),
self.optional_end_traits(), **kwargs)
def notify_exc_info(self, message, exception):
self.payload.update({
'message': message,
'exception': exception
})
self._notify('error', self.required_error_traits(),
self.optional_error_traits())
class KarborPlanCreate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'plan_create'
@abc.abstractmethod
def required_start_traits(self):
return ['name']
def optional_start_traits(self):
return ['parameters']
def required_end_traits(self):
return ['name']
class KarborPlanDelete(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'plan_delete'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
class KarborPlanUpdate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'plan_update'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
class KarborTriggerDelete(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'trigger_delete'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
class KarborTriggerCreate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'trigger_create'
@abc.abstractmethod
def required_start_traits(self):
return ['name']
def optional_start_traits(self):
return ['parameters']
def required_end_traits(self):
return ['name']
class KarborTriggerUpdate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'trigger_update'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
class KarborRestoreDelete(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'restore_delete'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
class KarborRestoreCreate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'restore_create'
@abc.abstractmethod
def required_start_traits(self):
return ['parameters']
def required_end_traits(self):
return ['parameters']
class KarborCheckpointCreate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'checkpoint_create'
@abc.abstractmethod
def required_start_traits(self):
return ['checkpoint_properties']
def required_end_traits(self):
return ['checkpoint_properties']
class KarborCheckpointDelete(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'checkpoint_delete'
@abc.abstractmethod
def required_start_traits(self):
return ['checkpoint_id']
def required_end_traits(self):
return ['checkpoint_id']
class KarborCheckpointUpdate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'checkpoint_update'
@abc.abstractmethod
def required_start_traits(self):
return ['checkpoint_id']
class KarborScheduledOpsCreate(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'scheduled_operation_create'
@abc.abstractmethod
def required_start_traits(self):
return ['operation_obj']
def required_end_traits(self):
return ['operation_obj']
class KarborScheduledOpsDelete(KaborAPINotification):
@abc.abstractmethod
def event_type(self):
return 'scheduled_operation_delete'
@abc.abstractmethod
def required_start_traits(self):
return ['id']
def required_end_traits(self):
return ['id']

View File

@ -0,0 +1,187 @@
# 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.
"""The notification module."""
from mock import Mock
from mock import patch
from karbor.common import notification
from karbor.common.notification import EndNotification
from karbor.common.notification import StartNotification
from karbor import context
from karbor import exception
from karbor import rpc
from karbor.tests import base
class TestEndNotification(base.TestCase):
def setUp(self):
super(TestEndNotification, self).setUp()
self.context = KarborTestContext(self)
def test_call(self):
with patch.object(self.context, "notification") as notification:
with EndNotification(self.context):
pass
self.assertTrue(notification.notify_end.called)
def server_exception(self, server_type):
with patch.object(self.context, "notification") as notification:
try:
with EndNotification(self.context):
raise exception.InvalidInput
except Exception:
self.assertTrue(notification.notify_exc_info.called)
class KarborTestContext(context.RequestContext):
def __init__(self, test_case, **kwargs):
super(KarborTestContext, self).__init__(user_id='demo',
project_id='abcd',
auth_token='efgh')
self.notification = KarborTestNotification(
self, request_id='req_id')
class TestStartNotification(base.TestCase):
def setUp(self):
super(TestStartNotification, self).setUp()
self.context = KarborTestContext(self)
def test_call(self):
with patch.object(self.context, "notification") as notification:
with StartNotification(self.context):
pass
self.assertTrue(notification.notify_start.called)
class KarborTestNotification(notification.KaborAPINotification):
def event_type(self):
return 'plan_test'
def required_start_traits(self):
return ['name']
def optional_start_traits(self):
return ['parameters']
def required_end_traits(self):
return ['name']
class TestKarborNotification(base.TestCase):
def setUp(self):
super(TestKarborNotification, self).setUp()
self.test_n = KarborTestNotification(Mock(), request=Mock())
def test_missing_required_start_traits(self):
self.assertRaisesRegex(exception.InvalidInput,
self.test_n.required_start_traits()[0],
self.test_n.notify_start)
def test_invalid_start_traits(self):
self.assertRaisesRegex(exception.InvalidInput,
"The following required keys",
self.test_n.notify_start, foo='bar')
def test_missing_required_end_traits(self):
self.assertRaisesRegex(exception.InvalidInput,
self.test_n.required_end_traits()[0],
self.test_n.notify_end)
def test_invalid_end_traits(self):
self.assertRaisesRegex(exception.InvalidInput,
"The following required keys",
self.test_n.notify_end, foo='bar')
def test_missing_required_error_traits(self):
self.assertRaisesRegex(exception.InvalidInput,
self.test_n.required_error_traits()[0],
self.test_n._notify, 'error',
self.test_n.required_error_traits(), [])
@patch.object(rpc, 'get_notifier')
def test_start_event(self, notifier):
self.test_n.notify_start(name='foo')
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
self.assertEqual('karbor.plan_test.start', a[1])
@patch.object(rpc, 'get_notifier')
def test_end_event(self, notifier):
self.test_n.notify_end(name='foo')
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
self.assertEqual('karbor.plan_test.end', a[1])
@patch.object(rpc, 'get_notifier')
def test_verify_base_values(self, notifier):
self.test_n.notify_start(name='foo')
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
payload = a[2]
self.assertIn('client_ip', payload)
self.assertIn('request_id', payload)
self.assertIn('tenant_id', payload)
@patch.object(rpc, 'get_notifier')
def test_verify_required_start_args(self, notifier):
self.test_n.notify_start(name='foo')
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
payload = a[2]
self.assertIn('name', payload)
@patch.object(rpc, 'get_notifier')
def test_verify_optional_start_args(self, notifier):
self.test_n.notify_start(name='foo', parameters="test")
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
payload = a[2]
self.assertIn('parameters', payload)
@patch.object(rpc, 'get_notifier')
def test_verify_required_end_args(self, notifier):
self.test_n.notify_end(name='foo')
self.assertTrue(notifier().info.called)
a, _ = notifier().info.call_args
payload = a[2]
self.assertIn('name', payload)
def _test_notify_callback(self, fn, *args, **kwargs):
with patch.object(rpc, 'get_notifier') as notifier:
mock_callback = Mock()
self.test_n.register_notify_callback(mock_callback)
mock_context = Mock()
mock_context.notification = Mock()
self.test_n.context = mock_context
fn(*args, **kwargs)
self.assertTrue(notifier().info.called)
self.assertTrue(mock_callback.called)
self.test_n.register_notify_callback(None)
def test_notify_callback(self):
required_keys = {
'name': 'name',
'parameters': 'parameters',
}
self._test_notify_callback(self.test_n.notify_start,
**required_keys)
self._test_notify_callback(self.test_n.notify_end,
**required_keys)
self._test_notify_callback(self.test_n.notify_exc_info,
'error', 'exc')