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

add lease_resource_event object
  • Loading branch information
DanNiESh committed Jun 3, 2023
1 parent be35c2d commit 8753b62
Show file tree
Hide file tree
Showing 28 changed files with 1,513 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\"")
147 changes: 147 additions & 0 deletions esi_leap/common/notification_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# 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_notification(context, obj, action, level, status,
crud_notify_obj, **kwargs):
"""Helper for emitting 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__
extra_args = kwargs
exception_values = {}
exception_message = None
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_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_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_notification(context, obj, action,
fields.NotificationLevel.INFO,
fields.NotificationStatus.END,
crud_notify_obj,
**kwargs)
97 changes: 97 additions & 0 deletions esi_leap/common/rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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 esi_leap.conf import CONF
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__,
]


def init(conf):
global NOTIFICATION_TRANSPORT
global VERSIONED_NOTIFIER
NOTIFICATION_TRANSPORT = messaging.get_notification_transport(
conf,
allowed_remote_exmods=ALLOWED_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=CONF.notification.versioned_notifications_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


# 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)
40 changes: 40 additions & 0 deletions esi_leap/conf/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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.')),
cfg.ListOpt(
'versioned_notifications_topics',
default=['esi_leap_versioned_notifications'],
help=_('Specifies the topics for '
'the versioned notifications issued by esi-leap.')),
]


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 8753b62

Please sign in to comment.