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
|
||||
Sphinx==1.6.2
|
||||
sphinxcontrib-websupport==1.0.1
|
||||
stevedore==1.20.0
|
||||
stevedore==1.29.0
|
||||
stestr==2.0.0
|
||||
testscenarios==0.4
|
||||
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
|
||||
six>=1.10.0 # MIT
|
||||
python-dateutil>=2.7.0 # BSD
|
||||
stevedore>=1.29.0 # Apache-2.0
|
||||
|
|
|
@ -22,6 +22,11 @@ classifier =
|
|||
packages =
|
||||
sushy
|
||||
|
||||
[entry_points]
|
||||
sushy.resources.system.oems =
|
||||
contoso = sushy.resources.oem.fake:FakeOEMSystemExtension
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
|
|
|
@ -61,6 +61,14 @@ class ArchiveParsingError(SushyError):
|
|||
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):
|
||||
"""Basic exception for HTTP errors"""
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import zipfile
|
|||
import six
|
||||
|
||||
from sushy import exceptions
|
||||
from sushy.resources import oem
|
||||
from sushy import utils
|
||||
|
||||
|
||||
|
@ -305,6 +306,9 @@ class ResourceBase(object):
|
|||
redfish_version = None
|
||||
"""The Redfish version"""
|
||||
|
||||
oem_vendors = Field('Oem', adapter=list)
|
||||
"""The list of OEM extension names for this resource."""
|
||||
|
||||
def __init__(self,
|
||||
connector,
|
||||
path='',
|
||||
|
@ -420,6 +424,22 @@ class ResourceBase(object):
|
|||
def path(self):
|
||||
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)
|
||||
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 import constants as res_cons
|
||||
from sushy.resources.manager import manager
|
||||
from sushy.resources.oem import fake
|
||||
from sushy.resources.system import bios
|
||||
from sushy.resources.system import mappings as sys_map
|
||||
from sushy.resources.system import processor
|
||||
|
@ -69,6 +70,8 @@ class SystemTestCase(base.TestCase):
|
|||
self.sys_inst.power_state)
|
||||
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
||||
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):
|
||||
self.sys_inst.json.pop('Actions')
|
||||
|
@ -504,6 +507,15 @@ class SystemTestCase(base.TestCase):
|
|||
self.assertEqual(
|
||||
'/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):
|
||||
|
||||
|
|
|
@ -25,20 +25,47 @@ from sushy.tests.unit import base
|
|||
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):
|
||||
|
||||
def _parse_attributes(self):
|
||||
pass
|
||||
|
||||
|
||||
class BaseResource2(resource_base.ResourceBase):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceBaseTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceBaseTestCase, self).setUp()
|
||||
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',
|
||||
redfish_version='1.0.2')
|
||||
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
|
||||
self.conn.reset_mock()
|
||||
|
||||
|
@ -102,6 +129,11 @@ class ResourceBaseTestCase(base.TestCase):
|
|||
reader=resource_base.
|
||||
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):
|
||||
"""A concrete Test Resource to test against"""
|
||||
|
|
|
@ -106,6 +106,26 @@ class UtilsTestCase(base.TestCase):
|
|||
self.assertEqual(0, utils.max_safe([]))
|
||||
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):
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
import collections
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import six
|
||||
|
||||
|
@ -281,3 +282,50 @@ def cache_clear(res_selfie, force_refresh, only_these=None):
|
|||
break
|
||||
else:
|
||||
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