Source code for zhmcclient._metrics

# Copyright 2017 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.

"""
The HMC supports the retrieval of metrics values for resources of a IBM Z
or LinuxONE computer. This section describes the zhmcclient API for retrieving
such metrics from the HMC.

A resource termed :term:`Metrics Context` is associated with any metrics
retrieval. These resources are user-created definitions of the kinds of metrics
that are intended to be retrieved. A metrics context mostly defines the names
of the metric groups to be retrieved. The available metric groups are described
in section 'Metric Groups' in the :term:`HMC API` book.

The zhmcclient API for metrics provides access to the metric values and to
their definitions, so that clients using the metric values do not need to have
intimate knowledge about the specific metric values when just displaying them.

The basic usage of the metrics API is shown in this example:

.. code-block:: python

    # Create a Metrics Context for the desired metric groups:
    metric_groups = ['dpm-system-usage-overview', 'partition-usage']
    mc = client.metrics_contexts.create(
        {'anticipated-frequency-seconds': 15,
         'metric-groups': metric_groups})

    # Retrieve the current metric values:
    mr_str = mc.get_metrics()

    # Display the metric values:
    print("Current metric values:")
    mr = zhmcclient.MetricsResponse(mc, mr_str)
    for mg in mr.metric_groups:
        mg_name = mg.name
        mg_def = mc.metric_group_definitions[mg_name]
        print("  Metric group: {}".format(mg_name))
        for ov in mg.object_values:
            print("    Resource: {}".format(ov.resource_uri))
            print("    Timestamp: {}".format(ov.timestamp))
            print("    Metric values:")
            for m_name in ov.metrics:
                m_value = ov.metrics[m_name]
                m_def = mg_def.metric_definitions[m_name]
                m_unit = m_def.unit
                m_type = m_def.type
                print("      {:30}  {} {}".
                      format(m_name, m_value, m_unit.encode('utf-8')))

    # Delete the Metrics Context:
    mc.delete()
"""

from __future__ import absolute_import

from collections import namedtuple
import re
from datetime import datetime
import pytz
import six

from ._manager import BaseManager
from ._resource import BaseResource
from ._logging import logged_api_call
from ._exceptions import NotFound
from ._utils import datetime_from_timestamp, repr_list

__all__ = ['MetricsContextManager', 'MetricsContext', 'MetricGroupDefinition',
           'MetricDefinition', 'MetricsResponse', 'MetricGroupValues',
           'MetricObjectValues']


