Skip to content

Commit

Permalink
Add notifications for leases
Browse files Browse the repository at this point in the history
add unit tests

Move emit notification to objects layer
  • Loading branch information
DanNiESh committed May 24, 2023
1 parent 7eb5db3 commit 3e7a470
Show file tree
Hide file tree
Showing 20 changed files with 1,327 additions and 13 deletions.
2 changes: 1 addition & 1 deletion esi_leap/api/controllers/v1/lease.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def delete(self, lease_id):
request, 'esi_leap:lease:get', lease_id,
statuses.LEASE_CAN_DELETE)

lease.cancel()
lease.cancel(request)

@staticmethod
def _lease_get_all_authorize_filters(cdict,
Expand Down
16 changes: 16 additions & 0 deletions esi_leap/common/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,19 @@ class InvalidTimeRange(ESILeapException):
class NodeNotFound(ESILeapException):
msg_fmt = _('Encountered an error fetching info for node %(uuid)s '
'(%(resource_type)s): %(err)s')


class NotificationPayloadError(ESILeapException):
_msg_fmt = _("Payload not populated when trying to send notification "
"\"%(class_name)s\"")


class NotificationSchemaObjectError(ESILeapException):
_msg_fmt = _("Expected object %(obj)s when populating notification payload"
" but got object %(source)s")


class NotificationSchemaKeyError(ESILeapException):
_msg_fmt = _("Object %(obj)s doesn't have the field \"%(field)s\" "
"required for populating notification schema key "
"\"%(key)s\"")
145 changes: 145 additions & 0 deletions esi_leap/common/notification_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# 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 contextlib

from oslo_config import cfg
from oslo_log import log
from oslo_messaging import exceptions as oslo_msg_exc
from oslo_utils import excutils
from oslo_versionedobjects import exception as oslo_vo_exc

from esi_leap.common import exception
from esi_leap.common.i18n import _
from esi_leap.objects import fields
from esi_leap.objects import notification

LOG = log.getLogger(__name__)
CONF = cfg.CONF


def _emit_api_notification(context, obj, action, level, status,
crud_notify_obj, **kwargs):
"""Helper for emitting API notifications.
:param context: request context.
:param obj: resource rpc object.
:param action: Action string to go in the EventType.
:param level: Notification level. One of
`esi_leap.objects.fields.NotificationLevel.ALL`
:param status: Status to go in the EventType. One of
`esi_leap.objects.fields.NotificationStatus.ALL`
:param kwargs: kwargs to use when creating the notification payload.
"""
resource = obj.__class__.__name__.lower()
extra_args = kwargs
try:
try:
if resource not in crud_notify_obj:
notification_name = payload_name = _("is not defined")
raise KeyError(_("Unsupported resource: %s") % resource)
else:
notification_method, payload_method = crud_notify_obj[resource]

notification_name = notification_method.__name__
payload_name = payload_method.__name__
finally:
# Prepare our exception message just in case
exception_values = {"resource": resource,
"uuid": obj.uuid,
"action": action,
"status": status,
"level": level,
"notification_method": notification_name,
"payload_method": payload_name}
exception_message = (_("Failed to send esi_leap.%(resource)s."
"%(action)s.%(status)s notification for "
"%(resource)s %(uuid)s with level "
"%(level)s, notification method "
"%(notification_method)s, payload method "
"%(payload_method)s, error %(error)s"))
payload = payload_method(obj, **extra_args)
notification_method(
publisher=notification.NotificationPublisher(
service='esi-leap-manager', host=CONF.host),
event_type=notification.EventType(
object=resource, action=action, status=status),
level=level,
payload=payload).emit(context)
LOG.info("Emit esi_leap notification: host is %s "
"event is esi_leap.%s.%s.%s ,"
"level is %s ,"
"notification method is %s",
CONF.host, resource, action, status,
level, notification_method)
except (exception.NotificationSchemaObjectError,
exception.NotificationSchemaKeyError,
exception.NotificationPayloadError,
oslo_msg_exc.MessageDeliveryFailure,
oslo_vo_exc.VersionedObjectsException) as e:
exception_values['error'] = e
LOG.warning(exception_message, exception_values)
LOG.exception(e.msg_fmt)

except Exception as e:
exception_values['error'] = e
LOG.exception(exception_message, exception_values)


def emit_start_notification(context, obj, action, crud_notify_obj, **kwargs):
"""Helper for emitting API 'start' notifications.
:param context: request context.
:param obj: resource rpc object.
:param action: Action string to go in the EventType.
:param kwargs: kwargs to use when creating the notification payload.
"""
_emit_api_notification(context, obj, action,
fields.NotificationLevel.INFO,
fields.NotificationStatus.START,
crud_notify_obj,
**kwargs)


@contextlib.contextmanager
def handle_error_notification(context, obj, action, crud_notify_obj, **kwargs):
"""Context manager to handle any error notifications.
:param context: request context.
:param obj: resource rpc object.
:param action: Action string to go in the EventType.
:param kwargs: kwargs to use when creating the notification payload.
"""
try:
yield
except Exception:
with excutils.save_and_reraise_exception():
_emit_api_notification(context, obj, action,
fields.NotificationLevel.ERROR,
fields.NotificationStatus.ERROR,
crud_notify_obj,
**kwargs)


def emit_end_notification(context, obj, action, crud_notify_obj, **kwargs):
"""Helper for emitting API 'end' notifications.
:param context: request context.
:param obj: resource rpc object.
:param action: Action string to go in the EventType.
:param kwargs: kwargs to use when creating the notification payload.
"""
_emit_api_notification(context, obj, action,
fields.NotificationLevel.INFO,
fields.NotificationStatus.END,
crud_notify_obj,
**kwargs)
103 changes: 103 additions & 0 deletions esi_leap/common/rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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.fic language governing permissions and limitations
# under the License.

from esi_leap.common import exception
from oslo_context import context as ctx
import oslo_messaging as messaging
from osprofiler import profiler

NOTIFICATION_TRANSPORT = None
VERSIONED_NOTIFIER = None

ALLOWED_EXMODS = [
exception.__name__,
]
EXTRA_EXMODS = []
TOPICS = ['esi_leap_versioned_notifications']


def init(conf):
global NOTIFICATION_TRANSPORT
global VERSIONED_NOTIFIER
exmods = get_allowed_exmods()
NOTIFICATION_TRANSPORT = messaging.get_notification_transport(
conf,
allowed_remote_exmods=exmods)

serializer = RequestContextSerializer(messaging.JsonPayloadSerializer())

if conf.notification.notification_level is None:

VERSIONED_NOTIFIER = messaging.Notifier(NOTIFICATION_TRANSPORT,
serializer=serializer,
driver='noop')
else:
VERSIONED_NOTIFIER = messaging.Notifier(
NOTIFICATION_TRANSPORT,
serializer=serializer,
topics=TOPICS)


def cleanup():
global NOTIFICATION_TRANSPORT
global VERSIONED_NOTIFIER
assert NOTIFICATION_TRANSPORT is not None
assert VERSIONED_NOTIFIER is not None
NOTIFICATION_TRANSPORT.cleanup()
NOTIFICATION_TRANSPORT = None
VERSIONED_NOTIFIER = None


def get_allowed_exmods():
return ALLOWED_EXMODS + EXTRA_EXMODS


# RequestContextSerializer borrowed from Ironic
class RequestContextSerializer(messaging.Serializer):

def __init__(self, base):
self._base = base

def serialize_entity(self, context, entity):
if not self._base:
return entity
return self._base.serialize_entity(context, entity)

def deserialize_entity(self, context, entity):
if not self._base:
return entity
return self._base.deserialize_entity(context, entity)

def serialize_context(self, context):
_context = context.to_dict()
prof = profiler.get()
if prof:
trace_info = {
"hmac_key": prof.hmac_key,
"base_id": prof.get_base_id(),
"parent_id": prof.get_id()
}
_context.update({"trace_info": trace_info})
return _context

def deserialize_context(self, context):
trace_info = context.pop("trace_info", None)
if trace_info:
profiler.init(**trace_info)
return ctx.RequestContext.from_dict(context)


def get_versioned_notifier(publisher_id=None):
assert VERSIONED_NOTIFIER is not None
assert publisher_id is not None
return VERSIONED_NOTIFIER.prepare(publisher_id=publisher_id)
2 changes: 2 additions & 0 deletions esi_leap/common/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from oslo_log import log
from oslo_service import service

from esi_leap.common import rpc
import esi_leap.conf
from esi_leap import objects
from esi_leap import version
Expand All @@ -30,6 +31,7 @@ def prepare_service(argv=None, default_config_files=None):
default_config_files=default_config_files)
db_options.set_defaults(CONF)
log.setup(CONF, 'esi-leap')
rpc.init(CONF)
objects.register_all()


