#!/usr/bin/env python
# Copyright 2021 IBM Corp. 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.
"""
Resource updater for
:ref:`auto-updating of resources <Auto-updating of resources>`.
"""
import logging
import json
try:
from json import JSONDecodeError as _JSONDecodeError
except ImportError:
_JSONDecodeError = ValueError
from ._logging import logged_api_call
from ._constants import DEFAULT_STOMP_PORT, JMS_LOGGER_NAME
__all__ = ['ResourceUpdater']
JMS_LOGGER = logging.getLogger(JMS_LOGGER_NAME)
[docs]class ResourceUpdater(object):
"""
A class that updates the properties of zhmcclient resource objects that are
enabled for auto-updating.
**Experimental:** This class is considered experimental at this point, and
its API may change incompatibly as long as it is experimental.
Note: The user should not create any objects of this class nor invoke any
methods of this class, because the objects are created automatically
when a :class:`~zhmcclient.Session` object is subscribed for
auto-update (via its :meth:`~zhmcclient.Session.subscribe_auto_update`
method).
Creating an object of this class establishes a JMS session with the HMC and
subscribes for the object notification topic of the session. This causes
the HMC to emit status notifications, property notifications, and inventory
notifications, which are processed by this class and cause the properties of
zhmcclient resource objects to be updated that are registered to this class
as a result of enabling auto-update for the resorce object
(via their :meth:`~zhmcclient.BaseResource.enable_auto_update` method).
Zhmcclient resource objects that are not enabled for auto-updating remain
unchanged.
"""
def __init__(self, session):
"""
Parameters:
session (:class:`~zhmcclient.Session`): Session for which the
resource updater should do its work. This defines the HMC host
and credentials that are used to establish the JMS session with
the HMC.
"""
# Registered resource objects, as:
# dict(key: uri, value: dict(key: id, value: object))
self._registered_objects = {}
# Subscription ID. We use some value that allows to identify on the
# HMC that this is the zhmcclient, but otherwise we are not using
# this value ourselves.
self._sub_id = 'zhmcclient.%s' % id(self)
# Lazy importing for stomp, because it is so slow (ca. 5 sec)
if 'Stomp_Connection' not in globals():
# pylint: disable=import-outside-toplevel
from stomp import Connection as Stomp_Connection
self._conn = Stomp_Connection(
[(session.host, DEFAULT_STOMP_PORT)], use_ssl="SSL")
listener = _UpdateListener(self, session)
self._conn.set_listener('', listener)
# pylint: disable=protected-access
self._conn.connect(session.userid, session._password, wait=True)
dest = "/topic/" + session.object_topic
self._conn.subscribe(destination=dest, id=self._sub_id, ack='auto')
JMS_LOGGER.info(
"JMS session for object notification topic '%s' has been "
"established", session.object_topic)
[docs] @logged_api_call
def close(self):
"""
Disconnect and close the JMS session with the HMC.
This implicitly unsubscribes from the object notification topic this
updater was created for.
"""
self._conn.disconnect()
[docs] def register_object(self, resource_obj):
"""
Register a resource object to this resource updater.
If this resource object (by id) is already registered, nothing is done.
"""
res_uri = resource_obj.uri
res_id = id(resource_obj)
if res_uri not in self._registered_objects:
self._registered_objects[res_uri] = {}
id_dict = self._registered_objects[res_uri]
if res_id not in id_dict:
id_dict[res_id] = resource_obj
[docs] def unregister_object(self, resource_obj):
"""
Unregister a resource object from this resource updater.
If this resource object (by id) is already unregistered, nothing is
done.
"""
res_uri = resource_obj.uri
res_id = id(resource_obj)
if res_uri in self._registered_objects:
id_dict = self._registered_objects[res_uri]
if res_id in id_dict:
del id_dict[res_id]
if not id_dict:
del self._registered_objects[res_uri]
[docs] def registered_objects(self, resource_uri):
"""
Generator that yields the resource objects for the specified URI.
"""
if resource_uri in self._registered_objects:
id_dict = self._registered_objects[resource_uri]
for res_obj in id_dict.values():
yield res_obj
[docs] def has_objects(self):
"""
Return boolean indicating whether there are any resource objects
registered.
"""
return bool(self._registered_objects)
class _UpdateListener(object):
# pylint: disable=too-few-public-methods
"""
A notification listener class for use by the Python `stomp` package.
This is an internal class that does not need to be accessed or created by
the user. An object of this class is automatically created by the
:class:`~zhmcclient.ResourceUpdater` class, for its notification
topic.
Note: In the stomp examples, this class inherits from
stomp.ConnectionListener. However, since that class defines only empty
methods and since we want to import the stomp module in a lazy manner,
we are not using that class, and stomp does not require us to.
"""
def __init__(self, updater, session):
self._updater = updater
self._session = session
def on_message(self, headers, message):
"""
Event method that gets called when this listener has received a JMS
message (representing an HMC notification).
Parameters:
headers (dict): JMS message headers, see HMC API book.
message (string): JMS message body as a string, which contains a
serialized JSON object, see HMC API book.
"""
try:
uri = headers['object-uri']
except KeyError:
try:
uri = headers['element-uri']
except KeyError:
JMS_LOGGER.error(
"JMS message for object notification topic '%s' "
"has no URI field in its headers (ignored): %r",
self._session.object_topic, headers)
return
noti_type = headers['notification-type']
if noti_type == 'property-change':
try:
msg_obj = json.loads(message)
except _JSONDecodeError:
JMS_LOGGER.error(
"JMS message for object notification topic '%s' "
"has a non-JSON message body (ignored): %r",
self._session.object_topic, message)
return
JMS_LOGGER.debug(
"JMS message for property change notification for topic '%s' "
"for resource %s with change reports: %r",
self._session.object_topic, uri, msg_obj['change-reports'])
# Build the latest values from all change records. They are ordered
# old to new.
new_props = {}
for cr in msg_obj['change-reports']:
new_props[cr['property-name']] = cr['new-value']
for obj in self._updater.registered_objects(uri):
if obj.auto_update_enabled():
obj.update_properties_local(new_props)
elif noti_type == 'status-change':
try:
msg_obj = json.loads(message)
except _JSONDecodeError:
JMS_LOGGER.error(
"JMS message for object notification topic '%s' "
"has a non-JSON message body (ignored): %r",
self._session.object_topic, message)
return
JMS_LOGGER.debug(
"JMS message for status change notification for topic '%s' "
"for resource %s with change reports: %r",
self._session.object_topic, uri, msg_obj['change-reports'])
# Build the latest values from all change records. They are ordered
# old to new.
new_props = {}
for cr in msg_obj['change-reports']:
if 'new-status' in cr:
new_props['status'] = cr['new-status']
if 'new-additional-status' in cr:
new_props['additional-status'] = cr['new-additional-status']
if 'has-unacceptable-status' in cr:
new_props['has-unacceptable-status'] = \
cr['has-unacceptable-status']
for obj in self._updater.registered_objects(uri):
if obj.auto_update_enabled():
obj.update_properties_local(new_props)
elif noti_type == 'inventory-change':
action = headers['action']
JMS_LOGGER.debug(
"JMS message for inventory change notification for topic '%s' "
"for resource %s with action: %r",
self._session.object_topic, uri, action)
if action == 'remove':
for obj in self._updater.registered_objects(uri):
if obj.auto_update_enabled():
obj.cease_existence_local()
else:
JMS_LOGGER.warning(
"JMS message for notification of type %s for topic '%s' "
"for resource %s is ignored",
noti_type, self._session.object_topic, uri)
def on_error(self, headers, message):
# pylint: disable=unused-argument
"""
Event method that gets called when this listener has received a JMS
error. This happens for example when the client registers for a
non-existing topic.
Parameters:
headers (dict): JMS message headers.
message (string): JMS message body as a string, which contains a
serialized JSON object.
"""
JMS_LOGGER.error(
"JMS error message received for object notification topic '%s' "
"(ignored): %s",
self._session.object_topic, message)
def on_disconnected(self):
"""
Event method that gets called when the JMS session has been
disconnected.
"""
JMS_LOGGER.info(
"JMS session for object notification topic '%s' has been "
"disconnected",
self._session.object_topic)