Merge "Add foundation for supporting Redfish OEMs"
This commit is contained in:
commit
b5c7db039d
|
@ -36,7 +36,7 @@ six==1.10.0
|
||||||
snowballstemmer==1.2.1
|
snowballstemmer==1.2.1
|
||||||
Sphinx==1.6.2
|
Sphinx==1.6.2
|
||||||
sphinxcontrib-websupport==1.0.1
|
sphinxcontrib-websupport==1.0.1
|
||||||
stevedore==1.20.0
|
stevedore==1.29.0
|
||||||
stestr==2.0.0
|
stestr==2.0.0
|
||||||
testscenarios==0.4
|
testscenarios==0.4
|
||||||
testtools==2.2.0
|
testtools==2.2.0
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds foundation for supporting resource extensibility proposed as
|
||||||
|
OEM extensibility in Redfish specification [1] to the library.
|
||||||
|
|
||||||
|
* Provides an attribute 'oem_vendors' in Resource classes to
|
||||||
|
discover the available OEM extensions.
|
||||||
|
* Provides a method 'get_oem_extension()' in Resource classes
|
||||||
|
to get the vendor defined resource OEM extension object, if
|
||||||
|
discovered.
|
||||||
|
|
||||||
|
[1] http://redfish.dmtf.org/schemas/DSP0266_1.1.html#resource-extensibility
|
|
@ -6,3 +6,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
requests>=2.14.2 # Apache-2.0
|
requests>=2.14.2 # Apache-2.0
|
||||||
six>=1.10.0 # MIT
|
six>=1.10.0 # MIT
|
||||||
python-dateutil>=2.7.0 # BSD
|
python-dateutil>=2.7.0 # BSD
|
||||||
|
stevedore>=1.29.0 # Apache-2.0
|
||||||
|
|
|
@ -22,6 +22,11 @@ classifier =
|
||||||
packages =
|
packages =
|
||||||
sushy
|
sushy
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
sushy.resources.system.oems =
|
||||||
|
contoso = sushy.resources.oem.fake:FakeOEMSystemExtension
|
||||||
|
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
build-dir = doc/build
|
build-dir = doc/build
|
||||||
|
|
|
@ -61,6 +61,14 @@ class ArchiveParsingError(SushyError):
|
||||||
message = 'Failed parsing archive "%(path)s": %(error)s'
|
message = 'Failed parsing archive "%(path)s": %(error)s'
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionError(SushyError):
|
||||||
|
message = ('Sushy Extension Error: %(error)s')
|
||||||
|
|
||||||
|
|
||||||
|
class OEMExtensionNotFoundError(SushyError):
|
||||||
|
message = 'No %(resource)s OEM extension found by name "%(name)s".'
|
||||||
|
|
||||||
|
|
||||||
class HTTPError(SushyError):
|
class HTTPError(SushyError):
|
||||||
"""Basic exception for HTTP errors"""
|
"""Basic exception for HTTP errors"""
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import zipfile
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from sushy import exceptions
|
from sushy import exceptions
|
||||||
|
from sushy.resources import oem
|
||||||
from sushy import utils
|
from sushy import utils
|
||||||
|
|
||||||
|
|
||||||
|
@ -305,6 +306,9 @@ class ResourceBase(object):
|
||||||
redfish_version = None
|
redfish_version = None
|
||||||
"""The Redfish version"""
|
"""The Redfish version"""
|
||||||
|
|
||||||
|
oem_vendors = Field('Oem', adapter=list)
|
||||||
|
"""The list of OEM extension names for this resource."""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
connector,
|
connector,
|
||||||
path='',
|
path='',
|
||||||
|
@ -420,6 +424,22 @@ class ResourceBase(object):
|
||||||
def path(self):
|
def path(self):
|
||||||
return self._path
|
return self._path
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class ResourceCollectionBase(ResourceBase):
|
class ResourceCollectionBase(ResourceBase):
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# 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 sushy.resources.oem.common import get_resource_extension_by_vendor
|
||||||
|
|
||||||
|
__all__ = ('get_resource_extension_by_vendor',)
|
|
@ -0,0 +1,107 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from sushy.resources import base
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OEMField(base.Field):
|
||||||
|
"""Marker class for OEM specific fields."""
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_oem_fields(resource):
|
||||||
|
"""Collect OEM fields from resource.
|
||||||
|
|
||||||
|
:param resource: OEMExtensionResourceBase instance.
|
||||||
|
:returns: generator of tuples (key, field)
|
||||||
|
"""
|
||||||
|
for attr in dir(resource.__class__):
|
||||||
|
field = getattr(resource.__class__, attr)
|
||||||
|
if isinstance(field, OEMField):
|
||||||
|
yield (attr, field)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_base_fields(resource):
|
||||||
|
"""Collect base fields from resource.
|
||||||
|
|
||||||
|
:param resource: OEMExtensionResourceBase instance.
|
||||||
|
:returns: generator of tuples (key, field)
|
||||||
|
"""
|
||||||
|
for attr in dir(resource.__class__):
|
||||||
|
field = getattr(resource.__class__, attr)
|
||||||
|
if not isinstance(field, OEMField) and isinstance(field, base.Field):
|
||||||
|
yield (attr, field)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class OEMCompositeField(base.CompositeField, OEMField):
|
||||||
|
"""CompositeField for OEM fields."""
|
||||||
|
|
||||||
|
|
||||||
|
class OEMListField(base.ListField, OEMField):
|
||||||
|
"""ListField for OEM fields."""
|
||||||
|
|
||||||
|
|
||||||
|
class OEMDictionaryField(base.DictionaryField, OEMField):
|
||||||
|
"""DictionaryField for OEM fields."""
|
||||||
|
|
||||||
|
|
||||||
|
class OEMMappedField(base.MappedField, OEMField):
|
||||||
|
"""MappedField for OEM fields."""
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class OEMExtensionResourceBase(object):
|
||||||
|
|
||||||
|
def __init__(self, resource, oem_property_name, *args, **kwargs):
|
||||||
|
"""A class representing the base of any resource OEM extension
|
||||||
|
|
||||||
|
Invokes the ``refresh()`` method for the first time from here
|
||||||
|
(constructor).
|
||||||
|
:param resource: The parent Sushy resource instance
|
||||||
|
:param oem_property_name: the unique OEM identifier string
|
||||||
|
"""
|
||||||
|
if not resource:
|
||||||
|
raise ValueError('"resource" argument cannot be void')
|
||||||
|
if not isinstance(resource, base.ResourceBase):
|
||||||
|
raise TypeError('"resource" argument must be a ResourceBase')
|
||||||
|
|
||||||
|
self.core_resource = resource
|
||||||
|
self.oem_property_name = oem_property_name
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def _parse_oem_attributes(self):
|
||||||
|
"""Parse the OEM extension attributes of a resource."""
|
||||||
|
oem_json_body = (self.core_resource.json.get('Oem').
|
||||||
|
get(self.oem_property_name))
|
||||||
|
for attr, field in _collect_oem_fields(self):
|
||||||
|
# Hide the Field object behind the real value
|
||||||
|
setattr(self, attr, field._load(oem_json_body, self))
|
||||||
|
|
||||||
|
for attr, field in _collect_base_fields(self):
|
||||||
|
# Hide the Field object behind the real value
|
||||||
|
setattr(self, attr, field._load(self.core_resource.json, self))
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Refresh the attributes of the resource extension.
|
||||||
|
|
||||||
|
Freshly parses the resource OEM attributes via
|
||||||
|
``_parse_oem_attributes()`` method.
|
||||||
|
"""
|
||||||
|
self._parse_oem_attributes()
|
|
@ -0,0 +1,132 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
import stevedore
|
||||||
|
|
||||||
|
from sushy import exceptions
|
||||||
|
from sushy import utils
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_global_extn_mgrs_by_resource = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _raise(m, ep, e):
|
||||||
|
raise exceptions.ExtensionError(
|
||||||
|
error='Failed to load entry point target: %(error)s' % {'error': e})
|
||||||
|
|
||||||
|
|
||||||
|
def _create_extension_manager(namespace):
|
||||||
|
"""Create the resource specific ExtensionManager instance.
|
||||||
|
|
||||||
|
Use stevedore to find all vendor extensions of resource from their
|
||||||
|
namespace and return the ExtensionManager instance.
|
||||||
|
:param namespace: The namespace for the entry points. It maps to a
|
||||||
|
specific Sushy resource type.
|
||||||
|
:returns: the ExtensionManager instance
|
||||||
|
:raises ExtensionError: on resource OEM extension load error.
|
||||||
|
"""
|
||||||
|
# namespace format is:
|
||||||
|
# ``sushy.resources.<underscore_joined_resource_name>.oems``
|
||||||
|
resource_name = namespace.split('.')[-2]
|
||||||
|
|
||||||
|
extension_manager = (
|
||||||
|
stevedore.ExtensionManager(namespace=namespace,
|
||||||
|
propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=_raise))
|
||||||
|
|
||||||
|
LOG.debug('Resource OEM extensions for "%(resource)s" under namespace '
|
||||||
|
'"%(namespace)s":',
|
||||||
|
{'resource': resource_name, 'namespace': namespace})
|
||||||
|
for extension in extension_manager:
|
||||||
|
LOG.debug('Found vendor: %(name)s target: %(target)s',
|
||||||
|
{'name': extension.name,
|
||||||
|
'target': extension.entry_point_target})
|
||||||
|
|
||||||
|
if not extension_manager.names():
|
||||||
|
m = (('No extensions found for "%(resource)s" under namespace '
|
||||||
|
'"%(namespace)s"') %
|
||||||
|
{'resource': resource_name,
|
||||||
|
'namespace': namespace})
|
||||||
|
LOG.error(m)
|
||||||
|
raise exceptions.ExtensionError(error=m)
|
||||||
|
|
||||||
|
return extension_manager
|
||||||
|
|
||||||
|
|
||||||
|
@utils.synchronized
|
||||||
|
def _get_extension_manager_of_resource(resource_name):
|
||||||
|
"""Get the resource specific ExtensionManager instance.
|
||||||
|
|
||||||
|
:param resource_name: The name of the resource e.g.
|
||||||
|
'system' / 'ethernet_interface' / 'update_service'
|
||||||
|
:returns: the ExtensionManager instance
|
||||||
|
:raises ExtensionError: on resource OEM extension load error.
|
||||||
|
"""
|
||||||
|
global _global_extn_mgrs_by_resource
|
||||||
|
|
||||||
|
if resource_name not in _global_extn_mgrs_by_resource:
|
||||||
|
resource_namespace = 'sushy.resources.' + resource_name + '.oems'
|
||||||
|
_global_extn_mgrs_by_resource[resource_name] = (
|
||||||
|
_create_extension_manager(resource_namespace)
|
||||||
|
)
|
||||||
|
return _global_extn_mgrs_by_resource[resource_name]
|
||||||
|
|
||||||
|
|
||||||
|
@utils.synchronized
|
||||||
|
def _get_resource_vendor_extension_obj(extension, resource, *args, **kwds):
|
||||||
|
"""Get the object returned by extension's plugin() method.
|
||||||
|
|
||||||
|
:param extension: stevedore Extension
|
||||||
|
:param resource: The Sushy resource instance
|
||||||
|
:param *args, **kwds: constructor arguments to plugin() method.
|
||||||
|
:returns: The object returned by ``plugin(*args, **kwds)`` of extension.
|
||||||
|
"""
|
||||||
|
if extension.obj is None:
|
||||||
|
extension.obj = extension.plugin(resource, *args, **kwds)
|
||||||
|
|
||||||
|
return extension.obj
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_extension_by_vendor(
|
||||||
|
resource_name, vendor, resource, *args, **kwds):
|
||||||
|
"""Helper method to get Resource specific OEM extension object for vendor
|
||||||
|
|
||||||
|
:param resource_name: The underscore joined name of the resource e.g.
|
||||||
|
'system' / 'ethernet_interface' / 'update_service'
|
||||||
|
:param vendor: This is the OEM vendor string which is the vendor-specific
|
||||||
|
extensibility identifier. Examples are: 'Contoso', 'Hpe'. As a matter
|
||||||
|
of fact the lowercase of this string will be the plugin entry point
|
||||||
|
name.
|
||||||
|
:param resource: The Sushy resource instance
|
||||||
|
:returns: The object returned by ``plugin(*args, **kwds)`` of extension.
|
||||||
|
:raises OEMExtensionNotFoundError: if no valid resource OEM extension
|
||||||
|
found.
|
||||||
|
"""
|
||||||
|
if resource_name in _global_extn_mgrs_by_resource:
|
||||||
|
resource_extn_mgr = _global_extn_mgrs_by_resource[resource_name]
|
||||||
|
else:
|
||||||
|
resource_extn_mgr = _get_extension_manager_of_resource(resource_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resource_vendor_extn = resource_extn_mgr[vendor.lower()]
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.OEMExtensionNotFoundError(
|
||||||
|
resource=resource_name, name=vendor.lower())
|
||||||
|
|
||||||
|
if resource_vendor_extn.obj is None:
|
||||||
|
return _get_resource_vendor_extension_obj(
|
||||||
|
resource_vendor_extn, resource, *args, **kwds)
|
||||||
|
return resource_vendor_extn.obj
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 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 logging
|
||||||
|
|
||||||
|
from sushy.resources import base
|
||||||
|
from sushy.resources.oem import base as oem_base
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionLocationField(oem_base.OEMCompositeField):
|
||||||
|
facility_name = base.Field('FacilityName')
|
||||||
|
country = base.Field('Country')
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOEMSystemExtension(oem_base.OEMExtensionResourceBase):
|
||||||
|
|
||||||
|
data_type = oem_base.OEMField('@odata.type')
|
||||||
|
production_location = ProductionLocationField('ProductionLocation')
|
||||||
|
reset_action = base.Field(['Actions', 'Oem', '#Contoso.Reset'])
|
||||||
|
|
||||||
|
def __init__(self, resource, *args, **kwargs):
|
||||||
|
"""A class representing ComputerSystem OEM extension for Contoso
|
||||||
|
|
||||||
|
:param resource: The parent System resource instance
|
||||||
|
"""
|
||||||
|
super(FakeOEMSystemExtension, self).__init__(
|
||||||
|
resource, 'Contoso', *args, **kwargs)
|
||||||
|
|
||||||
|
def get_reset_system_path(self):
|
||||||
|
return self.reset_action.get('target')
|
|
@ -0,0 +1,186 @@
|
||||||
|
# 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 mock
|
||||||
|
import stevedore
|
||||||
|
|
||||||
|
from sushy import exceptions
|
||||||
|
from sushy.resources import base as res_base
|
||||||
|
from sushy.resources.oem import base as oem_base
|
||||||
|
from sushy.resources.oem import common as oem_common
|
||||||
|
from sushy.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class ContosoResourceOEMExtension(oem_base.OEMExtensionResourceBase):
|
||||||
|
|
||||||
|
def __init__(self, resource, *args, **kwargs):
|
||||||
|
super(ContosoResourceOEMExtension, self).__init__(
|
||||||
|
resource, 'Contoso', *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FauxResourceOEMExtension(oem_base.OEMExtensionResourceBase):
|
||||||
|
|
||||||
|
def __init__(self, resource, *args, **kwargs):
|
||||||
|
super(FauxResourceOEMExtension, self).__init__(
|
||||||
|
resource, 'Faux', *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceOEMCommonMethodsTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(ResourceOEMCommonMethodsTestCase, self).setUp()
|
||||||
|
# We use ExtensionManager.make_test_instance() and instantiate the
|
||||||
|
# test instance outside of the test cases in setUp. Inside of the
|
||||||
|
# test cases we set this as the return value of the mocked
|
||||||
|
# constructor. Also note that this instrumentation has been done
|
||||||
|
# only for one specific resource namespace which gets passed in the
|
||||||
|
# constructor of ExtensionManager. Moreover, this setUp also enables
|
||||||
|
# us to verify that the constructor is called correctly while still
|
||||||
|
# using a more realistic ExtensionManager.
|
||||||
|
contoso_ep = mock.Mock()
|
||||||
|
contoso_ep.module_name = __name__
|
||||||
|
contoso_ep.attrs = ['ContosoResourceOEMExtension']
|
||||||
|
self.contoso_extn = stevedore.extension.Extension(
|
||||||
|
'contoso', contoso_ep, ContosoResourceOEMExtension, None)
|
||||||
|
self.contoso_extn_dup = stevedore.extension.Extension(
|
||||||
|
'contoso_dup', contoso_ep, ContosoResourceOEMExtension, None)
|
||||||
|
|
||||||
|
faux_ep = mock.Mock()
|
||||||
|
faux_ep.module_name = __name__
|
||||||
|
faux_ep.attrs = ['FauxResourceOEMExtension']
|
||||||
|
self.faux_extn = stevedore.extension.Extension(
|
||||||
|
'faux', faux_ep, FauxResourceOEMExtension, None)
|
||||||
|
self.faux_extn_dup = stevedore.extension.Extension(
|
||||||
|
'faux_dup', faux_ep, FauxResourceOEMExtension, None)
|
||||||
|
|
||||||
|
self.fake_ext_mgr = (
|
||||||
|
stevedore.extension.ExtensionManager.make_test_instance(
|
||||||
|
[self.contoso_extn, self.faux_extn]))
|
||||||
|
self.fake_ext_mgr2 = (
|
||||||
|
stevedore.extension.ExtensionManager.make_test_instance(
|
||||||
|
[self.contoso_extn_dup, self.faux_extn_dup]))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super(ResourceOEMCommonMethodsTestCase, self).tearDown()
|
||||||
|
if oem_common._global_extn_mgrs_by_resource:
|
||||||
|
oem_common._global_extn_mgrs_by_resource = {}
|
||||||
|
|
||||||
|
@mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
|
||||||
|
def test__create_extension_manager(self, ExtensionManager_mock):
|
||||||
|
system_resource_oem_ns = 'sushy.resources.system.oems'
|
||||||
|
ExtensionManager_mock.return_value = self.fake_ext_mgr
|
||||||
|
|
||||||
|
result = oem_common._create_extension_manager(system_resource_oem_ns)
|
||||||
|
|
||||||
|
self.assertEqual(self.fake_ext_mgr, result)
|
||||||
|
ExtensionManager_mock.assert_called_once_with(
|
||||||
|
system_resource_oem_ns, propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=oem_common._raise)
|
||||||
|
|
||||||
|
@mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
|
||||||
|
def test__create_extension_manager_no_extns(self, ExtensionManager_mock):
|
||||||
|
system_resource_oem_ns = 'sushy.resources.system.oems'
|
||||||
|
ExtensionManager_mock.return_value.names.return_value = []
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
exceptions.ExtensionError, 'No extensions found',
|
||||||
|
oem_common._create_extension_manager,
|
||||||
|
system_resource_oem_ns)
|
||||||
|
|
||||||
|
@mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
|
||||||
|
def test__get_extension_manager_of_resource(self, ExtensionManager_mock):
|
||||||
|
ExtensionManager_mock.return_value = self.fake_ext_mgr
|
||||||
|
|
||||||
|
result = oem_common._get_extension_manager_of_resource('system')
|
||||||
|
self.assertEqual(self.fake_ext_mgr, result)
|
||||||
|
ExtensionManager_mock.assert_called_once_with(
|
||||||
|
namespace='sushy.resources.system.oems',
|
||||||
|
propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=oem_common._raise)
|
||||||
|
ExtensionManager_mock.reset_mock()
|
||||||
|
|
||||||
|
result = oem_common._get_extension_manager_of_resource('manager')
|
||||||
|
self.assertEqual(self.fake_ext_mgr, result)
|
||||||
|
ExtensionManager_mock.assert_called_once_with(
|
||||||
|
namespace='sushy.resources.manager.oems',
|
||||||
|
propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=oem_common._raise)
|
||||||
|
for name, extension in result.items():
|
||||||
|
self.assertTrue(name in ('contoso', 'faux'))
|
||||||
|
self.assertTrue(extension in (self.contoso_extn,
|
||||||
|
self.faux_extn))
|
||||||
|
|
||||||
|
def test__get_resource_vendor_extension_obj_lazy_plugin_invoke(self):
|
||||||
|
resource_instance_mock = mock.Mock()
|
||||||
|
extension_mock = mock.MagicMock()
|
||||||
|
extension_mock.obj = None
|
||||||
|
|
||||||
|
result = oem_common._get_resource_vendor_extension_obj(
|
||||||
|
extension_mock, resource_instance_mock)
|
||||||
|
self.assertEqual(extension_mock.plugin.return_value, result)
|
||||||
|
extension_mock.plugin.assert_called_once_with(resource_instance_mock)
|
||||||
|
extension_mock.reset_mock()
|
||||||
|
|
||||||
|
# extension_mock.obj is not None anymore
|
||||||
|
result = oem_common._get_resource_vendor_extension_obj(
|
||||||
|
extension_mock, resource_instance_mock)
|
||||||
|
self.assertEqual(extension_mock.plugin.return_value, result)
|
||||||
|
self.assertFalse(extension_mock.plugin.called)
|
||||||
|
|
||||||
|
@mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
|
||||||
|
def test_get_resource_extension_by_vendor(self, ExtensionManager_mock):
|
||||||
|
resource_instance_mock = mock.Mock(spec=res_base.ResourceBase)
|
||||||
|
ExtensionManager_mock.side_effect = [self.fake_ext_mgr,
|
||||||
|
self.fake_ext_mgr2]
|
||||||
|
|
||||||
|
result = oem_common.get_resource_extension_by_vendor(
|
||||||
|
'system', 'Faux', resource_instance_mock)
|
||||||
|
self.assertIsInstance(result, FauxResourceOEMExtension)
|
||||||
|
ExtensionManager_mock.assert_called_once_with(
|
||||||
|
'sushy.resources.system.oems', propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=oem_common._raise)
|
||||||
|
ExtensionManager_mock.reset_mock()
|
||||||
|
|
||||||
|
result = oem_common.get_resource_extension_by_vendor(
|
||||||
|
'system', 'Contoso', resource_instance_mock)
|
||||||
|
self.assertIsInstance(result, ContosoResourceOEMExtension)
|
||||||
|
self.assertFalse(ExtensionManager_mock.called)
|
||||||
|
ExtensionManager_mock.reset_mock()
|
||||||
|
|
||||||
|
result = oem_common.get_resource_extension_by_vendor(
|
||||||
|
'manager', 'Faux_dup', resource_instance_mock)
|
||||||
|
self.assertIsInstance(result, FauxResourceOEMExtension)
|
||||||
|
ExtensionManager_mock.assert_called_once_with(
|
||||||
|
'sushy.resources.manager.oems', propagate_map_exceptions=True,
|
||||||
|
on_load_failure_callback=oem_common._raise)
|
||||||
|
ExtensionManager_mock.reset_mock()
|
||||||
|
|
||||||
|
result = oem_common.get_resource_extension_by_vendor(
|
||||||
|
'manager', 'Contoso_dup', resource_instance_mock)
|
||||||
|
self.assertIsInstance(result, ContosoResourceOEMExtension)
|
||||||
|
self.assertFalse(ExtensionManager_mock.called)
|
||||||
|
ExtensionManager_mock.reset_mock()
|
||||||
|
|
||||||
|
@mock.patch.object(stevedore, 'ExtensionManager', autospec=True)
|
||||||
|
def test_get_resource_extension_by_vendor_fail(
|
||||||
|
self, ExtensionManager_mock):
|
||||||
|
resource_instance_mock = mock.Mock(spec=res_base.ResourceBase)
|
||||||
|
# ``fake_ext_mgr2`` has extension names as ``faux_dup``
|
||||||
|
# and ``contoso_dup``.
|
||||||
|
ExtensionManager_mock.return_value = self.fake_ext_mgr2
|
||||||
|
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
exceptions.OEMExtensionNotFoundError,
|
||||||
|
'No sushy.resources.system.oems OEM extension found '
|
||||||
|
'by name "faux"',
|
||||||
|
oem_common.get_resource_extension_by_vendor,
|
||||||
|
'sushy.resources.system.oems', 'Faux', resource_instance_mock)
|
|
@ -0,0 +1,51 @@
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from sushy.resources.oem import fake
|
||||||
|
from sushy.resources.system import system
|
||||||
|
from sushy.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOEMSystemExtensionTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(FakeOEMSystemExtensionTestCase, self).setUp()
|
||||||
|
self.conn = mock.MagicMock()
|
||||||
|
with open('sushy/tests/unit/json_samples/system.json', 'r') as f:
|
||||||
|
self.conn.get.return_value.json.return_value = json.loads(f.read())
|
||||||
|
|
||||||
|
self.sys_instance = system.System(
|
||||||
|
self.conn, '/redfish/v1/Systems/437XR1138R2',
|
||||||
|
redfish_version='1.0.2')
|
||||||
|
self.fake_sys_oem_extn = fake.FakeOEMSystemExtension(self.sys_instance)
|
||||||
|
|
||||||
|
def test__parse_oem_attributes(self):
|
||||||
|
self.assertEqual('http://Contoso.com/Schema#Contoso.ComputerSystem',
|
||||||
|
self.fake_sys_oem_extn.data_type)
|
||||||
|
self.assertEqual('PacWest Production Facility', (
|
||||||
|
self.fake_sys_oem_extn.production_location.facility_name))
|
||||||
|
self.assertEqual('USA', (
|
||||||
|
self.fake_sys_oem_extn.production_location.country))
|
||||||
|
self.assertEqual({
|
||||||
|
"target": ("/redfish/v1/Systems/437XR1138R2/Oem/Contoso/Actions/"
|
||||||
|
"Contoso.Reset")}, self.fake_sys_oem_extn.reset_action)
|
||||||
|
|
||||||
|
def test_get_reset_system_path(self):
|
||||||
|
value = self.fake_sys_oem_extn.get_reset_system_path()
|
||||||
|
expected = (
|
||||||
|
'/redfish/v1/Systems/437XR1138R2/Oem/Contoso/Actions/Contoso.Reset'
|
||||||
|
)
|
||||||
|
self.assertEqual(expected, value)
|
|
@ -22,6 +22,7 @@ from sushy import exceptions
|
||||||
from sushy.resources.chassis import chassis
|
from sushy.resources.chassis import chassis
|
||||||
from sushy.resources import constants as res_cons
|
from sushy.resources import constants as res_cons
|
||||||
from sushy.resources.manager import manager
|
from sushy.resources.manager import manager
|
||||||
|
from sushy.resources.oem import fake
|
||||||
from sushy.resources.system import bios
|
from sushy.resources.system import bios
|
||||||
from sushy.resources.system import mappings as sys_map
|
from sushy.resources.system import mappings as sys_map
|
||||||
from sushy.resources.system import processor
|
from sushy.resources.system import processor
|
||||||
|
@ -69,6 +70,8 @@ class SystemTestCase(base.TestCase):
|
||||||
self.sys_inst.power_state)
|
self.sys_inst.power_state)
|
||||||
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
||||||
self.assertEqual("OK", self.sys_inst.memory_summary.health)
|
self.assertEqual("OK", self.sys_inst.memory_summary.health)
|
||||||
|
for oem_vendor in self.sys_inst.oem_vendors:
|
||||||
|
self.assertIn(oem_vendor, ('Contoso', 'Chipwise'))
|
||||||
|
|
||||||
def test__parse_attributes_missing_actions(self):
|
def test__parse_attributes_missing_actions(self):
|
||||||
self.sys_inst.json.pop('Actions')
|
self.sys_inst.json.pop('Actions')
|
||||||
|
@ -504,6 +507,15 @@ class SystemTestCase(base.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'/redfish/v1/Chassis/1U', actual_chassis[0].path)
|
'/redfish/v1/Chassis/1U', actual_chassis[0].path)
|
||||||
|
|
||||||
|
def test_get_oem_extension(self):
|
||||||
|
# | WHEN |
|
||||||
|
contoso_system_extn_inst = self.sys_inst.get_oem_extension('Contoso')
|
||||||
|
# | THEN |
|
||||||
|
self.assertIsInstance(contoso_system_extn_inst,
|
||||||
|
fake.FakeOEMSystemExtension)
|
||||||
|
self.assertIs(self.sys_inst, contoso_system_extn_inst.core_resource)
|
||||||
|
self.assertEqual('Contoso', contoso_system_extn_inst.oem_property_name)
|
||||||
|
|
||||||
|
|
||||||
class SystemCollectionTestCase(base.TestCase):
|
class SystemCollectionTestCase(base.TestCase):
|
||||||
|
|
||||||
|
|
|
@ -25,20 +25,47 @@ from sushy.tests.unit import base
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
BASE_RESOURCE_JSON = {
|
||||||
|
"@odata.type": "#FauxResource.v1_0_0.FauxResource",
|
||||||
|
"Id": "1111AAAA",
|
||||||
|
"Name": "Faux Resource",
|
||||||
|
"@odata.id": "/redfish/v1/FauxResource/1111AAAA",
|
||||||
|
"Oem": {
|
||||||
|
"Contoso": {
|
||||||
|
"@odata.type": "http://contoso.com/schemas/extensions.v1_2_1#contoso.AnvilTypes1", # noqa
|
||||||
|
"slogan": "Contoso never fail",
|
||||||
|
"disclaimer": "* Most of the time"
|
||||||
|
},
|
||||||
|
"EID_412_ASB_123": {
|
||||||
|
"@odata.type": "http://AnotherStandardsBody/schemas.v1_0_1#styleInfoExt", # noqa
|
||||||
|
"Style": "Executive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseResource(resource_base.ResourceBase):
|
class BaseResource(resource_base.ResourceBase):
|
||||||
|
|
||||||
def _parse_attributes(self):
|
def _parse_attributes(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResource2(resource_base.ResourceBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ResourceBaseTestCase(base.TestCase):
|
class ResourceBaseTestCase(base.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ResourceBaseTestCase, self).setUp()
|
super(ResourceBaseTestCase, self).setUp()
|
||||||
self.conn = mock.Mock()
|
self.conn = mock.Mock()
|
||||||
|
self.conn.get.return_value.json.return_value = (
|
||||||
|
copy.deepcopy(BASE_RESOURCE_JSON))
|
||||||
self.base_resource = BaseResource(connector=self.conn, path='/Foo',
|
self.base_resource = BaseResource(connector=self.conn, path='/Foo',
|
||||||
redfish_version='1.0.2')
|
redfish_version='1.0.2')
|
||||||
self.assertFalse(self.base_resource._is_stale)
|
self.assertFalse(self.base_resource._is_stale)
|
||||||
|
self.base_resource2 = BaseResource2(connector=self.conn, path='/Foo',
|
||||||
|
redfish_version='1.0.2')
|
||||||
# refresh() is called in the constructor
|
# refresh() is called in the constructor
|
||||||
self.conn.reset_mock()
|
self.conn.reset_mock()
|
||||||
|
|
||||||
|
@ -102,6 +129,11 @@ class ResourceBaseTestCase(base.TestCase):
|
||||||
reader=resource_base.
|
reader=resource_base.
|
||||||
JsonArchiveReader('Test.2.0.json'))
|
JsonArchiveReader('Test.2.0.json'))
|
||||||
|
|
||||||
|
def test__parse_attributes(self):
|
||||||
|
for oem_vendor in self.base_resource2.oem_vendors:
|
||||||
|
self.assertTrue(oem_vendor in ('Contoso', 'EID_412_ASB_123'))
|
||||||
|
self.assertEqual('base_resource2', self.base_resource2.resource_name)
|
||||||
|
|
||||||
|
|
||||||
class TestResource(resource_base.ResourceBase):
|
class TestResource(resource_base.ResourceBase):
|
||||||
"""A concrete Test Resource to test against"""
|
"""A concrete Test Resource to test against"""
|
||||||
|
|
|
@ -106,6 +106,26 @@ class UtilsTestCase(base.TestCase):
|
||||||
self.assertEqual(0, utils.max_safe([]))
|
self.assertEqual(0, utils.max_safe([]))
|
||||||
self.assertIsNone(utils.max_safe([], default=None))
|
self.assertIsNone(utils.max_safe([], default=None))
|
||||||
|
|
||||||
|
def test_camelcase_to_underscore_joined(self):
|
||||||
|
input_vs_expected = [
|
||||||
|
('GarbageCollection', 'garbage_collection'),
|
||||||
|
('DD', 'dd'),
|
||||||
|
('rr', 'rr'),
|
||||||
|
('AABbbC', 'aa_bbb_c'),
|
||||||
|
('AABbbCCCDd', 'aa_bbb_ccc_dd'),
|
||||||
|
('Manager', 'manager'),
|
||||||
|
('EthernetInterfaceCollection', 'ethernet_interface_collection'),
|
||||||
|
(' ', ' '),
|
||||||
|
]
|
||||||
|
for inp, exp in input_vs_expected:
|
||||||
|
self.assertEqual(exp, utils.camelcase_to_underscore_joined(inp))
|
||||||
|
|
||||||
|
def test_camelcase_to_underscore_joined_fails_with_empty_string(self):
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
ValueError,
|
||||||
|
'"camelcase_str" cannot be empty',
|
||||||
|
utils.camelcase_to_underscore_joined, '')
|
||||||
|
|
||||||
|
|
||||||
class NestedResource(resource_base.ResourceBase):
|
class NestedResource(resource_base.ResourceBase):
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -281,3 +282,50 @@ def cache_clear(res_selfie, force_refresh, only_these=None):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
setattr(res_selfie, cache_attr_name, None)
|
setattr(res_selfie, cache_attr_name, None)
|
||||||
|
|
||||||
|
|
||||||
|
def camelcase_to_underscore_joined(camelcase_str):
|
||||||
|
"""Convert camelCase string to underscore_joined string
|
||||||
|
|
||||||
|
:param camelcase_str: The camelCase string
|
||||||
|
:returns: the equivalent underscore_joined string
|
||||||
|
"""
|
||||||
|
if not camelcase_str:
|
||||||
|
raise ValueError('"camelcase_str" cannot be empty')
|
||||||
|
|
||||||
|
r = camelcase_str[0].lower()
|
||||||
|
for i, letter in enumerate(camelcase_str[1:], 1):
|
||||||
|
if letter.isupper():
|
||||||
|
try:
|
||||||
|
if (camelcase_str[i - 1].islower()
|
||||||
|
or camelcase_str[i + 1].islower()):
|
||||||
|
r += '_'
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
r += letter.lower()
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def synchronized(wrapped):
|
||||||
|
"""Simple synchronization decorator.
|
||||||
|
|
||||||
|
Decorating a method like so:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def foo(self, *args):
|
||||||
|
...
|
||||||
|
|
||||||
|
ensures that only one thread will execute the foo method at a time.
|
||||||
|
"""
|
||||||
|
lock = threading.RLock()
|
||||||
|
|
||||||
|
@six.wraps(wrapped)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
with lock:
|
||||||
|
return wrapped(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
Loading…
Reference in New Issue