Expand Down
2 changes: 2 additions & 0 deletions esi_leap/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from esi_leap.conf import ironic
from esi_leap.conf import keystone
from esi_leap.conf import netconf
from esi_leap.conf import notification
from esi_leap.conf import pecan
from oslo_config import cfg

Expand All @@ -28,4 +29,5 @@
ironic.register_opts(CONF)
keystone.register_opts(CONF)
netconf.register_opts(CONF)
notification.register_opts(CONF)
pecan.register_opts(CONF)
35 changes: 35 additions & 0 deletions esi_leap/conf/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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 esi_leap.common.i18n import _
from oslo_config import cfg

# borrowed from Ironic
opts = [
cfg.StrOpt('notification_level',
choices=[('debug', _('"debug" level')),
('info', _('"info" level')),
('warning', _('"warning" level')),
('error', _('"error" level')),
('critical', _('"critical" level'))],
help=_('Specifies the minimum level for which to send '
'notifications. If not set, no notifications will '
'be sent.')),
]


notification_group = cfg.OptGroup('notification', title='Notification Options')


def register_opts(conf):
conf.register_opts(opts, group=notification_group)
conf.set_default('notification_level', 'info', group=notification_group)
1 change: 1 addition & 0 deletions esi_leap/conf/opts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
('ironic', esi_leap.conf.ironic.list_opts()),
('keystone', esi_leap.conf.keystone.list_opts()),
('pecan', esi_leap.conf.pecan.opts),
('notification', esi_leap.conf.notification.opts),
]


Expand Down
35 changes: 35 additions & 0 deletions esi_leap/objects/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,38 @@ class StringField(object_fields.StringField):

class UUIDField(object_fields.UUIDField):
pass


class NotificationLevel(object_fields.Enum):
DEBUG = 'debug'
INFO = 'info'
WARNING = 'warning'
ERROR = 'error'
CRITICAL = 'critical'

ALL = (DEBUG, INFO, WARNING, ERROR, CRITICAL)

def __init__(self):
super(NotificationLevel, self).__init__(
valid_values=NotificationLevel.ALL)


class NotificationLevelField(object_fields.BaseEnumField):
AUTO_TYPE = NotificationLevel()


class NotificationStatus(object_fields.Enum):
START = 'start'
END = 'end'
ERROR = 'error'
SUCCESS = 'success'

ALL = (START, END, ERROR, SUCCESS)

def __init__(self):
super(NotificationStatus, self).__init__(
valid_values=NotificationStatus.ALL)


class NotificationStatusField(object_fields.BaseEnumField):
AUTO_TYPE = NotificationStatus()
Loading

0 comments on commit 3e7a470

Please sign in to comment.