Implemented audit.update notification
In this changeset, I implemented the sending of update notifications whenever an audit is modified. Change-Id: I5ccc2516ce896ae7d4ef542b133e8f052eaed602 Partially-Implements: blueprint audit-versioned-notifications-api
This commit is contained in:
26
watcher/notifications/__init__.py
Normal file
26
watcher/notifications/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
|
||||
#
|
||||
# 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.
|
||||
|
||||
# Note(gibi): Importing publicly called functions so the caller code does not
|
||||
# need to be changed after we moved these function inside the package
|
||||
# Todo(gibi): remove these imports after legacy notifications using these are
|
||||
# transformed to versioned notifications
|
||||
from watcher.notifications import audit # noqa
|
||||
from watcher.notifications import exception # noqa
|
||||
from watcher.notifications import goal # noqa
|
||||
from watcher.notifications import strategy # noqa
|
||||
152
watcher/notifications/audit.py
Normal file
152
watcher/notifications/audit.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
|
||||
from watcher.common import exception
|
||||
from watcher.notifications import base as notificationbase
|
||||
from watcher.notifications import goal as goal_notifications
|
||||
from watcher.notifications import strategy as strategy_notifications
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditPayload(notificationbase.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'uuid': ('audit', 'uuid'),
|
||||
|
||||
'audit_type': ('audit', 'audit_type'),
|
||||
'state': ('audit', 'state'),
|
||||
'parameters': ('audit', 'parameters'),
|
||||
'interval': ('audit', 'interval'),
|
||||
'scope': ('audit', 'scope'),
|
||||
# 'goal_uuid': ('audit', 'goal_uuid'),
|
||||
# 'strategy_uuid': ('audit', 'strategy_uuid'),
|
||||
|
||||
'created_at': ('audit', 'created_at'),
|
||||
'updated_at': ('audit', 'updated_at'),
|
||||
'deleted_at': ('audit', 'deleted_at'),
|
||||
}
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'uuid': wfields.UUIDField(),
|
||||
'audit_type': wfields.StringField(),
|
||||
'state': wfields.StringField(),
|
||||
'parameters': wfields.FlexibleDictField(nullable=True),
|
||||
'interval': wfields.IntegerField(nullable=True),
|
||||
'scope': wfields.FlexibleListOfDictField(nullable=True),
|
||||
'goal_uuid': wfields.UUIDField(),
|
||||
'strategy_uuid': wfields.UUIDField(nullable=True),
|
||||
'goal': wfields.ObjectField('GoalPayload'),
|
||||
'strategy': wfields.ObjectField('StrategyPayload', nullable=True),
|
||||
|
||||
'created_at': wfields.DateTimeField(nullable=True),
|
||||
'updated_at': wfields.DateTimeField(nullable=True),
|
||||
'deleted_at': wfields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, audit, **kwargs):
|
||||
super(AuditPayload, self).__init__(**kwargs)
|
||||
self.populate_schema(audit=audit)
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditStateUpdatePayload(notificationbase.NotificationPayloadBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'old_state': wfields.StringField(nullable=True),
|
||||
'state': wfields.StringField(nullable=True),
|
||||
}
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditUpdatePayload(AuditPayload):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'state_update': wfields.ObjectField('AuditStateUpdatePayload'),
|
||||
}
|
||||
|
||||
def __init__(self, audit, state_update, goal, strategy):
|
||||
super(AuditUpdatePayload, self).__init__(
|
||||
audit=audit,
|
||||
state_update=state_update,
|
||||
goal=goal,
|
||||
strategy=strategy)
|
||||
|
||||
|
||||
@notificationbase.notification_sample('audit-update.json')
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class AuditUpdateNotification(notificationbase.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'payload': wfields.ObjectField('AuditUpdatePayload')
|
||||
}
|
||||
|
||||
|
||||
def send_update(context, audit, service='infra-optim',
|
||||
host=None, old_state=None):
|
||||
"""Emit an audit.update notification."""
|
||||
goal = None
|
||||
strategy = None
|
||||
try:
|
||||
goal = audit.goal
|
||||
if audit.strategy_id:
|
||||
strategy = audit.strategy
|
||||
except NotImplementedError:
|
||||
raise exception.EagerlyLoadedAuditRequired(audit=audit.uuid)
|
||||
|
||||
goal_payload = goal_notifications.GoalPayload(goal=goal)
|
||||
|
||||
strategy_payload = None
|
||||
if strategy:
|
||||
strategy_payload = strategy_notifications.StrategyPayload(
|
||||
strategy=strategy)
|
||||
|
||||
state_update = AuditStateUpdatePayload(
|
||||
old_state=old_state,
|
||||
state=audit.state if old_state else None)
|
||||
|
||||
versioned_payload = AuditUpdatePayload(
|
||||
audit=audit,
|
||||
state_update=state_update,
|
||||
goal=goal_payload,
|
||||
strategy=strategy_payload,
|
||||
)
|
||||
|
||||
notification = AuditUpdateNotification(
|
||||
priority=wfields.NotificationPriority.INFO,
|
||||
event_type=notificationbase.EventType(
|
||||
object='audit',
|
||||
action=wfields.NotificationAction.UPDATE),
|
||||
publisher=notificationbase.NotificationPublisher(
|
||||
host=host or CONF.host,
|
||||
binary=service),
|
||||
payload=versioned_payload)
|
||||
|
||||
notification.emit(context)
|
||||
203
watcher/notifications/base.py
Normal file
203
watcher/notifications/base.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 oslo_config import cfg
|
||||
|
||||
from watcher.common import exception
|
||||
from watcher.common import rpc
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
# Definition of notification levels in increasing order of severity
|
||||
NOTIFY_LEVELS = {
|
||||
wfields.NotificationPriority.DEBUG: 0,
|
||||
wfields.NotificationPriority.INFO: 1,
|
||||
wfields.NotificationPriority.WARNING: 2,
|
||||
wfields.NotificationPriority.ERROR: 3,
|
||||
wfields.NotificationPriority.CRITICAL: 4
|
||||
}
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_if(False)
|
||||
class NotificationObject(base.WatcherObject):
|
||||
"""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 watcher 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)
|
||||
|
||||
def save(self, context):
|
||||
raise exception.UnsupportedError()
|
||||
|
||||
def obj_load_attr(self, attrname):
|
||||
raise exception.UnsupportedError()
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class EventType(NotificationObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'object': wfields.StringField(),
|
||||
'action': wfields.NotificationActionField(),
|
||||
'phase': wfields.NotificationPhaseField(nullable=True),
|
||||
}
|
||||
|
||||
def to_notification_event_type_field(self):
|
||||
"""Serialize the object to the wire format."""
|
||||
s = '%s.%s' % (self.object, self.action)
|
||||
if self.obj_attr_is_set('phase'):
|
||||
s += '.%s' % self.phase
|
||||
return s
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.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.WatcherObjectRegistry.register_notification
|
||||
class NotificationPublisher(NotificationObject):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'host': wfields.StringField(nullable=False),
|
||||
'binary': wfields.StringField(nullable=False),
|
||||
}
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.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': wfields.NotificationPriorityField(),
|
||||
'event_type': wfields.ObjectField('EventType'),
|
||||
'publisher': wfields.ObjectField('NotificationPublisher'),
|
||||
}
|
||||
|
||||
def _should_notify(self):
|
||||
"""Determine whether the notification should be sent.
|
||||
|
||||
A notification is sent when the level of the notification is
|
||||
greater than or equal to the level specified in the
|
||||
configuration, in the increasing order of DEBUG, INFO, WARNING,
|
||||
ERROR, CRITICAL.
|
||||
:return: True if notification should be sent, False otherwise.
|
||||
"""
|
||||
if not CONF.notification_level:
|
||||
return False
|
||||
return (NOTIFY_LEVELS[self.priority] >=
|
||||
NOTIFY_LEVELS[CONF.notification_level])
|
||||
|
||||
def _emit(self, context, event_type, publisher_id, payload):
|
||||
notifier = rpc.get_notifier(publisher_id)
|
||||
notify = getattr(notifier, self.priority)
|
||||
notify(context, event_type=event_type, payload=payload)
|
||||
|
||||
def emit(self, context):
|
||||
"""Send the notification."""
|
||||
if not self._should_notify():
|
||||
return
|
||||
if not self.payload.populated:
|
||||
raise exception.NotificationPayloadError(
|
||||
class_name=self.__class__.__name__)
|
||||
# Note(gibi): 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):
|
||||
"""Provide a notification sample of the decatorated notification.
|
||||
|
||||
Class decorator 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 watcher
|
||||
repository root.
|
||||
"""
|
||||
def wrap(cls):
|
||||
if not getattr(cls, 'samples', None):
|
||||
cls.samples = [sample]
|
||||
else:
|
||||
cls.samples.append(sample)
|
||||
return cls
|
||||
return wrap
|
||||
53
watcher/notifications/exception.py
Normal file
53
watcher/notifications/exception.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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 watcher.notifications import base as notificationbase
|
||||
from watcher.objects import base as base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class ExceptionPayload(notificationbase.NotificationPayloadBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'module_name': wfields.StringField(),
|
||||
'function_name': wfields.StringField(),
|
||||
'exception': wfields.StringField(),
|
||||
'exception_message': wfields.StringField()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_exception(cls, fault):
|
||||
trace = inspect.trace()[-1]
|
||||
# TODO(gibi): 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
|
||||
return cls(
|
||||
function_name=trace[3],
|
||||
module_name=inspect.getmodule(trace[0]).__name__,
|
||||
exception=fault.__class__.__name__,
|
||||
exception_message=six.text_type(fault))
|
||||
|
||||
|
||||
@notificationbase.notification_sample('infra-optim-exception.json')
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class ExceptionNotification(notificationbase.NotificationBase):
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
fields = {
|
||||
'payload': wfields.ObjectField('ExceptionPayload')
|
||||
}
|
||||
53
watcher/notifications/goal.py
Normal file
53
watcher/notifications/goal.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
|
||||
#
|
||||
# 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 watcher.notifications import base as notificationbase
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class GoalPayload(notificationbase.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'uuid': ('goal', 'uuid'),
|
||||
'name': ('goal', 'name'),
|
||||
'display_name': ('goal', 'display_name'),
|
||||
'efficacy_specification': ('goal', 'efficacy_specification'),
|
||||
|
||||
'created_at': ('goal', 'created_at'),
|
||||
'updated_at': ('goal', 'updated_at'),
|
||||
'deleted_at': ('goal', 'deleted_at'),
|
||||
}
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'uuid': wfields.UUIDField(),
|
||||
'name': wfields.StringField(),
|
||||
'display_name': wfields.StringField(),
|
||||
'efficacy_specification': wfields.FlexibleListOfDictField(),
|
||||
|
||||
'created_at': wfields.DateTimeField(nullable=True),
|
||||
'updated_at': wfields.DateTimeField(nullable=True),
|
||||
'deleted_at': wfields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, goal, **kwargs):
|
||||
super(GoalPayload, self).__init__(**kwargs)
|
||||
self.populate_schema(goal=goal)
|
||||
53
watcher/notifications/strategy.py
Normal file
53
watcher/notifications/strategy.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Copyright (c) 2016 b<>com
|
||||
#
|
||||
# Authors: Vincent FRANCOISE <vincent.francoise@b-com.com>
|
||||
#
|
||||
# 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 watcher.notifications import base as notificationbase
|
||||
from watcher.objects import base
|
||||
from watcher.objects import fields as wfields
|
||||
|
||||
|
||||
@base.WatcherObjectRegistry.register_notification
|
||||
class StrategyPayload(notificationbase.NotificationPayloadBase):
|
||||
SCHEMA = {
|
||||
'uuid': ('strategy', 'uuid'),
|
||||
'name': ('strategy', 'name'),
|
||||
'display_name': ('strategy', 'display_name'),
|
||||
'parameters_spec': ('strategy', 'parameters_spec'),
|
||||
|
||||
'created_at': ('strategy', 'created_at'),
|
||||
'updated_at': ('strategy', 'updated_at'),
|
||||
'deleted_at': ('strategy', 'deleted_at'),
|
||||
}
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
|
||||
fields = {
|
||||
'uuid': wfields.UUIDField(),
|
||||
'name': wfields.StringField(),
|
||||
'display_name': wfields.StringField(),
|
||||
'parameters_spec': wfields.FlexibleDictField(nullable=True),
|
||||
|
||||
'created_at': wfields.DateTimeField(nullable=True),
|
||||
'updated_at': wfields.DateTimeField(nullable=True),
|
||||
'deleted_at': wfields.DateTimeField(nullable=True),
|
||||
}
|
||||
|
||||
def __init__(self, strategy, **kwargs):
|
||||
super(StrategyPayload, self).__init__(**kwargs)
|
||||
self.populate_schema(strategy=strategy)
|
||||
Reference in New Issue
Block a user