773 lines
26 KiB
Python
773 lines
26 KiB
Python
# Copyright 2017 Red Hat, Inc.
|
|
# 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.
|
|
|
|
import abc
|
|
import collections
|
|
import copy
|
|
import io
|
|
import json
|
|
import logging
|
|
import re
|
|
import zipfile
|
|
|
|
import pkg_resources
|
|
|
|
from sushy import exceptions
|
|
from sushy.resources import mappings as res_maps
|
|
from sushy.resources import oem
|
|
from sushy import utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Field(object):
|
|
"""Definition for fields fetched from JSON."""
|
|
|
|
def __init__(self, path, required=False, default=None,
|
|
adapter=lambda x: x):
|
|
"""Create a field definition.
|
|
|
|
:param path: JSON field to fetch the value from. Either a string,
|
|
or a list of strings in case of a nested field.
|
|
:param required: whether this field is required. Missing required,
|
|
but not defaulted, fields result in MissingAttributeError.
|
|
:param default: the default value to use when the field is missing.
|
|
:param adapter: a function to call to transform and/or validate
|
|
the received value. UnicodeError, ValueError or TypeError from
|
|
this call are reraised as MalformedAttributeError.
|
|
"""
|
|
if not callable(adapter):
|
|
raise TypeError("Adapter must be callable")
|
|
|
|
if not isinstance(path, list):
|
|
path = [path]
|
|
|
|
elif not path:
|
|
raise ValueError('Path cannot be empty')
|
|
|
|
self._path = path
|
|
self._required = required
|
|
self._default = default
|
|
self._adapter = adapter
|
|
|
|
def _get_item(self, dct, key_or_callable, **context):
|
|
if not callable(key_or_callable):
|
|
return dct[key_or_callable]
|
|
|
|
for candidate_key in dct:
|
|
if key_or_callable(
|
|
candidate_key, value=dct[candidate_key], **context):
|
|
return dct[candidate_key]
|
|
|
|
raise KeyError(key_or_callable)
|
|
|
|
def _load(self, body, resource, nested_in=None):
|
|
"""Load this field from a JSON object.
|
|
|
|
:param body: parsed JSON body.
|
|
:param resource: ResourceBase instance for which the field is loaded.
|
|
:param nested_in: parent resource path (for error reporting only),
|
|
must be a list of strings or None.
|
|
:raises: MissingAttributeError if a required field is missing
|
|
and not defaulted.
|
|
:raises: MalformedAttributeError on invalid field value or type.
|
|
:returns: loaded and verified value
|
|
"""
|
|
name = self._path[-1]
|
|
for path_item in self._path[:-1]:
|
|
body = body.get(path_item, {})
|
|
|
|
try:
|
|
item = self._get_item(body, name)
|
|
|
|
except KeyError:
|
|
if self._required:
|
|
path = (nested_in or []) + self._path
|
|
|
|
if self._default is None:
|
|
raise exceptions.MissingAttributeError(
|
|
attribute='/'.join(path),
|
|
resource=resource.path)
|
|
|
|
logging.warning(
|
|
'Applying default "%s" on required, but missing '
|
|
'attribute "%s"' % (self._default, path))
|
|
|
|
# Do not run the adapter on the default value
|
|
return self._default
|
|
|
|
# NOTE(etingof): this is just to account for schema violation
|
|
if item is None:
|
|
return
|
|
|
|
try:
|
|
return self._adapter(item)
|
|
|
|
except (UnicodeError, ValueError, TypeError) as exc:
|
|
path = (nested_in or []) + self._path
|
|
raise exceptions.MalformedAttributeError(
|
|
attribute='/'.join(path),
|
|
resource=resource.path,
|
|
error=exc)
|
|
|
|
|
|
def _collect_fields(resource):
|
|
"""Collect fields from the JSON.
|
|
|
|
:param resource: ResourceBase or CompositeField instance.
|
|
:returns: generator of tuples (key, field)
|
|
"""
|
|
for attr in dir(resource.__class__):
|
|
field = getattr(resource.__class__, attr)
|
|
if isinstance(field, Field):
|
|
yield (attr, field)
|
|
|
|
|
|
class CompositeField(collections.abc.Mapping, Field, metaclass=abc.ABCMeta):
|
|
"""Base class for fields consisting of several sub-fields."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(CompositeField, self).__init__(*args, **kwargs)
|
|
self._subfields = dict(_collect_fields(self))
|
|
|
|
def _load(self, body, resource, nested_in=None):
|
|
"""Load the composite field.
|
|
|
|
:param body: parent JSON body.
|
|
:param resource: parent resource.
|
|
:param nested_in: parent resource name (for error reporting only).
|
|
:returns: a new object with sub-fields attached to it.
|
|
"""
|
|
nested_in = (nested_in or []) + self._path
|
|
value = super(CompositeField, self)._load(body, resource)
|
|
if value is None:
|
|
return None
|
|
|
|
# We need a new instance, as this method is called a singleton instance
|
|
# that is attached to a class (not instance) of a resource or another
|
|
# CompositeField. We don't want to end up modifying this instance.
|
|
instance = copy.copy(self)
|
|
for attr, field in self._subfields.items():
|
|
# Hide the Field object behind the real value
|
|
setattr(instance, attr, field._load(value, resource, nested_in))
|
|
|
|
return instance
|
|
|
|
# Satisfy the mapping interface, see
|
|
# https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping
|
|
|
|
def __getitem__(self, key):
|
|
if key in self._subfields:
|
|
return getattr(self, key)
|
|
else:
|
|
raise KeyError(key)
|
|
|
|
def __len__(self):
|
|
return len(self._subfields)
|
|
|
|
def __iter__(self):
|
|
return iter(self._subfields)
|
|
|
|
|
|
class ListField(Field):
|
|
"""Base class for fields consisting of a list of several sub-fields."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ListField, self).__init__(*args, **kwargs)
|
|
self._subfields = dict(_collect_fields(self))
|
|
|
|
def _load(self, body, resource, nested_in=None):
|
|
"""Load the field list.
|
|
|
|
:param body: parent JSON body.
|
|
:param resource: parent resource.
|
|
:param nested_in: parent resource name (for error reporting only).
|
|
:returns: a new list object containing subfields.
|
|
"""
|
|
nested_in = (nested_in or []) + self._path
|
|
values = super(ListField, self)._load(body, resource)
|
|
if values is None:
|
|
return None
|
|
|
|
# Initialize the list that will contain each field instance
|
|
instances = []
|
|
for value in values:
|
|
instance = copy.copy(self)
|
|
for attr, field in self._subfields.items():
|
|
# Hide the Field object behind the real value
|
|
setattr(instance, attr, field._load(value,
|
|
resource,
|
|
nested_in))
|
|
instances.append(instance)
|
|
|
|
return instances
|
|
|
|
def __getitem__(self, key):
|
|
return getattr(self, key)
|
|
|
|
|
|
class LinksField(CompositeField):
|
|
"""Reference to linked resources."""
|
|
oem_vendors = Field('Oem', adapter=list)
|
|
|
|
|
|
class DictionaryField(Field):
|
|
"""Base class for fields consisting of dictionary of several sub-fields."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DictionaryField, self).__init__(*args, **kwargs)
|
|
self._subfields = dict(_collect_fields(self))
|
|
|
|
def _load(self, body, resource, nested_in=None):
|
|
"""Load the dictionary.
|
|
|
|
:param body: parent JSON body.
|
|
:param resource: parent resource.
|
|
:param nested_in: parent resource name (for error reporting only).
|
|
:returns: a new dictionary object containing subfields.
|
|
"""
|
|
nested_in = (nested_in or []) + self._path
|
|
values = super(DictionaryField, self)._load(body, resource)
|
|
if values is None:
|
|
return None
|
|
|
|
instances = {}
|
|
for key, value in values.items():
|
|
instance_value = copy.copy(self)
|
|
for attr, field in self._subfields.items():
|
|
# Hide the Field object behind the real value
|
|
setattr(instance_value, attr, field._load(value,
|
|
resource,
|
|
nested_in))
|
|
instances[key] = instance_value
|
|
|
|
return instances
|
|
|
|
def __getitem__(self, key):
|
|
return getattr(self, key)
|
|
|
|
|
|
class MappedField(Field):
|
|
"""Field taking real value from a mapping."""
|
|
|
|
def __init__(self, field, mapping, required=False, default=None):
|
|
"""Create a mapped field definition.
|
|
|
|
:param field: JSON field to fetch the value from. This can be either
|
|
a string or a list of string. In the latter case, the value will
|
|
be fetched from a nested object.
|
|
:param mapping: a mapping to take values from.
|
|
:param required: whether this field is required. Missing required,
|
|
but not defaulted, fields result in MissingAttributeError.
|
|
:param default: the default value to use when the field is missing.
|
|
This value is not matched against the mapping.
|
|
"""
|
|
if not isinstance(mapping, collections.abc.Mapping):
|
|
raise TypeError("The mapping argument must be a mapping")
|
|
|
|
super(MappedField, self).__init__(
|
|
field, required=required, default=default,
|
|
adapter=mapping.get)
|
|
|
|
|
|
class MappedListField(Field):
|
|
"""Field taking a list of values with a mapping for the values
|
|
|
|
Given JSON {'field':['xxx', 'yyy']}, a sushy resource definition and
|
|
mapping {'xxx':'a', 'yyy':'b'}, the sushy object to come out will be like
|
|
resource.field = ['a', 'b']
|
|
"""
|
|
|
|
def __init__(self, field, mapping, required=False, default=None):
|
|
"""Create a mapped list field definition.
|
|
|
|
:param field: JSON field to fetch the list of values from.
|
|
:param mapping: a mapping for the list elements.
|
|
:param required: whether this field is required. Missing required,
|
|
but not defaulted, fields result in MissingAttributeError.
|
|
:param default: the default value to use when the field is missing.
|
|
"""
|
|
if not isinstance(mapping, collections.abc.Mapping):
|
|
raise TypeError("The mapping argument must be a mapping")
|
|
|
|
self._mapping_adapter = mapping.get
|
|
super(MappedListField, self).__init__(
|
|
field, required=required, default=default,
|
|
adapter=lambda x: x)
|
|
|
|
def _load(self, body, resource, nested_in=None):
|
|
"""Load the mapped list.
|
|
|
|
:param body: parent JSON body.
|
|
:param resource: parent resource.
|
|
:param nested_in: parent resource name (for error reporting only).
|
|
:returns: a new list object containing the mapped values.
|
|
"""
|
|
nested_in = (nested_in or []) + self._path
|
|
values = super(MappedListField, self)._load(body, resource)
|
|
|
|
if values is None:
|
|
return
|
|
|
|
instances = [self._mapping_adapter(value) for value in values
|
|
if self._mapping_adapter(value) is not None]
|
|
|
|
return instances
|
|
|
|
|
|
class MessageListField(ListField):
|
|
"""List of messages with details of settings update status"""
|
|
|
|
message_id = Field('MessageId')
|
|
"""The key for this message which can be used
|
|
to look up the message in a message registry
|
|
"""
|
|
|
|
message = Field('Message')
|
|
"""Human readable message, if provided"""
|
|
|
|
severity = MappedField('Severity',
|
|
res_maps.SEVERITY_VALUE_MAP)
|
|
"""Severity of the error"""
|
|
|
|
resolution = Field('Resolution')
|
|
"""Used to provide suggestions on how to resolve
|
|
the situation that caused the error
|
|
"""
|
|
|
|
_related_properties = Field('RelatedProperties')
|
|
"""List of properties described by the message"""
|
|
|
|
message_args = Field('MessageArgs')
|
|
"""List of message substitution arguments for the message
|
|
referenced by `message_id` from the message registry
|
|
"""
|
|
|
|
|
|
class FieldData(object):
|
|
"""Contains data to be used when constructing Fields"""
|
|
|
|
def __init__(self, status_code, headers, json_doc):
|
|
"""Initializes the FieldData instance"""
|
|
self._status_code = status_code
|
|
self._headers = headers
|
|
self._json_doc = json_doc
|
|
|
|
@property
|
|
def status_code(self):
|
|
"""The status code"""
|
|
return self._status_code
|
|
|
|
@property
|
|
def headers(self):
|
|
"""The headers"""
|
|
return self._headers
|
|
|
|
@property
|
|
def json_doc(self):
|
|
"""The parsed JSON body"""
|
|
return self._json_doc
|
|
|
|
|
|
class AbstractDataReader(object, metaclass=abc.ABCMeta):
|
|
|
|
def set_connection(self, connector, path):
|
|
"""Sets mandatory connection parameters
|
|
|
|
:param connector: A Connector instance
|
|
:param path: path of the resource
|
|
"""
|
|
self._conn = connector
|
|
self._path = path
|
|
|
|
@abc.abstractmethod
|
|
def get_data(self):
|
|
"""Based on data source get data and parse to JSON"""
|
|
|
|
|
|
class JsonDataReader(AbstractDataReader):
|
|
"""Gets the data from HTTP response given by path"""
|
|
|
|
def get_data(self):
|
|
"""Gets JSON file from URI directly"""
|
|
data = self._conn.get(path=self._path)
|
|
|
|
json_data = data.json() if data.content else {}
|
|
|
|
return FieldData(data.status_code, data.headers, json_data)
|
|
|
|
|
|
class JsonPublicFileReader(AbstractDataReader):
|
|
"""Loads the data from the Internet"""
|
|
|
|
def get_data(self):
|
|
"""Get JSON file from full URI"""
|
|
data = self._conn.get(self._path)
|
|
|
|
return FieldData(data.status_code, data.headers, data.json())
|
|
|
|
|
|
class JsonArchiveReader(AbstractDataReader):
|
|
"""Gets the data from JSON file in archive"""
|
|
|
|
def __init__(self, archive_file):
|
|
"""Initializes the reader
|
|
|
|
:param archive_file: file name of JSON file in archive
|
|
"""
|
|
self._archive_file = archive_file
|
|
|
|
def get_data(self):
|
|
"""Gets JSON file from archive. Currently supporting ZIP only"""
|
|
|
|
data = self._conn.get(path=self._path)
|
|
if data.headers.get('content-type') == 'application/zip':
|
|
try:
|
|
archive = zipfile.ZipFile(io.BytesIO(data.content))
|
|
json_data = json.loads(archive.read(self._archive_file)
|
|
.decode(encoding='utf-8'))
|
|
return FieldData(data.status_code, data.headers, json_data)
|
|
except (zipfile.BadZipfile, ValueError) as e:
|
|
raise exceptions.ArchiveParsingError(
|
|
path=self._path, error=e)
|
|
else:
|
|
LOG.error('Support for %(type)s not implemented',
|
|
{'type': data.headers['content-type']})
|
|
|
|
return FieldData(data.status_code, data.headers, None)
|
|
|
|
|
|
class JsonPackagedFileReader(AbstractDataReader):
|
|
"""Gets the data from packaged file given by path"""
|
|
|
|
def __init__(self, resource_package_name):
|
|
"""Initializes the reader
|
|
|
|
:param resource_package: Python package/module name
|
|
"""
|
|
self._resource_package_name = resource_package_name
|
|
|
|
def get_data(self):
|
|
"""Gets JSON file from packaged file denoted by path"""
|
|
|
|
with pkg_resources.resource_stream(self._resource_package_name,
|
|
self._path) as resource:
|
|
json_data = json.loads(resource.read().decode(encoding='utf-8'))
|
|
return FieldData(None, None, json_data)
|
|
|
|
|
|
def get_reader(connector, path, reader=None):
|
|
"""Create and configure the reader.
|
|
|
|
:param connector: A Connector instance
|
|
:param path: sub-URI path to the resource.
|
|
:param reader: Reader to use to fetch JSON data.
|
|
:returns: the reader
|
|
"""
|
|
if reader is None:
|
|
reader = JsonDataReader()
|
|
reader.set_connection(connector, path)
|
|
|
|
return reader
|
|
|
|
|
|
class ResourceBase(object, metaclass=abc.ABCMeta):
|
|
|
|
redfish_version = None
|
|
"""The Redfish version"""
|
|
|
|
_oem_vendors = Field('Oem', adapter=list)
|
|
"""The list of OEM extension names for this resource."""
|
|
|
|
links = LinksField('Links')
|
|
|
|
def __init__(self,
|
|
connector,
|
|
path='',
|
|
redfish_version=None,
|
|
registries=None,
|
|
reader=None,
|
|
json_doc=None):
|
|
"""A class representing the base of any Redfish resource
|
|
|
|
Invokes the ``refresh()`` method of resource for the first
|
|
time from here (constructor).
|
|
:param connector: A Connector instance
|
|
:param path: sub-URI path to the resource.
|
|
:param redfish_version: The version of Redfish. Used to construct
|
|
the object according to schema of the given version.
|
|
:param registries: Dict of Redfish Message Registry objects to be
|
|
used in any resource that needs registries to parse messages
|
|
:param reader: Reader to use to fetch JSON data.
|
|
:param json_doc: parsed JSON document in form of Python types.
|
|
"""
|
|
self._conn = connector
|
|
self._path = path
|
|
self._json = None
|
|
self.redfish_version = redfish_version
|
|
self._registries = registries
|
|
# Note(deray): Indicates if the resource holds stale data or not.
|
|
# Starting off with True and eventually gets set to False when
|
|
# attribute values are fetched.
|
|
self._is_stale = True
|
|
|
|
self._reader = get_reader(connector, path, reader)
|
|
|
|
self.refresh(json_doc=json_doc)
|
|
|
|
def _get_value(self, val):
|
|
"""Iterate through the input to get values for all attributes
|
|
|
|
:param val: Either a value or a resource
|
|
:returns: Attribute value, which may be a dictionary
|
|
"""
|
|
if isinstance(val, dict):
|
|
subfields = {}
|
|
for key, s_val in val.items():
|
|
subfields[key] = self._get_value(s_val)
|
|
return subfields
|
|
|
|
elif isinstance(val, list):
|
|
return [self._get_value(val[i]) for i in range(len(val))]
|
|
|
|
elif (isinstance(val, DictionaryField)
|
|
or isinstance(val, CompositeField)
|
|
or isinstance(val, ListField)):
|
|
subfields = {}
|
|
for attr, field in val._subfields.items():
|
|
subfields[attr] = self._get_value(val.__getitem__(attr))
|
|
return subfields
|
|
|
|
return val
|
|
|
|
def _parse_attributes(self, json_doc):
|
|
"""Parse the attributes of a resource.
|
|
|
|
Parsed JSON fields are set to `self` as declared in the class.
|
|
|
|
:param json_doc: parsed JSON document in form of Python types
|
|
:returns: dictionary of attribute/values after parsing
|
|
"""
|
|
settings = {}
|
|
for attr, field in _collect_fields(self):
|
|
# Hide the Field object behind the real value
|
|
setattr(self, attr, field._load(json_doc, self))
|
|
|
|
# Get the attribute/value pairs that have been parsed
|
|
settings[attr] = self._get_value(getattr(self, attr))
|
|
|
|
return settings
|
|
|
|
def _get_etag(self):
|
|
"""Returns the ETag of the HTTP request if any was specified.
|
|
|
|
:returns ETag or None
|
|
"""
|
|
pattern = re.compile(r'^(W\/)?("\w*")$')
|
|
match = pattern.match(self._get_headers().get('ETag', ''))
|
|
if match:
|
|
return match.group(2)
|
|
return None
|
|
|
|
def _get_headers(self):
|
|
"""Returns the HTTP headers of the request for the resource.
|
|
|
|
:returns: dict of HTTP headers
|
|
"""
|
|
return self._reader.get_data()._headers
|
|
|
|
def _allow_patch(self):
|
|
"""Returns if the resource supports the PATCH HTTP method.
|
|
|
|
If the resource supports the PATCH HTTP method for updates,
|
|
it will return it in the Allow HTTP header.
|
|
:returns: Boolean flag if PATCH is supported or not
|
|
"""
|
|
allow_header = self._get_headers().get('Allow', '')
|
|
methods = set([h.strip().upper() for h in allow_header.split(',')])
|
|
return "PATCH" in methods
|
|
|
|
def refresh(self, force=True, json_doc=None):
|
|
"""Refresh the resource
|
|
|
|
Freshly retrieves/fetches the resource attributes and invokes
|
|
``_parse_attributes()`` method on successful retrieval.
|
|
It is recommended not to override this method in concrete ResourceBase
|
|
classes. Resource classes can place their refresh specific operations
|
|
in ``_do_refresh()`` method, if needed. This method represents the
|
|
template method in the paradigm of Template design pattern.
|
|
|
|
:param force: if set to False, will only refresh if the resource is
|
|
marked as stale, otherwise neither it nor its subresources will
|
|
be refreshed.
|
|
:param json_doc: parsed JSON document in form of Python types.
|
|
:raises: ResourceNotFoundError
|
|
:raises: ConnectionError
|
|
:raises: HTTPError
|
|
"""
|
|
# Note(deray): Don't re-fetch / invalidate the sub-resources if the
|
|
# resource is "_not_ stale" (i.e. fresh) OR _not_ forced.
|
|
if not self._is_stale and not force:
|
|
return
|
|
|
|
if json_doc:
|
|
self._json = json_doc
|
|
else:
|
|
self._json = self._reader.get_data().json_doc
|
|
|
|
attributes = self._parse_attributes(self._json)
|
|
LOG.debug('Received representation of %(type)s %(path)s: %(json)s',
|
|
{'type': self.__class__.__name__,
|
|
'path': self._path, 'json': attributes})
|
|
self._do_refresh(force)
|
|
|
|
# Mark it fresh
|
|
self._is_stale = False
|
|
|
|
def _do_refresh(self, force):
|
|
"""Primitive method to be overridden by refresh related activities.
|
|
|
|
Derived classes are supposed to override this method with the
|
|
resource specific refresh operations to be performed. This is a
|
|
primitive method in the paradigm of Template design pattern.
|
|
|
|
As for the base implementation of this method the approach taken is:
|
|
On refresh, all sub-resources are marked as stale. That means
|
|
invalidate (or undefine) the exposed attributes for nested resources
|
|
for fresh evaluation in subsequent calls to those exposed attributes.
|
|
In other words greedy-refresh is not done for them, unless forced by
|
|
``force`` argument.
|
|
|
|
:param force: should force refresh the resource and its sub-resources,
|
|
if set to True.
|
|
:raises: ResourceNotFoundError
|
|
:raises: ConnectionError
|
|
:raises: HTTPError
|
|
"""
|
|
utils.cache_clear(self, force_refresh=force)
|
|
|
|
def invalidate(self, force_refresh=False):
|
|
"""Mark the resource as stale, prompting refresh() before getting used.
|
|
|
|
If ``force_refresh`` is set to True, then it invokes ``refresh()``
|
|
on the resource.
|
|
|
|
:param force_refresh: will invoke refresh on the resource,
|
|
if set to True.
|
|
:raises: ResourceNotFoundError
|
|
:raises: ConnectionError
|
|
:raises: HTTPError
|
|
"""
|
|
self._is_stale = True
|
|
if force_refresh:
|
|
self.refresh()
|
|
|
|
@property
|
|
def oem_vendors(self):
|
|
return list(
|
|
set((self._oem_vendors or []) + (self.links.oem_vendors or []))
|
|
)
|
|
|
|
@property
|
|
def json(self):
|
|
return self._json
|
|
|
|
@property
|
|
def path(self):
|
|
return self._path
|
|
|
|
def clone_resource(self, new_resource, path=''):
|
|
"""Instantiate given resource using existing BMC connection context"""
|
|
return new_resource(
|
|
self._conn, path or self.path,
|
|
redfish_version=self.redfish_version,
|
|
reader=self._reader)
|
|
|
|
@property
|
|
def resource_name(self):
|
|
return utils.camelcase_to_underscore_joined(self.__class__.__name__)
|
|
|
|
def get_oem_extension(self, vendor):
|
|
"""Get the OEM extension instance for this resource by OEM vendor
|
|
|
|
:param vendor: the OEM vendor string which is the vendor-specific
|
|
extensibility identifier. Examples are 'Contoso', 'Hpe'.
|
|
Possible value can be got from ``oem_vendors`` attribute.
|
|
:returns: the Redfish resource OEM extension instance.
|
|
:raises: OEMExtensionNotFoundError
|
|
"""
|
|
return oem.get_resource_extension_by_vendor(
|
|
self.resource_name, vendor, self)
|
|
|
|
@property
|
|
def registries(self):
|
|
return self._registries
|
|
|
|
|
|
class ResourceCollectionBase(ResourceBase, metaclass=abc.ABCMeta):
|
|
|
|
name = Field('Name')
|
|
"""The name of the collection"""
|
|
|
|
members_identities = Field('Members', default=[],
|
|
adapter=utils.get_members_identities)
|
|
"""A tuple with the members identities"""
|
|
|
|
def __init__(self, connector, path, redfish_version=None, registries=None):
|
|
"""A class representing the base of any Redfish resource collection
|
|
|
|
It gets inherited from ``ResourceBase`` and invokes the base class
|
|
constructor.
|
|
:param connector: A Connector instance
|
|
:param path: sub-URI path to the resource collection.
|
|
:param redfish_version: The version of Redfish. Used to construct
|
|
the object according to schema of the given version.
|
|
:param registries: Dict of Redfish Message Registry objects to be
|
|
used in any resource that needs registries to parse messages.
|
|
"""
|
|
super(ResourceCollectionBase, self).__init__(
|
|
connector, path, redfish_version, registries)
|
|
LOG.debug('Received %(count)d member(s) for %(type)s %(path)s',
|
|
{'count': len(self.members_identities),
|
|
'type': self.__class__.__name__, 'path': self._path})
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def _resource_type(self):
|
|
"""The resource class that the collection contains.
|
|
|
|
Override this property to specify the resource class that the
|
|
collection contains.
|
|
"""
|
|
|
|
def get_member(self, identity):
|
|
"""Given the identity return a ``_resource_type`` object
|
|
|
|
:param identity: The identity of the ``_resource_type``
|
|
:returns: The ``_resource_type`` object
|
|
:raises: ResourceNotFoundError
|
|
"""
|
|
return self._resource_type(
|
|
self._conn, identity, self.redfish_version, self.registries)
|
|
|
|
@utils.cache_it
|
|
def get_members(self):
|
|
"""Return a list of ``_resource_type`` objects present in collection
|
|
|
|
:returns: A list of ``_resource_type`` objects
|
|
"""
|
|
return [self.get_member(id_) for id_ in self.members_identities]
|