[docs]class MetricsContextManager(BaseManager): """ Manager providing access to the :term:`Metrics Context` resources that were created through this manager object. Derived from :class:`~zhmcclient.BaseManager`; see there for common methods and attributes. Objects of this class are not directly created by the user; they are accessible via the :attr:`~zhmcclient.Client.metrics_contexts` attribute of the :class:`~zhmcclient.Client` object connected to the HMC. """ def __init__(self, client): # This function should not go into the docs. # Parameters: # client (:class:`~zhmcclient.Client`): # Client object for the HMC to be used. super(MetricsContextManager, self).__init__( resource_class=MetricsContext, class_name='', session=client.session, parent=None, base_uri='/api/services/metrics/context', oid_prop='', uri_prop='metrics-context-uri', name_prop='', query_props=[]) self._client = client self._metrics_contexts = []
[docs] def __repr__(self): """ Return a string with the state of this manager object, for debug purposes. """ ret = ( "{classname} at 0x{id:08x} (\n" " _resource_class={_resource_class!r},\n" " _class_name={_class_name!r},\n" " _session={_session_classname} at 0x{_session_id:08x},\n" " _parent={_parent_classname} at 0x{_parent_id:08x},\n" " _base_uri={_base_uri!r},\n" " _oid_prop={_oid_prop!r},\n" " _uri_prop={_uri_prop!r},\n" " _name_prop={_name_prop!r},\n" " _query_props={_query_props},\n" " _list_has_name={_list_has_name!r},\n" " _name_uri_cache={_name_uri_cache!r},\n" " _client={_client_classname} at 0x{_client_id:08x},\n" " _metrics_contexts={_metrics_contexts},\n" ")".format( classname=self.__class__.__name__, id=id(self), _resource_class=self._resource_class, _class_name=self._class_name, _session_classname=self._session.__class__.__name__, _session_id=id(self._session), _parent_classname=self._parent.__class__.__name__, _parent_id=id(self._parent), _base_uri=self._base_uri, _oid_prop=self._oid_prop, _uri_prop=self._uri_prop, _name_prop=self._name_prop, _query_props=repr_list(self._query_props, indent=2), _list_has_name=self._list_has_name, _name_uri_cache=self._name_uri_cache, _client_classname=self._client.__class__.__name__, _client_id=id(self._client), _metrics_contexts=repr_list(self._metrics_contexts, indent=2), )) return ret
@property def client(self): """ :class:`~zhmcclient.Client`: The client defining the scope for this manager. """ return self._client
[docs] @logged_api_call def list(self, full_properties=False): """ List the :term:`Metrics Context` resources that were created through this manager object. Note that the HMC does not provide a way to enumerate the existing :term:`Metrics Context` resources. Therefore, this method will only list the :term:`Metrics Context` resources that were created through this manager object. For example, :term:`Metrics Context` resources created through a second :class:`~zhmcclient.Client` object will not be listed. Parameters: full_properties (bool): This parameter exists for compatibility with other resource classes, but for this class, it has no effect on the result. Returns: : A list of :class:`~zhmcclient.MetricsContext` objects. Raises: :exc:`~zhmcclient.HTTPError` :exc:`~zhmcclient.ParseError` :exc:`~zhmcclient.AuthError` :exc:`~zhmcclient.ConnectionError` """ return self._metrics_contexts
[docs] @logged_api_call def create(self, properties): """ Create a :term:`Metrics Context` resource in the HMC this client is connected to. Parameters: properties (dict): Initial property values. Allowable properties are defined in section 'Request body contents' in section 'Create Metrics Context' in the :term:`HMC API` book. Returns: :class:`~zhmcclient.MetricsContext`: The resource object for the new :term:`Metrics Context` resource. Raises: :exc:`~zhmcclient.HTTPError` :exc:`~zhmcclient.ParseError` :exc:`~zhmcclient.AuthError` :exc:`~zhmcclient.ConnectionError` """ result = self.session.post('/api/services/metrics/context', body=properties) mc_properties = properties.copy() mc_properties.update(result) new_metrics_context = MetricsContext(self, result['metrics-context-uri'], None, mc_properties) self._metrics_contexts.append(new_metrics_context) return new_metrics_context
[docs]class MetricsContext(BaseResource): """ Representation of a :term:`Metrics Context` resource. A :term:`Metrics Context` resource specifies a list of metrics groups for which the current metric values can be retrieved using the :meth:`~zhmcclient.MetricsContext.get_metrics` method. The properties of this resource are the response fields described for the 'Create Metrics Context' operation in the :term:`HMC API` book. This class is derived from :class:`~zhmcclient.BaseResource`; see there for common methods and attributes. Objects of this class can be created by the user with the :meth:`zhmcclient.MetricsContextManager.create` method. """ def __init__(self, manager, uri, name=None, properties=None): # This function should not go into the docs. # manager (:class:`~zhmcclient.MetricsContextManager`): # Manager object for this resource object. # uri (string): # Canonical URI path of the resource. # name (string): # Name of the resource. # properties (dict): # Properties to be set for this resource object. May be `None` or # empty. if not isinstance(manager, MetricsContextManager): raise AssertionError( "MetricsContext init: Expected manager type %s, got %s" % (MetricsContextManager, type(manager))) super(MetricsContext, self).__init__(manager, uri, name, properties) self._metric_group_definitions = self._setup_metric_group_definitions() def _setup_metric_group_definitions(self): """ Return the dict of MetricGroupDefinition objects for this metrics context, by processing its 'metric-group-infos' property. """ # Dictionary of MetricGroupDefinition objects, by metric group name metric_group_definitions = dict() for mg_info in self.properties['metric-group-infos']: mg_name = mg_info['group-name'] mg_def = MetricGroupDefinition( name=mg_name, resource_class=_resource_class_from_group(mg_name), metric_definitions=dict()) for i, m_info in enumerate(mg_info['metric-infos']): m_name = m_info['metric-name'] m_def = MetricDefinition( index=i, name=m_name, type=_metric_type(m_info['metric-type']), unit=_metric_unit_from_name(m_name)) mg_def.metric_definitions[m_name] = m_def metric_group_definitions[mg_name] = mg_def return metric_group_definitions @property def metric_group_definitions(self): """ dict: The metric definitions for the metric groups of this :term:`Metrics Context` resource, as a dictionary of :class:`~zhmcclient.MetricGroupDefinition` objects, by metric group name. """ return self._metric_group_definitions
[docs] @logged_api_call def get_metrics(self): """ Retrieve the current metric values for this :term:`Metrics Context` resource from the HMC. The metric values are returned by this method as a string in the `MetricsResponse` format described with the 'Get Metrics' operation in the :term:`HMC API` book. The :class:`~zhmcclient.MetricsResponse` class can be used to process the `MetricsResponse` string returned by this method, and provides structured access to the metrics values. Returns: :term:`string`: The current metric values, in the `MetricsResponse` string format. Raises: :exc:`~zhmcclient.HTTPError` :exc:`~zhmcclient.ParseError` :exc:`~zhmcclient.AuthError` :exc:`~zhmcclient.ConnectionError` """ metrics_response = self.manager.session.get(self.uri) return metrics_response
[docs] @logged_api_call def delete(self): """ Delete this :term:`Metrics Context` resource. Raises: :exc:`~zhmcclient.HTTPError` :exc:`~zhmcclient.ParseError` :exc:`~zhmcclient.AuthError` :exc:`~zhmcclient.ConnectionError` """ # pylint: disable=protected-access self.manager.session.delete(self.uri) self.manager._metrics_contexts.remove(self)
_MetricGroupDefinitionTuple = namedtuple( '_MetricGroupDefinitionTuple', ['name', 'resource_class', 'metric_definitions'] )
[docs]class MetricGroupDefinition(_MetricGroupDefinitionTuple): """ A :func:`namedtuple <py:collections.namedtuple>` representing definitional information for a metric group. """ def __new__(cls, name, resource_class, metric_definitions): """ Parameters: name (:term:`string`): Metric group name, as defined in section 'Metric groups' in the :term:`HMC API` book. resource_class (:term:`string`): A string identifying the resource class to which this metric group belongs, using the values from the 'class' property of resource objects. metric_definitions (dict): Metric definitions for the metrics in this metric group, as a dictionary where the key is the metric name and the value is the :class:`~zhmcclient.MetricDefinition` object for the metric. All these parameters are also available as same-named attributes. """ self = super(MetricGroupDefinition, cls).__new__( cls, name, resource_class, metric_definitions) return self __slots__ = () def __repr__(self): repr_str = "MetricGroupDefinition(" \ "name={s.name!r}, " \ "resource_class={s.resource_class!r}, " \ "metric_definitions={s.metric_definitions!r})". \ format(s=self) return repr_str
_MetricDefinitionTuple = namedtuple( '_MetricDefinitionTuple', ['index', 'name', 'type', 'unit'] )
[docs]class MetricDefinition(_MetricDefinitionTuple): """ A :func:`namedtuple <py:collections.namedtuple>` representing definitional information for a single metric. """ def __new__(cls, index, name, type, unit): # pylint: disable=redefined-builtin """ Parameters: index (:term:`integer`): 0-based index (=position) of the metric in a MetricsResponse value row. name (:term:`string`): Metric field name, as shown in the tables defining the metric groups in section 'Metric groups' in the :term:`HMC API` book. type (:term:`callable`): Python type for the metric value. The type must be a constructor (callable) that takes the metrics value from the `MetricsResponse` string as its only argument, using the following Python types for the metric group description types shown in the :term:`HMC API` book: ============================= ====================== Description type Python type ============================= ====================== Boolean :class:`py:bool` Byte :term:`integer` Short :term:`integer` Integer :term:`integer` Long :term:`integer` Double :class:`py:float` String, String Enum :term:`unicode string` ============================= ====================== unit (:term:`string`): Unit of the metric value. All these parameters are also available as same-named attributes. """ self = super(MetricDefinition, cls).__new__( cls, index, name, type, unit) return self __slots__ = () def __repr__(self): repr_str = "MetricDefinition(" \ "index={s.index!r}, " \ "name={s.name!r}, " \ "type={s.type!r}, " \ "unit={s.unit!r})". \ format(s=self) return repr_str
def _metric_type(metric_type_name): """ Return a constructor callable for the given metric type name. The returned callable takes the metric value as a string as its only argument and returns a Python object representing that metric value using the correct Python type. """ return _METRIC_TYPES_BY_NAME[metric_type_name] _METRIC_TYPES_BY_NAME = { 'boolean-metric': bool, 'byte-metric': int, 'short-metric': int, 'integer-metric': int, 'long-metric': int, 'double-metric': float, 'string-metric': six.text_type, } def _metric_value(value_str, metric_type): """ Return a Python-typed metric value from a metric value string. """ if metric_type in (int, float): try: return metric_type(value_str) except ValueError: raise ValueError("Invalid {} metric value: {!r}". format(metric_type.__class__.__name__, value_str)) elif metric_type is six.text_type: # In Python 3, decode('unicode_escape) requires bytes, so we need # to encode to bytes. This also works in Python 2. return value_str.strip('"').encode('utf-8').decode('unicode_escape') else: assert metric_type is bool lower_str = value_str.lower() if lower_str == 'true': return True if lower_str == 'false': return False raise ValueError("Invalid boolean metric value: {!r}".format(value_str)) def _metric_unit_from_name(metric_name): """ Return a metric unit string for human consumption, that is inferred from the metric name. If a unit cannot be inferred, `None` is returned. """ for item in _PATTERN_UNIT_LIST: pattern, unit = item if pattern.match(metric_name): return unit return None _USE_UNICODE = True if _USE_UNICODE: MICROSECONDS = u"\u00b5s" # U+00B5 = Micro Sign CELSIUS = u"\u00B0C" # U+00B0 = Degree Sign # Note: Use of U+2103 (Degree Celsius) is discouraged by Unicode standard else: MICROSECONDS = u"us" CELSIUS = u"degree Celsius" # Official SI unit when not using degree sign _PATTERN_UNIT_LIST = { # End patterns: (re.compile(r".+-usage$"), u"%"), (re.compile(r".+-time$"), MICROSECONDS), (re.compile(r".+-time-used$"), MICROSECONDS), (re.compile(r".+-celsius$"), CELSIUS), (re.compile(r".+-watts$"), u"W"), (re.compile(r".+-paging-rate$"), u"pages/s"), (re.compile(r".+-sampling-rate$"), u"samples/s"), # Begin patterns: (re.compile(r"^bytes-.+"), u"B"), (re.compile(r"^heat-load.+"), u"BTU/h"), # Note: No trailing hyphen (re.compile(r"^interval-bytes-.+"), u"B"), (re.compile(r"^bytes-per-second-.+"), u"B/s"), # Special cases: (re.compile(r"^storage-rate$"), u"kB/s"), (re.compile(r"^humidity$"), u"%"), (re.compile(r"^memory-used$"), u"MiB"), (re.compile(r"^policy-activation-time$"), u""), # timestamp (re.compile(r"^velocity-numerator$"), MICROSECONDS), (re.compile(r"^velocity-denominator$"), MICROSECONDS), (re.compile(r"^utilization$"), u"%"), } def _resource_class_from_group(metric_group_name): """ Return the resource class string from the metric group name. Metric groups for resources that are specific to ensemble mode are not supported. Returns an empty string if a metric group name is unknown. """ return _CLASS_FROM_GROUP.get(metric_group_name, '') _CLASS_FROM_GROUP = { # DPM mode only: 'dpm-system-usage-overview': 'cpc', 'partition-usage': 'partition', 'adapter-usage': 'adapter', 'network-physical-adapter-port': 'adapter', 'partition-attached-network-interface': 'nic', # Classic mode only: 'cpc-usage-overview': 'cpc', 'logical-partition-usage': 'logical-partition', 'channel-usage': 'cpc', 'crypto-usage': 'cpc', 'flash-memory-usage': 'cpc', # TODO: verify CPC mode dependency 'roce-usage': 'cpc', # TODO: verify CPC mode dependency # DPM mode or classic mode: 'zcpc-environmentals-and-power': 'cpc', 'zcpc-processor-usage': 'cpc', }
[docs]class MetricsResponse(object): """ Represents the metric values returned by one call to the :meth:`~zhmcclient.MetricsContext.get_metrics` method, and provides structured access to the data. """ def __init__(self, metrics_context, metrics_response_str): """ Parameters: metrics_context (:class:`~zhmcclient.MetricsContext`): The :class:`~zhmcclient.MetricsContext` object that was used to retrieve the metrics response string. It defines the structure of the metric values in the metrics response string. metrics_response_str (:term:`string`): The metrics response string, as returned by the :meth:`~zhmcclient.MetricsContext.get_metrics` method. """ self._metrics_context = metrics_context self._metrics_response_str = metrics_response_str self._client = self._metrics_context.manager.client self._metric_group_values = self._setup_metric_group_values() def _setup_metric_group_values(self): """ Return the list of MetricGroupValues objects for this metrics response, by processing its metrics response string. The lines in the metrics response string are:: MetricsResponse: MetricsGroup{0,*} <emptyline> a third empty line at the end MetricsGroup: MetricsGroupName ObjectValues{0,*} <emptyline> a second empty line after each MG ObjectValues: ObjectURI Timestamp ValueRow{1,*} <emptyline> a first empty line after this blk """ mg_defs = self._metrics_context.metric_group_definitions metric_group_name = None resource_uri = None dt_timestamp = None object_values = None metric_group_values = list() state = 0 for mr_line in self._metrics_response_str.splitlines(): if state == 0: if object_values is not None: # Store the result from the previous metric group mgv = MetricGroupValues(metric_group_name, object_values) metric_group_values.append(mgv) object_values = None if mr_line == '': # Skip initial (or trailing) empty lines pass else: # Process the next metrics group metric_group_name = mr_line.strip('"') # No " or \ inside assert metric_group_name in mg_defs m_defs = mg_defs[metric_group_name].metric_definitions object_values = list() state = 1 elif state == 1: if mr_line == '': # There are no (or no more) ObjectValues items in this # metrics group state = 0 else: # There are ObjectValues items resource_uri = mr_line.strip('"') # No " or \ inside state = 2 elif state == 2: # Process the timestamp assert mr_line != '' try: dt_timestamp = datetime_from_timestamp(int(mr_line)) except ValueError: # Sometimes, the returned epoch timestamp values are way # too large, e.g. 3651584404810066 (which would translate # to the year 115791 A.D.). Python datetime supports # up to the year 9999. We circumvent this issue by # simply using the current date&time. # TODO: Remove the circumvention for too large timestamps. dt_timestamp = datetime.now(pytz.utc) state = 3 elif state == 3: if mr_line != '': # Process the metric values in the ValueRow line str_values = mr_line.split(',') metrics = dict() for m_name in m_defs: m_def = m_defs[m_name] m_type = m_def.type m_value_str = str_values[m_def.index] m_value = _metric_value(m_value_str, m_type) metrics[m_name] = m_value ov = MetricObjectValues( self._client, mg_defs[metric_group_name], resource_uri, dt_timestamp, metrics) object_values.append(ov) # stay in this state, for more ValueRow lines else: # On the empty line after the last ValueRow line state = 1 return metric_group_values @property def metrics_context(self): """ :class:`~zhmcclient.MetricsContext` object for this metric response. This can be used to access the metric definitions for this response. """ return self._metrics_context @property def metric_group_values(self): """ :class:`py:list`: The list of :class:`~zhmcclient.MetricGroupValues` objects representing the metric groups in this metric response. Each :class:`~zhmcclient.MetricGroupValues` object contains a list of :class:`~zhmcclient.MetricObjectValues` objects representing the metric values in this group (each for a single resource and point in time). """ return self._metric_group_values
[docs]class MetricGroupValues(object): """ Represents the metric values for a metric group in a MetricsResponse string. """ def __init__(self, name, object_values): """ Parameters: name (:term:`string`): Metric group name. object_values (:class:`py:list`): The :class:`~zhmcclient.MetricObjectValues` objects in this metric group. Each of them represents the metric values for a single resource at a single point in time. """ self._name = name self._object_values = object_values @property def name(self): """ string: The metric group name. """ return self._name @property def object_values(self): """ :class:`py:list`: The :class:`~zhmcclient.MetricObjectValues` objects in this metric group. Each of them represents the metric values for a single resource at a single point in time. """ return self._object_values
[docs]class MetricObjectValues(object): """ Represents the metric values for a single resource at a single point in time. """ def __init__(self, client, metric_group_definition, resource_uri, timestamp, metrics): """ Parameters: client (:class:`~zhmcclient.Client`): Client object, for retrieving the actual resource. metric_group_definition (:class:`~zhmcclient.MetricGroupDefinition`): Metric group definition for this set of metric values. resource_uri (:term:`string`): Resource URI of the resource these metric values apply to. timestamp (:class:`py:datetime.datetime`): Point in time when the HMC captured these metric values (as a timezone-aware datetime object). metrics (dict): The metric values, as a dictionary of the (Python typed) metric values, by metric name. """ self._client = client self._metric_group_definition = metric_group_definition self._resource_uri = resource_uri self._timestamp = timestamp self._metrics = metrics self._resource = None # Lazy initialization @property def client(self): """ :class:`~zhmcclient.Client`: Client object, for retrieving the actual resource. """ return self._client @property def metric_group_definition(self): """ :class:`~zhmcclient.MetricGroupDefinition`: Metric group definition for this set of metric values. """ return self._metric_group_definition @property def resource_uri(self): """ string: The canonical URI path of the resource these metric values apply to. Example: ``/api/cpcs/12345`` """ return self._resource_uri @property def timestamp(self): """ :class:`py:datetime.datetime`: Point in time when the HMC captured these metric values (as a timezone-aware datetime object). """ return self._timestamp @property def metrics(self): """ dict: The metric values, as a dictionary of the (Python typed) metric values, by metric name. """ return self._metrics @property def resource(self): """ :class:`~zhmcclient.BaseResource`: The Python resource object of the resource these metric values apply to. Raises: :exc:`~zhmcclient.NotFound`: No resource found for this URI in the management scope of the HMC. """ if self._resource is not None: return self._resource resource_class = self.metric_group_definition.resource_class resource_uri = self.resource_uri if resource_class == 'cpc': filter_args = {'object-uri': resource_uri} resource = self.client.cpcs.find(**filter_args) elif resource_class == 'logical-partition': for cpc in self.client.cpcs.list(): try: filter_args = {'object-uri': resource_uri} resource = cpc.lpars.find(**filter_args) break except NotFound: pass # Try next CPC else: raise AssertionError("No CPC found") elif resource_class == 'partition': for cpc in self.client.cpcs.list(): try: filter_args = {'object-uri': resource_uri} resource = cpc.partitions.find(**filter_args) break except NotFound: pass # Try next CPC else: raise AssertionError("No CPC found") elif resource_class == 'adapter': for cpc in self.client.cpcs.list(): try: filter_args = {'object-uri': resource_uri} resource = cpc.adapters.find(**filter_args) break except NotFound: pass # Try next CPC else: raise AssertionError("No CPC found") elif resource_class == 'nic': for cpc in self.client.cpcs.list(): found = False for partition in cpc.partitions.list(): try: filter_args = {'element-uri': resource_uri} resource = partition.nics.find(**filter_args) found = True break except NotFound: pass # Try next partition / next CPC if found: break else: raise AssertionError("No CPC found") else: raise ValueError( "Invalid resource class: {!r}".format(resource_class)) self._resource = resource return self._resource