Define objects used for notification
This patch adds the object definition used for notification. Co-Authored-By: Shilpa Devharakar <Shilpa.Devharakar@nttdata.com> Change-Id: If6e67b67fa30b41a1dffac374f8a1b395b33ada3 Partial-Implements: bp notifications-in-masakari
This commit is contained in:
parent
3253b13eb9
commit
00d11056ba
|
@ -0,0 +1,168 @@
|
|||
# Copyright (c) 2018 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
from masakari.objects import base
|
||||
from masakari.objects import fields
|
||||
from masakari import rpc
|
||||
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class NotificationObject(base.MasakariObject):
|
||||
"""Base class for every notification related versioned object."""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(NotificationObject, self).__init__(**kwargs)
|
||||
# The notification objects are created on the fly when masakari emits
|
||||
# the notification. This causes that every object shows every field as
|
||||
# changed. We don't want to send this meaningless information so we
|
||||
# reset the object after creation.
|
||||
self.obj_reset_changes(recursive=False)
|
||||
|
||||
|
||||
@base.MasakariObjectRegistry.register_notification
|
||||
class EventType(NotificationObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'action': fields.EventNotificationActionField(nullable=False),
|
||||
'phase': fields.EventNotificationPhaseField(nullable=True),
|
||||
}
|
||||
|
||||
def to_notification_event_type_field(self):
|
||||
"""Serialize the object to the wire format."""
|
||||
s = '%s' % (self.action)
|
||||
if self.obj_attr_is_set('phase'):
|
||||
s += '.%s' % self.phase
|
||||
return s
|
||||
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class NotificationPayloadBase(NotificationObject):
|
||||
"""Base class for the payload of versioned notifications."""
|
||||
# SCHEMA defines how to populate the payload fields. It is a dictionary
|
||||
# where every key value pair has the following format:
|
||||
# <payload_field_name>: (<data_source_name>,
|
||||
# <field_of_the_data_source>)
|
||||
# The <payload_field_name> is the name where the data will be stored in the
|
||||
# payload object, this field has to be defined as a field of the payload.
|
||||
# The <data_source_name> shall refer to name of the parameter passed as
|
||||
# kwarg to the payload's populate_schema() call and this object will be
|
||||
# used as the source of the data. The <field_of_the_data_source> shall be
|
||||
# a valid field of the passed argument.
|
||||
# The SCHEMA needs to be applied with the populate_schema() call before the
|
||||
# notification can be emitted.
|
||||
# The value of the payload.<payload_field_name> field will be set by the
|
||||
# <data_source_name>.<field_of_the_data_source> field. The
|
||||
# <data_source_name> will not be part of the payload object internal or
|
||||
# external representation.
|
||||
# Payload fields that are not set by the SCHEMA can be filled in the same
|
||||
# way as in any versioned object.
|
||||
SCHEMA = {}
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(NotificationPayloadBase, self).__init__(**kwargs)
|
||||
self.populated = not self.SCHEMA
|
||||
|
||||
def populate_schema(self, **kwargs):
|
||||
"""Populate the object based on the SCHEMA and the source objects
|
||||
|
||||
:param kwargs: A dict contains the source object at the key defined in
|
||||
the SCHEMA
|
||||
"""
|
||||
for key, (obj, field) in self.SCHEMA.items():
|
||||
source = kwargs[obj]
|
||||
if source.obj_attr_is_set(field):
|
||||
setattr(self, key, getattr(source, field))
|
||||
self.populated = True
|
||||
|
||||
# the schema population will create changed fields but we don't need
|
||||
# this information in the notification
|
||||
self.obj_reset_changes(recursive=False)
|
||||
|
||||
|
||||
@base.MasakariObjectRegistry.register_notification
|
||||
class NotificationPublisher(NotificationObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'host': fields.StringField(nullable=False),
|
||||
'binary': fields.StringField(nullable=False),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_service_obj(cls, service):
|
||||
return cls(host=service.host, binary=service.binary)
|
||||
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class NotificationBase(NotificationObject):
|
||||
"""Base class for versioned notifications.
|
||||
|
||||
Every subclass shall define a 'payload' field.
|
||||
"""
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'priority': fields.EventNotificationPriorityField(),
|
||||
'event_type': fields.ObjectField('EventType'),
|
||||
'publisher': fields.ObjectField('NotificationPublisher'),
|
||||
}
|
||||
|
||||
def _emit(self, context, event_type, publisher_id, payload):
|
||||
notifier = rpc.get_versioned_notifier(publisher_id)
|
||||
notify = getattr(notifier, self.priority)
|
||||
notify(context, event_type=event_type, payload=payload)
|
||||
|
||||
def emit(self, context):
|
||||
"""Send the notification."""
|
||||
assert self.payload.populated
|
||||
|
||||
# notification payload will be a newly populated object
|
||||
# therefore every field of it will look changed so this does not carry
|
||||
# any extra information so we drop this from the payload.
|
||||
self.payload.obj_reset_changes(recursive=False)
|
||||
|
||||
self._emit(context,
|
||||
event_type=self.event_type.to_notification_event_type_field(),
|
||||
publisher_id='%s:%s' %
|
||||
(self.publisher.binary,
|
||||
self.publisher.host),
|
||||
payload=self.payload.obj_to_primitive())
|
||||
|
||||
|
||||
def notification_sample(sample):
|
||||
"""Class decorator for documentation generation purposes.
|
||||
|
||||
This is to attach the notification sample information
|
||||
to the notification object for documentation generation purposes.
|
||||
|
||||
:param sample: the path of the sample json file relative to the
|
||||
doc/notification_samples/ directory in the masakari
|
||||
repository root.
|
||||
"""
|
||||
def wrap(cls):
|
||||
if not getattr(cls, 'samples', None):
|
||||
cls.samples = [sample]
|
||||
else:
|
||||
cls.samples.append(sample)
|
||||
return cls
|
||||
return wrap
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright (c) 2018 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
import inspect
|
||||
|
||||
import six
|
||||
|
||||
from masakari.notifications.objects import base
|
||||
from masakari.objects import base as masakari_base
|
||||
from masakari.objects import fields
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class ExceptionPayload(base.NotificationPayloadBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'module_name': fields.StringField(),
|
||||
'function_name': fields.StringField(),
|
||||
'exception': fields.StringField(),
|
||||
'exception_message': fields.StringField()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_exception(cls, fault):
|
||||
trace = inspect.trace()[-1]
|
||||
# apply strutils.mask_password on exception_message and
|
||||
# consider emitting the exception_message only if the safe flag is
|
||||
# true in the exception like in the REST API
|
||||
module = inspect.getmodule(trace[0])
|
||||
module_name = module.__name__ if module else 'unknown'
|
||||
return cls(
|
||||
function_name=trace[3],
|
||||
module_name=module_name,
|
||||
exception=fault.__class__.__name__,
|
||||
exception_message=six.text_type(fault))
|
||||
|
||||
|
||||
@base.notification_sample('engine-exception.json')
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class ExceptionNotification(base.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'payload': fields.ObjectField('ExceptionPayload')
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
# Copyright (c) 2018 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
from masakari.notifications.objects import base
|
||||
from masakari.objects import base as masakari_base
|
||||
from masakari.objects import fields
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class SegmentApiPayloadBase(base.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'id': ('segment', 'id'),
|
||||
'uuid': ('segment', 'uuid'),
|
||||
'name': ('segment', 'name'),
|
||||
'service_type': ('segment', 'service_type'),
|
||||
'description': ('segment', 'description'),
|
||||
'recovery_method': ('segment', 'recovery_method'),
|
||||
}
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'uuid': fields.UUIDField(),
|
||||
'name': fields.StringField(),
|
||||
'service_type': fields.StringField(),
|
||||
'description': fields.StringField(nullable=True),
|
||||
'recovery_method': fields.FailoverSegmentRecoveryMethodField(),
|
||||
}
|
||||
|
||||
def __init__(self, segment, **kwargs):
|
||||
super(SegmentApiPayloadBase, self).__init__(**kwargs)
|
||||
self.populate_schema(segment=segment)
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class SegmentApiPayload(SegmentApiPayloadBase):
|
||||
# No SCHEMA as all the additional fields are calculated
|
||||
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'fault': fields.ObjectField('ExceptionPayload', nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, segment, fault, **kwargs):
|
||||
super(SegmentApiPayload, self).__init__(
|
||||
segment=segment,
|
||||
fault=fault,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class HostApiPayloadBase(base.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'id': ('host', 'id'),
|
||||
'uuid': ('host', 'uuid'),
|
||||
'name': ('host', 'name'),
|
||||
'failover_segment_id': ('host', 'failover_segment_id'),
|
||||
'failover_segment': ('host', 'failover_segment'),
|
||||
'type': ('host', 'type'),
|
||||
'reserved': ('host', 'reserved'),
|
||||
'control_attributes': ('host', 'control_attributes'),
|
||||
'on_maintenance': ('host', 'on_maintenance'),
|
||||
}
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'uuid': fields.UUIDField(),
|
||||
'name': fields.StringField(),
|
||||
'failover_segment_id': fields.UUIDField(),
|
||||
'failover_segment': fields.ObjectField('FailoverSegment'),
|
||||
'type': fields.StringField(),
|
||||
'reserved': fields.BooleanField(),
|
||||
'control_attributes': fields.StringField(),
|
||||
'on_maintenance': fields.BooleanField(),
|
||||
}
|
||||
|
||||
def __init__(self, host, **kwargs):
|
||||
super(HostApiPayloadBase, self).__init__(**kwargs)
|
||||
self.populate_schema(host=host)
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class HostApiPayload(HostApiPayloadBase):
|
||||
# No SCHEMA as all the additional fields are calculated
|
||||
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'fault': fields.ObjectField('ExceptionPayload', nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, host, fault, **kwargs):
|
||||
super(HostApiPayload, self).__init__(
|
||||
host=host,
|
||||
fault=fault,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class NotificationApiPayloadBase(base.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'id': ('notification', 'id'),
|
||||
'notification_uuid': ('notification', 'notification_uuid'),
|
||||
'generated_time': ('notification', 'generated_time'),
|
||||
'source_host_uuid': ('notification', 'source_host_uuid'),
|
||||
'type': ('notification', 'type'),
|
||||
'payload': ('notification', 'payload'),
|
||||
'status': ('notification', 'status'),
|
||||
}
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
'notification_uuid': fields.UUIDField(),
|
||||
'generated_time': fields.DateTimeField(),
|
||||
'source_host_uuid': fields.UUIDField(),
|
||||
'type': fields.NotificationTypeField(),
|
||||
'payload': fields.DictOfStringsField(),
|
||||
'status': fields.NotificationStatusField(),
|
||||
}
|
||||
|
||||
def __init__(self, notification, **kwargs):
|
||||
super(NotificationApiPayloadBase, self).__init__(**kwargs)
|
||||
self.populate_schema(notification=notification)
|
||||
|
||||
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class NotificationApiPayload(NotificationApiPayloadBase):
|
||||
# No SCHEMA as all the additional fields are calculated
|
||||
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'fault': fields.ObjectField('ExceptionPayload', nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, notification, fault, **kwargs):
|
||||
super(NotificationApiPayload, self).__init__(
|
||||
notification=notification,
|
||||
fault=fault,
|
||||
**kwargs)
|
||||
|
||||
|
||||
@base.notification_sample('create-segment-start.json')
|
||||
@base.notification_sample('create-segment-end.json')
|
||||
@base.notification_sample('create-segment-error.json')
|
||||
@base.notification_sample('update-segment-start.json')
|
||||
@base.notification_sample('update-segment-end.json')
|
||||
@base.notification_sample('update-segment-error.json')
|
||||
@base.notification_sample('delete-segment-start.json')
|
||||
@base.notification_sample('delete-segment-end.json')
|
||||
@base.notification_sample('delete-segment-error.json')
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class SegmentApiNotification(base.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': fields.ObjectField('SegmentApiPayload')
|
||||
}
|
||||
|
||||
|
||||
@base.notification_sample('create-host-start.json')
|
||||
@base.notification_sample('create-host-end.json')
|
||||
@base.notification_sample('create-host-error.json')
|
||||
@base.notification_sample('update-host-start.json')
|
||||
@base.notification_sample('update-host-end.json')
|
||||
@base.notification_sample('update-host-error.json')
|
||||
@base.notification_sample('delete-host-start.json')
|
||||
@base.notification_sample('delete-host-end.json')
|
||||
@base.notification_sample('delete-host-error.json')
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class HostApiNotification(base.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': fields.ObjectField('HostApiPayload')
|
||||
}
|
||||
|
||||
|
||||
@base.notification_sample('create-notification-start.json')
|
||||
@base.notification_sample('create-notification-end.json')
|
||||
@base.notification_sample('create-notification-error.json')
|
||||
@masakari_base.MasakariObjectRegistry.register_notification
|
||||
class NotificationApiNotification(base.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': fields.ObjectField('NotificationApiPayload')
|
||||
}
|
|
@ -35,6 +35,12 @@ Enum = fields.Enum
|
|||
FieldType = fields.FieldType
|
||||
|
||||
|
||||
class BaseMasakariEnum(Enum):
|
||||
def __init__(self, **kwargs):
|
||||
super(BaseMasakariEnum, self).__init__(
|
||||
valid_values=self.__class__.ALL)
|
||||
|
||||
|
||||
class FailoverSegmentRecoveryMethod(Enum):
|
||||
"""Represents possible recovery_methods for failover segment."""
|
||||
|
||||
|
@ -180,6 +186,74 @@ class NotificationStatus(Enum):
|
|||
return cls.ALL[index]
|
||||
|
||||
|
||||
class EventNotificationAction(Enum):
|
||||
# Actions of segments
|
||||
SEGMENT_CREATE = 'segment.create'
|
||||
SEGMENT_UPDATE = 'segment.update'
|
||||
SEGMENT_DELETE = 'segment.delete'
|
||||
|
||||
# Actions of hosts
|
||||
HOST_CREATE = 'host.create'
|
||||
HOST_UPDATE = 'host.update'
|
||||
HOST_DELETE = 'host.delete'
|
||||
|
||||
# Actions of notifications
|
||||
NOTIFICATION_CREATE = 'notification.create'
|
||||
NOTIFICATION_PROCESS = 'notification.process'
|
||||
|
||||
ALL = (SEGMENT_CREATE, SEGMENT_UPDATE, SEGMENT_DELETE, HOST_CREATE,
|
||||
HOST_UPDATE, HOST_DELETE, NOTIFICATION_CREATE,
|
||||
NOTIFICATION_PROCESS)
|
||||
|
||||
def __init__(self):
|
||||
super(EventNotificationAction,
|
||||
self).__init__(valid_values=EventNotificationAction.ALL)
|
||||
|
||||
@classmethod
|
||||
def index(cls, value):
|
||||
"""Return an index into the Enum given a value."""
|
||||
return cls.ALL.index(value)
|
||||
|
||||
@classmethod
|
||||
def from_index(cls, index):
|
||||
"""Return the Enum value at a given index."""
|
||||
return cls.ALL[index]
|
||||
|
||||
|
||||
class EventNotificationPriority(BaseMasakariEnum):
|
||||
AUDIT = 'audit'
|
||||
CRITICAL = 'critical'
|
||||
DEBUG = 'debug'
|
||||
INFO = 'info'
|
||||
ERROR = 'error'
|
||||
SAMPLE = 'sample'
|
||||
WARN = 'warn'
|
||||
|
||||
ALL = (AUDIT, CRITICAL, DEBUG, INFO, ERROR, SAMPLE, WARN)
|
||||
|
||||
|
||||
class EventNotificationPhase(Enum):
|
||||
START = 'start'
|
||||
END = 'end'
|
||||
ERROR = 'error'
|
||||
|
||||
ALL = (START, END, ERROR)
|
||||
|
||||
def __init__(self):
|
||||
super(EventNotificationPhase,
|
||||
self).__init__(valid_values=EventNotificationPhase.ALL)
|
||||
|
||||
@classmethod
|
||||
def index(cls, value):
|
||||
"""Return an index into the Enum given a value."""
|
||||
return cls.ALL.index(value)
|
||||
|
||||
@classmethod
|
||||
def from_index(cls, index):
|
||||
"""Return the Enum value at a given index."""
|
||||
return cls.ALL[index]
|
||||
|
||||
|
||||
class FailoverSegmentRecoveryMethodField(BaseEnumField):
|
||||
AUTO_TYPE = FailoverSegmentRecoveryMethod()
|
||||
|
||||
|
@ -190,3 +264,15 @@ class NotificationTypeField(BaseEnumField):
|
|||
|
||||
class NotificationStatusField(BaseEnumField):
|
||||
AUTO_TYPE = NotificationStatus()
|
||||
|
||||
|
||||
class EventNotificationActionField(BaseEnumField):
|
||||
AUTO_TYPE = EventNotificationAction()
|
||||
|
||||
|
||||
class EventNotificationPriorityField(BaseEnumField):
|
||||
AUTO_TYPE = EventNotificationPriority()
|
||||
|
||||
|
||||
class EventNotificationPhaseField(BaseEnumField):
|
||||
AUTO_TYPE = EventNotificationPhase()
|
||||
|
|
|
@ -20,6 +20,7 @@ inline callbacks.
|
|||
|
||||
"""
|
||||
import contextlib
|
||||
import datetime
|
||||
import eventlet
|
||||
eventlet.monkey_patch(os=False) # noqa
|
||||
|
||||
|
@ -31,6 +32,7 @@ import testtools
|
|||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from masakari.tests import fixtures as masakari_fixtures
|
||||
from masakari.tests.unit import conf_fixture
|
||||
|
@ -117,6 +119,69 @@ class TestCase(testtools.TestCase):
|
|||
for k, v in kw.items():
|
||||
CONF.set_override(k, v, group)
|
||||
|
||||
def assertJsonEqual(self, expected, observed):
|
||||
"""Asserts that 2 complex data structures are json equivalent.
|
||||
|
||||
We use data structures which serialize down to json throughout
|
||||
the code, and often times we just need to know that these are
|
||||
json equivalent. This means that list order is not important,
|
||||
and should be sorted.
|
||||
|
||||
Because this is a recursive set of assertions, when failure
|
||||
happens we want to expose both the local failure and the
|
||||
global view of the 2 data structures being compared. So a
|
||||
MismatchError which includes the inner failure as the
|
||||
mismatch, and the passed in expected / observed as matchee /
|
||||
matcher.
|
||||
|
||||
"""
|
||||
if isinstance(expected, six.string_types):
|
||||
expected = jsonutils.loads(expected)
|
||||
if isinstance(observed, six.string_types):
|
||||
observed = jsonutils.loads(observed)
|
||||
|
||||
def sort_key(x):
|
||||
if isinstance(x, (set, list)) or isinstance(x, datetime.datetime):
|
||||
return str(x)
|
||||
if isinstance(x, dict):
|
||||
items = ((sort_key(key), sort_key(value))
|
||||
for key, value in x.items())
|
||||
return sorted(items)
|
||||
return x
|
||||
|
||||
def inner(expected, observed):
|
||||
if isinstance(expected, dict) and isinstance(observed, dict):
|
||||
self.assertEqual(len(expected), len(observed))
|
||||
expected_keys = sorted(expected)
|
||||
observed_keys = sorted(observed)
|
||||
self.assertEqual(expected_keys, observed_keys)
|
||||
|
||||
for key in list(six.iterkeys(expected)):
|
||||
inner(expected[key], observed[key])
|
||||
elif (isinstance(expected, (list, tuple, set)) and isinstance(
|
||||
observed, (list, tuple, set))):
|
||||
self.assertEqual(len(expected), len(observed))
|
||||
|
||||
expected_values_iter = iter(sorted(expected, key=sort_key))
|
||||
observed_values_iter = iter(sorted(observed, key=sort_key))
|
||||
|
||||
for i in range(len(expected)):
|
||||
inner(next(expected_values_iter),
|
||||
next(observed_values_iter))
|
||||
else:
|
||||
self.assertEqual(expected, observed)
|
||||
|
||||
try:
|
||||
inner(expected, observed)
|
||||
except testtools.matchers.MismatchError as e:
|
||||
inner_mismatch = e.mismatch
|
||||
# inverting the observed / expected because testtools
|
||||
# error messages assume expected is second. Possibly makes
|
||||
# reading the error messages less confusing.
|
||||
raise testtools.matchers.MismatchError(observed, expected,
|
||||
inner_mismatch,
|
||||
verbose=True)
|
||||
|
||||
|
||||
class NoDBTestCase(TestCase):
|
||||
"""`NoDBTestCase` differs from TestCase in that DB access is not supported.
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
# Copyright (c) 2018 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
import collections
|
||||
|
||||
import mock
|
||||
from oslo_utils import timeutils
|
||||
from oslo_versionedobjects import fixture
|
||||
|
||||
from masakari.notifications.objects import base as notification
|
||||
from masakari.objects import base
|
||||
from masakari.objects import fields
|
||||
from masakari import test
|
||||
|
||||
|
||||
class TestNotificationBase(test.NoDBTestCase):
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestObject(base.MasakariObject):
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'field_1': fields.StringField(),
|
||||
'field_2': fields.IntegerField(),
|
||||
'not_important_field': fields.IntegerField(),
|
||||
}
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestNotificationPayload(notification.NotificationPayloadBase):
|
||||
VERSION = '1.0'
|
||||
|
||||
SCHEMA = {
|
||||
'field_1': ('source_field', 'field_1'),
|
||||
'field_2': ('source_field', 'field_2'),
|
||||
}
|
||||
|
||||
fields = {
|
||||
'extra_field': fields.StringField(), # filled by ctor
|
||||
'field_1': fields.StringField(), # filled by the schema
|
||||
'field_2': fields.IntegerField(), # filled by the schema
|
||||
}
|
||||
|
||||
def populate_schema(self, source_field):
|
||||
super(TestNotificationBase.TestNotificationPayload,
|
||||
self).populate_schema(source_field=source_field)
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestNotificationPayloadEmptySchema(
|
||||
notification.NotificationPayloadBase):
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'extra_field': fields.StringField(), # filled by ctor
|
||||
}
|
||||
|
||||
@notification.notification_sample('test-update-1.json')
|
||||
@notification.notification_sample('test-update-2.json')
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestNotification(notification.NotificationBase):
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'payload': fields.ObjectField('TestNotificationPayload')
|
||||
}
|
||||
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestNotificationEmptySchema(notification.NotificationBase):
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'payload': fields.ObjectField('TestNotificationPayloadEmptySchema')
|
||||
}
|
||||
|
||||
fake_service = {
|
||||
'created_at': timeutils.utcnow().replace(microsecond=0),
|
||||
'updated_at': None,
|
||||
'deleted_at': None,
|
||||
'deleted': False,
|
||||
'id': 123,
|
||||
'host': 'fake-host',
|
||||
'binary': 'masakari-fake',
|
||||
'topic': 'fake-service-topic',
|
||||
'report_count': 1,
|
||||
'forced_down': False,
|
||||
'disabled': False,
|
||||
'disabled_reason': None,
|
||||
'last_seen_up': None,
|
||||
'version': 1}
|
||||
|
||||
expected_payload = {
|
||||
'masakari_object.name': 'TestNotificationPayload',
|
||||
'masakari_object.data': {
|
||||
'extra_field': 'test string',
|
||||
'field_1': 'test1',
|
||||
'field_2': 42},
|
||||
'masakari_object.version': '1.0',
|
||||
'masakari_object.namespace': 'masakari'}
|
||||
|
||||
def setUp(self):
|
||||
super(TestNotificationBase, self).setUp()
|
||||
mock_context = mock.Mock()
|
||||
mock_context.to_dict.return_value = {}
|
||||
self.publisher = notification.NotificationPublisher(
|
||||
context=mock_context, host='fake-host',
|
||||
binary='masakari-fake')
|
||||
|
||||
self.my_obj = self.TestObject(field_1='test1',
|
||||
field_2=42,
|
||||
not_important_field=13)
|
||||
|
||||
self.payload = self.TestNotificationPayload(
|
||||
extra_field='test string')
|
||||
self.payload.populate_schema(source_field=self.my_obj)
|
||||
|
||||
self.notification = self.TestNotification(
|
||||
event_type=notification.EventType(
|
||||
object='test_object',
|
||||
action=fields.EventNotificationAction.SEGMENT_CREATE,
|
||||
phase=fields.EventNotificationPhase.START),
|
||||
publisher=self.publisher,
|
||||
priority=fields.EventNotificationPriority.INFO,
|
||||
payload=self.payload)
|
||||
|
||||
def _verify_notification(self, mock_notifier, mock_context,
|
||||
expected_event_type,
|
||||
expected_payload):
|
||||
mock_notifier.prepare.assert_called_once_with(
|
||||
publisher_id='masakari-fake:fake-host')
|
||||
mock_notify = mock_notifier.prepare.return_value.info
|
||||
self.assertTrue(mock_notify.called)
|
||||
self.assertEqual(mock_notify.call_args[0][0], mock_context)
|
||||
self.assertEqual(mock_notify.call_args[1]['event_type'],
|
||||
expected_event_type)
|
||||
actual_payload = mock_notify.call_args[1]['payload']
|
||||
self.assertJsonEqual(expected_payload, actual_payload)
|
||||
|
||||
@mock.patch('masakari.rpc.NOTIFIER')
|
||||
def test_emit_notification(self, mock_notifier):
|
||||
|
||||
mock_context = mock.Mock()
|
||||
mock_context.to_dict.return_value = {}
|
||||
self.notification.emit(mock_context)
|
||||
|
||||
self._verify_notification(
|
||||
mock_notifier,
|
||||
mock_context,
|
||||
expected_event_type='segment.create.start',
|
||||
expected_payload=self.expected_payload)
|
||||
|
||||
@mock.patch('masakari.rpc.NOTIFIER')
|
||||
def test_emit_with_host_and_binary_as_publisher(self, mock_notifier):
|
||||
noti = self.TestNotification(
|
||||
event_type=notification.EventType(
|
||||
object='test_object',
|
||||
action=fields.EventNotificationAction.SEGMENT_CREATE),
|
||||
publisher=notification.NotificationPublisher(
|
||||
host='fake-host', binary='masakari-fake'),
|
||||
priority=fields.EventNotificationPriority.INFO,
|
||||
payload=self.payload)
|
||||
|
||||
mock_context = mock.Mock()
|
||||
mock_context.to_dict.return_value = {}
|
||||
noti.emit(mock_context)
|
||||
|
||||
self._verify_notification(
|
||||
mock_notifier,
|
||||
mock_context,
|
||||
expected_event_type='segment.create',
|
||||
expected_payload=self.expected_payload)
|
||||
|
||||
@mock.patch('masakari.rpc.NOTIFIER')
|
||||
def test_emit_event_type_without_phase(self, mock_notifier):
|
||||
noti = self.TestNotification(
|
||||
event_type=notification.EventType(
|
||||
object='test_object',
|
||||
action=fields.EventNotificationAction.SEGMENT_CREATE),
|
||||
publisher=self.publisher,
|
||||
priority=fields.EventNotificationPriority.INFO,
|
||||
payload=self.payload)
|
||||
|
||||
mock_context = mock.Mock()
|
||||
mock_context.to_dict.return_value = {}
|
||||
noti.emit(mock_context)
|
||||
|
||||
self._verify_notification(
|
||||
mock_notifier,
|
||||
mock_context,
|
||||
expected_event_type='segment.create',
|
||||
expected_payload=self.expected_payload)
|
||||
|
||||
@mock.patch('masakari.rpc.NOTIFIER')
|
||||
def test_not_possible_to_emit_if_not_populated(self, mock_notifier):
|
||||
non_populated_payload = self.TestNotificationPayload(
|
||||
extra_field='test string')
|
||||
noti = self.TestNotification(
|
||||
event_type=notification.EventType(
|
||||
object='test_object',
|
||||
action=fields.EventNotificationAction.SEGMENT_CREATE),
|
||||
publisher=self.publisher,
|
||||
priority=fields.EventNotificationPriority.INFO,
|
||||
payload=non_populated_payload)
|
||||
|
||||
mock_context = mock.Mock()
|
||||
self.assertRaises(AssertionError, noti.emit, mock_context)
|
||||
self.assertFalse(mock_notifier.called)
|
||||
|
||||
@mock.patch('masakari.rpc.NOTIFIER')
|
||||
def test_empty_schema(self, mock_notifier):
|
||||
non_populated_payload = self.TestNotificationPayloadEmptySchema(
|
||||
extra_field='test string')
|
||||
noti = self.TestNotificationEmptySchema(
|
||||
event_type=notification.EventType(
|
||||
object='test_object',
|
||||
action=fields.EventNotificationAction.SEGMENT_CREATE),
|
||||
publisher=self.publisher,
|
||||
priority=fields.EventNotificationPriority.INFO,
|
||||
payload=non_populated_payload)
|
||||
|
||||
mock_context = mock.Mock()
|
||||
mock_context.to_dict.return_value = {}
|
||||
noti.emit(mock_context)
|
||||
|
||||
self._verify_notification(
|
||||
mock_notifier,
|
||||
mock_context,
|
||||
expected_event_type='segment.create',
|
||||
expected_payload={
|
||||
'masakari_object.name': 'TestNotificationPayloadEmptySchema',
|
||||
'masakari_object.data': {'extra_field': u'test string'},
|
||||
'masakari_object.version': '1.0',
|
||||
'masakari_object.namespace': 'masakari'})
|
||||
|
||||
def test_sample_decorator(self):
|
||||
self.assertEqual(2, len(self.TestNotification.samples))
|
||||
self.assertIn('test-update-1.json', self.TestNotification.samples)
|
||||
self.assertIn('test-update-2.json', self.TestNotification.samples)
|
||||
|
||||
|
||||
class TestNotificationObjectVersions(test.NoDBTestCase):
|
||||
|
||||
def test_notification_payload_version_depends_on_the_schema(self):
|
||||
@base.MasakariObjectRegistry.register_if(False)
|
||||
class TestNotificationPayload(notification.NotificationPayloadBase):
|
||||
VERSION = '1.0'
|
||||
|
||||
SCHEMA = {
|
||||
'field_1': ('source_field', 'field_1'),
|
||||
'field_2': ('source_field', 'field_2'),
|
||||
}
|
||||
|
||||
fields = {
|
||||
'extra_field': fields.StringField(), # filled by ctor
|
||||
'field_1': fields.StringField(), # filled by the schema
|
||||
'field_2': fields.IntegerField(), # filled by the schema
|
||||
}
|
||||
|
||||
checker = fixture.ObjectVersionChecker(
|
||||
{'TestNotificationPayload': (TestNotificationPayload,)})
|
||||
|
||||
old_hash = checker.get_hashes(extra_data_func=get_extra_data)
|
||||
TestNotificationPayload.SCHEMA['field_3'] = ('source_field',
|
||||
'field_3')
|
||||
new_hash = checker.get_hashes(extra_data_func=get_extra_data)
|
||||
|
||||
self.assertNotEqual(old_hash, new_hash)
|
||||
|
||||
|
||||
def get_extra_data(obj_class):
|
||||
extra_data = tuple()
|
||||
|
||||
# Get the SCHEMA items to add to the fingerprint
|
||||
# if we are looking at a notification
|
||||
if issubclass(obj_class, notification.NotificationPayloadBase):
|
||||
schema_data = collections.OrderedDict(
|
||||
sorted(obj_class.SCHEMA.items()))
|
||||
|
||||
extra_data += (schema_data,)
|
||||
|
||||
return extra_data
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright (c) 2018 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
from masakari.notifications.objects import base
|
||||
from masakari.notifications.objects import exception
|
||||
from masakari.notifications.objects import notification as event_notification
|
||||
from masakari.objects import host as host_obj
|
||||
from masakari.objects import notification as notification_obj
|
||||
from masakari.objects import segment as segment_obj
|
||||
|
||||
|
||||
segment = segment_obj.FailoverSegment()
|
||||
host = host_obj.Host()
|
||||
notification = notification_obj.Notification()
|
||||
fault = None
|
||||
|
||||
|
||||
init_args = {
|
||||
event_notification.SegmentApiPayloadBase: [segment],
|
||||
event_notification.SegmentApiPayload: [segment, fault],
|
||||
event_notification.HostApiPayloadBase: [host],
|
||||
event_notification.HostApiPayload: [host, fault],
|
||||
event_notification.NotificationApiPayloadBase: [notification],
|
||||
event_notification.NotificationApiPayload: [notification, fault],
|
||||
event_notification.SegmentApiNotification: [],
|
||||
event_notification.HostApiNotification: [],
|
||||
event_notification.NotificationApiNotification: [],
|
||||
exception.ExceptionPayload: [],
|
||||
exception.ExceptionNotification: [],
|
||||
base.EventType: [],
|
||||
base.NotificationPublisher: [],
|
||||
segment_obj.FailoverSegment: [],
|
||||
segment_obj.FailoverSegmentList: [],
|
||||
host_obj.Host: [],
|
||||
host_obj.HostList: [],
|
||||
notification_obj.Notification: [],
|
||||
notification_obj.NotificationList: [],
|
||||
}
|
||||
|
||||
|
||||
init_kwargs = {
|
||||
event_notification.SegmentApiPayloadBase: {},
|
||||
event_notification.SegmentApiPayload: {},
|
||||
event_notification.HostApiPayloadBase: {},
|
||||
event_notification.HostApiPayload: {},
|
||||
event_notification.NotificationApiPayloadBase: {},
|
||||
event_notification.NotificationApiPayload: {},
|
||||
event_notification.SegmentApiNotification: {},
|
||||
event_notification.HostApiNotification: {},
|
||||
event_notification.NotificationApiNotification: {},
|
||||
exception.ExceptionPayload: {},
|
||||
exception.ExceptionNotification: {},
|
||||
base.EventType: {},
|
||||
base.NotificationPublisher: {},
|
||||
segment_obj.FailoverSegment: {},
|
||||
segment_obj.FailoverSegmentList: {},
|
||||
host_obj.Host: {},
|
||||
host_obj.HostList: {},
|
||||
notification_obj.Notification: {},
|
||||
notification_obj.NotificationList: {},
|
||||
}
|
|
@ -28,6 +28,7 @@ from masakari.objects import base
|
|||
from masakari.objects import fields
|
||||
from masakari.objects import segment
|
||||
from masakari import test
|
||||
from masakari.tests.unit.objects import fake_args
|
||||
|
||||
|
||||
class MyOwnedObject(base.MasakariPersistentObject, base.MasakariObject):
|
||||
|
@ -658,8 +659,21 @@ object_data = {
|
|||
'HostList': '1.0-25ebe1b17fbd9f114fae8b6a10d198c0',
|
||||
'Notification': '1.0-eedfa3c203c100897021bd23f0ddf68c',
|
||||
'NotificationList': '1.0-25ebe1b17fbd9f114fae8b6a10d198c0',
|
||||
'EventType': '1.0-d1d2010a7391fa109f0868d964152607',
|
||||
'ExceptionNotification': '1.0-1187e93f564c5cca692db76a66cda2a6',
|
||||
'ExceptionPayload': '1.0-4516ae282a55fe2fd5c754967ee6248b',
|
||||
'HostApiNotification': '1.0-1187e93f564c5cca692db76a66cda2a6',
|
||||
'HostApiPayload': '1.0-ca9035d81cec6697f12dd4cac4c8f027',
|
||||
'HostApiPayloadBase': '1.0-211379087a876212df6194b011207339',
|
||||
'NotificationApiPayload': '1.0-c050869a1f4aed23e7645bd4d1830ecd',
|
||||
'NotificationApiPayloadBase': '1.0-cda8d53a77e64f83e3782fc9c4d499bb',
|
||||
'NotificationApiNotification': '1.0-1187e93f564c5cca692db76a66cda2a6',
|
||||
'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',
|
||||
'MyObj': '1.6-ee7b607402fbfb3390a92ab7199e0d88',
|
||||
'MyOwnedObject': '1.0-fec853730bd02d54cc32771dd67f08a0'
|
||||
'MyOwnedObject': '1.0-fec853730bd02d54cc32771dd67f08a0',
|
||||
'SegmentApiNotification': '1.0-1187e93f564c5cca692db76a66cda2a6',
|
||||
'SegmentApiPayload': '1.0-4c85836a1c2e4069b9dc84fa029a4657',
|
||||
'SegmentApiPayloadBase': '1.0-93a7c8b78d0e9ea3f6811d4ed75fa799'
|
||||
}
|
||||
|
||||
|
||||
|
@ -684,7 +698,11 @@ def get_masakari_objects():
|
|||
return masakari_classes
|
||||
|
||||
|
||||
class TestObjectVersions(test.NoDBTestCase):
|
||||
class TestObjectVersions(test.NoDBTestCase, _BaseTestCase):
|
||||
def setUp(self):
|
||||
super(test.NoDBTestCase, self).setUp()
|
||||
base.MasakariObjectRegistry.register_notification_objects()
|
||||
|
||||
def test_versions(self):
|
||||
checker = fixture.ObjectVersionChecker(
|
||||
get_masakari_objects())
|
||||
|
@ -714,8 +732,8 @@ class TestObjectVersions(test.NoDBTestCase):
|
|||
# Hold a dictionary of args/kwargs that need to get passed into
|
||||
# __init__() for specific classes. The key in the dictionary is
|
||||
# the obj_class that needs the init args/kwargs.
|
||||
init_args = {}
|
||||
init_kwargs = {}
|
||||
init_args = fake_args.init_args
|
||||
init_kwargs = fake_args.init_kwargs
|
||||
|
||||
checker = fixture.ObjectVersionChecker(
|
||||
base.MasakariObjectRegistry.obj_classes())
|
||||
|
|
Loading…
Reference in New Issue