Merge "Add foundation for supporting Redfish OEMs"

This commit is contained in:
Zuul 2019-02-28 11:07:22 +00:00 committed by Gerrit Code Review
commit b5c7db039d
17 changed files with 692 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"""

View File

@ -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):

View File

@ -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',)

107
sushy/resources/oem/base.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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"""

View File

@ -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):

View File

@ -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