Declarative approach to parsing JSON fields
Replaces explicit fields parsing and validation with sqlalchemy-alike class-level declarations. This allows uniform handling of fields in different resources, especially wrt validation. Change-Id: Ia1f2fbb6d2358831bafc7386e4a1cecd235de892
This commit is contained in:
parent
a340a17055
commit
0c3ebfb319
@ -40,6 +40,11 @@ class MissingAttributeError(SushyError):
|
||||
'resource %(resource)s')
|
||||
|
||||
|
||||
class MalformedAttributeError(SushyError):
|
||||
message = ('The attribute %(attribute)s is malformed in the '
|
||||
'resource %(resource)s: %(error)s')
|
||||
|
||||
|
||||
class MissingActionError(SushyError):
|
||||
message = ('The action %(action)s is missing from the '
|
||||
'resource %(resource)s')
|
||||
|
@ -14,16 +14,169 @@
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
from sushy import exceptions
|
||||
from sushy import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Field(object):
|
||||
"""Definition for fields fetched from JSON."""
|
||||
|
||||
def __init__(self, path, required=False, default=None,
|
||||
adapter=lambda x: x):
|
||||
"""Create a field definition.
|
||||
|
||||
:param path: JSON field to fetch the value from. Either a string,
|
||||
or a list of strings in case of a nested field.
|
||||
:param required: whether this field is required. Missing required
|
||||
fields result in MissingAttributeError.
|
||||
:param default: the default value to use when the field is missing.
|
||||
Only has effect when the field is not required.
|
||||
:param adapter: a function to call to transform and/or validate
|
||||
the received value. UnicodeError, ValueError or TypeError from
|
||||
this call are reraised as MalformedAttributeError.
|
||||
"""
|
||||
if not callable(adapter):
|
||||
raise TypeError("Adapter must be callable")
|
||||
|
||||
if isinstance(path, six.string_types):
|
||||
path = [path]
|
||||
elif not path:
|
||||
raise ValueError('Path cannot be empty')
|
||||
|
||||
self._path = path
|
||||
self._required = required
|
||||
self._default = default
|
||||
self._adapter = adapter
|
||||
|
||||
def _load(self, body, resource, nested_in=None):
|
||||
"""Load this field from a JSON object.
|
||||
|
||||
:param body: parsed JSON body.
|
||||
:param resource: ResourceBase instance for which the field is loaded.
|
||||
:param nested_in: parent resource path (for error reporting only),
|
||||
must be a list of strings or None.
|
||||
:raises: MissingAttributeError if a required field is missing.
|
||||
:raises: MalformedAttributeError on invalid field value or type.
|
||||
:returns: loaded and verified value
|
||||
"""
|
||||
name = self._path[-1]
|
||||
for path_item in self._path[:-1]:
|
||||
body = body.get(path_item, {})
|
||||
|
||||
if name not in body:
|
||||
if self._required:
|
||||
path = (nested_in or []) + self._path
|
||||
raise exceptions.MissingAttributeError(
|
||||
attribute='/'.join(path),
|
||||
resource=resource.path)
|
||||
else:
|
||||
# Do not run the adapter on the default value
|
||||
return self._default
|
||||
|
||||
try:
|
||||
value = self._adapter(body[name])
|
||||
except (UnicodeError, ValueError, TypeError) as exc:
|
||||
path = (nested_in or []) + self._path
|
||||
raise exceptions.MalformedAttributeError(
|
||||
attribute='/'.join(path),
|
||||
resource=resource.path,
|
||||
error=exc)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _collect_fields(resource):
|
||||
"""Collect fields from the JSON.
|
||||
|
||||
:param resource: ResourceBase or CompositeField instance.
|
||||
:returns: generator of tuples (key, field)
|
||||
"""
|
||||
for attr in dir(resource.__class__):
|
||||
field = getattr(resource.__class__, attr)
|
||||
if isinstance(field, Field):
|
||||
yield (attr, field)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CompositeField(collections.Mapping, Field):
|
||||
"""Base class for fields consisting of several sub-fields."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CompositeField, self).__init__(*args, **kwargs)
|
||||
self._subfields = dict(_collect_fields(self))
|
||||
|
||||
def _load(self, body, resource, nested_in=None):
|
||||
"""Load the composite field.
|
||||
|
||||
:param body: parent JSON body.
|
||||
:param resource: parent resource.
|
||||
:param nested_in: parent resource name (for error reporting only).
|
||||
:returns: a new object with sub-fields attached to it.
|
||||
"""
|
||||
nested_in = (nested_in or []) + self._path
|
||||
value = super(CompositeField, self)._load(body, resource)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# We need a new instance, as this method is called a singleton instance
|
||||
# that is attached to a class (not instance) of a resource or another
|
||||
# CompositeField. We don't want to end up modifying this instance.
|
||||
instance = copy.copy(self)
|
||||
for attr, field in self._subfields.items():
|
||||
# Hide the Field object behind the real value
|
||||
setattr(instance, attr, field._load(value, resource, nested_in))
|
||||
|
||||
return instance
|
||||
|
||||
# Satisfy the mapping interface, see
|
||||
# https://docs.python.org/2/library/collections.html#collections.Mapping.
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self._subfields:
|
||||
return getattr(self, key)
|
||||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._subfields)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._subfields)
|
||||
|
||||
|
||||
class MappedField(Field):
|
||||
"""Field taking real value from a mapping."""
|
||||
|
||||
def __init__(self, field, mapping, required=False, default=None):
|
||||
"""Create a mapped field definition.
|
||||
|
||||
:param field: JSON field to fetch the value from. This can be either
|
||||
a string or a list of string. In the latter case, the value will
|
||||
be fetched from a nested object.
|
||||
:param mapping: a mapping to take values from.
|
||||
:param required: whether this field is required. Missing required
|
||||
fields result in MissingAttributeError.
|
||||
:param default: the default value to use when the field is missing.
|
||||
Only has effect when the field is not required. This value is not
|
||||
matched against the mapping.
|
||||
"""
|
||||
if not isinstance(mapping, collections.Mapping):
|
||||
raise TypeError("The mapping argument must be a mapping")
|
||||
|
||||
super(MappedField, self).__init__(
|
||||
field, required=required, default=default,
|
||||
adapter=mapping.get)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ResourceBase(object):
|
||||
|
||||
@ -46,6 +199,12 @@ class ResourceBase(object):
|
||||
self.redfish_version = redfish_version
|
||||
self.refresh()
|
||||
|
||||
def _parse_attributes(self):
|
||||
"""Parse the attributes of a resource."""
|
||||
for attr, field in _collect_fields(self):
|
||||
# Hide the Field object behind the real value
|
||||
setattr(self, attr, field._load(self.json, self))
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the resource
|
||||
|
||||
@ -69,22 +228,15 @@ class ResourceBase(object):
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@abc.abstractmethod
|
||||
def _parse_attributes(self):
|
||||
"""Parse the attributes of a resource
|
||||
|
||||
This method should be overwritten and is responsible for parsing
|
||||
all the attributes of a resource.
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ResourceCollectionBase(ResourceBase):
|
||||
|
||||
name = None
|
||||
name = Field('Name')
|
||||
"""The name of the collection"""
|
||||
|
||||
members_identities = None
|
||||
members_identities = Field('Members', default=[],
|
||||
adapter=utils.get_members_identities)
|
||||
"""A tuple with the members identities"""
|
||||
|
||||
def __init__(self, connector, path, redfish_version=None):
|
||||
@ -99,6 +251,9 @@ class ResourceCollectionBase(ResourceBase):
|
||||
"""
|
||||
super(ResourceCollectionBase, self).__init__(connector, path,
|
||||
redfish_version)
|
||||
LOG.debug('Received %(count)d member(s) for %(type)s %(path)s',
|
||||
{'count': len(self.members_identities),
|
||||
'type': self.__class__.__name__, 'path': self._path})
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
@ -109,14 +264,6 @@ class ResourceCollectionBase(ResourceBase):
|
||||
collection contains.
|
||||
"""
|
||||
|
||||
def _parse_attributes(self):
|
||||
self.name = self.json.get('Name')
|
||||
self.members_identities = (
|
||||
utils.get_members_identities(self.json.get('Members', [])))
|
||||
LOG.debug('Received %(count)d member(s) for %(type)s %(path)s',
|
||||
{'count': len(self.members_identities),
|
||||
'type': self.__class__.__name__, 'path': self._path})
|
||||
|
||||
def get_member(self, identity):
|
||||
"""Given the identity return a ``_resource_type`` object
|
||||
|
||||
|
@ -26,36 +26,37 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
class Processor(base.ResourceBase):
|
||||
|
||||
identity = None
|
||||
identity = base.Field('Id', required=True)
|
||||
"""The processor identity string"""
|
||||
|
||||
socket = None
|
||||
socket = base.Field('Socket')
|
||||
"""The socket or location of the processor"""
|
||||
|
||||
# TODO(deray): Create mappings for the processor_type
|
||||
processor_type = None
|
||||
processor_type = base.Field('ProcessorType')
|
||||
"""The type of processor"""
|
||||
|
||||
processor_architecture = None
|
||||
processor_architecture = base.MappedField(
|
||||
'ProcessorArchitecture', sys_maps.PROCESSOR_ARCH_VALUE_MAP)
|
||||
"""The architecture of the processor"""
|
||||
|
||||
# TODO(deray): Create mappings for the instruction_set
|
||||
instruction_set = None
|
||||
instruction_set = base.Field('InstructionSet')
|
||||
"""The instruction set of the processor"""
|
||||
|
||||
manufacturer = None
|
||||
manufacturer = base.Field('Manufacturer')
|
||||
"""The processor manufacturer"""
|
||||
|
||||
model = None
|
||||
model = base.Field('Model')
|
||||
"""The product model number of this device"""
|
||||
|
||||
max_speed_mhz = None
|
||||
max_speed_mhz = base.Field('MaxSpeedMHz', adapter=int)
|
||||
"""The maximum clock speed of the processor in MHz."""
|
||||
|
||||
total_cores = None
|
||||
total_cores = base.Field('TotalCores', adapter=int)
|
||||
"""The total number of cores contained in this processor"""
|
||||
|
||||
total_threads = None
|
||||
total_threads = base.Field('TotalThreads', adapter=int)
|
||||
"""The total number of execution threads supported by this processor"""
|
||||
|
||||
def __init__(self, connector, identity, redfish_version=None):
|
||||
@ -68,20 +69,6 @@ class Processor(base.ResourceBase):
|
||||
"""
|
||||
super(Processor, self).__init__(connector, identity, redfish_version)
|
||||
|
||||
def _parse_attributes(self):
|
||||
self.identity = self.json.get('Id')
|
||||
self.socket = self.json.get('Socket')
|
||||
self.processor_type = self.json.get('ProcessorType')
|
||||
self.processor_architecture = (
|
||||
sys_maps.PROCESSOR_ARCH_VALUE_MAP.get(
|
||||
self.json.get('ProcessorArchitecture')))
|
||||
self.instruction_set = self.json.get('InstructionSet')
|
||||
self.manufacturer = self.json.get('Manufacturer')
|
||||
self.model = self.json.get('Model')
|
||||
self.max_speed_mhz = self.json.get('MaxSpeedMHz')
|
||||
self.total_cores = self.json.get('TotalCores')
|
||||
self.total_threads = self.json.get('TotalThreads')
|
||||
|
||||
|
||||
class ProcessorCollection(base.ResourceCollectionBase):
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import logging
|
||||
|
||||
from sushy import exceptions
|
||||
@ -22,74 +21,108 @@ from sushy.resources.system import constants as sys_cons
|
||||
from sushy.resources.system import mappings as sys_maps
|
||||
from sushy.resources.system import processor
|
||||
|
||||
# Representation of Memory information summary
|
||||
MemorySummary = collections.namedtuple('MemorySummary',
|
||||
['size_gib', 'health'])
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResetActionField(base.CompositeField):
|
||||
allowed_values = base.Field('ResetType@Redfish.AllowableValues',
|
||||
adapter=list)
|
||||
|
||||
target_uri = base.Field('target', required=True)
|
||||
|
||||
|
||||
class ActionsField(base.CompositeField):
|
||||
reset = ResetActionField('#ComputerSystem.Reset')
|
||||
|
||||
|
||||
class BootField(base.CompositeField):
|
||||
allowed_values = base.Field(
|
||||
'BootSourceOverrideTarget@Redfish.AllowableValues',
|
||||
adapter=list)
|
||||
|
||||
enabled = base.MappedField('BootSourceOverrideEnabled',
|
||||
sys_maps.BOOT_SOURCE_ENABLED_MAP)
|
||||
|
||||
mode = base.MappedField('BootSourceOverrideMode',
|
||||
sys_maps.BOOT_SOURCE_MODE_MAP)
|
||||
|
||||
target = base.MappedField('BootSourceOverrideTarget',
|
||||
sys_maps.BOOT_SOURCE_TARGET_MAP)
|
||||
|
||||
|
||||
class MemorySummaryField(base.CompositeField):
|
||||
health = base.Field(['Status', 'HealthRollup'])
|
||||
"""The overall health state of memory.
|
||||
|
||||
This signifies health state of memory along with its dependent resources.
|
||||
"""
|
||||
|
||||
size_gib = base.Field('TotalSystemMemoryGiB', adapter=int)
|
||||
"""The size of memory of the system in GiB.
|
||||
|
||||
This signifies the total installed, operating system-accessible memory
|
||||
(RAM), measured in GiB.
|
||||
"""
|
||||
|
||||
|
||||
class System(base.ResourceBase):
|
||||
|
||||
asset_tag = None
|
||||
asset_tag = base.Field('AssetTag')
|
||||
"""The system asset tag"""
|
||||
|
||||
bios_version = None
|
||||
bios_version = base.Field('BiosVersion')
|
||||
"""The system BIOS version"""
|
||||
|
||||
boot = None
|
||||
boot = BootField('Boot', required=True)
|
||||
"""A dictionary containg the current boot device, frequency and mode"""
|
||||
|
||||
description = None
|
||||
description = base.Field('Description')
|
||||
"""The system description"""
|
||||
|
||||
hostname = None
|
||||
hostname = base.Field('HostName')
|
||||
"""The system hostname"""
|
||||
|
||||
identity = None
|
||||
identity = base.Field('Id', required=True)
|
||||
"""The system identity string"""
|
||||
|
||||
# TODO(lucasagomes): Create mappings for the indicator_led
|
||||
indicator_led = None
|
||||
indicator_led = base.Field('IndicatorLED')
|
||||
"""Whether the indicator LED is lit or off"""
|
||||
|
||||
manufacturer = None
|
||||
manufacturer = base.Field('Manufacturer')
|
||||
"""The system manufacturer"""
|
||||
|
||||
name = None
|
||||
name = base.Field('Name')
|
||||
"""The system name"""
|
||||
|
||||
part_number = None
|
||||
part_number = base.Field('PartNumber')
|
||||
"""The system part number"""
|
||||
|
||||
power_state = None
|
||||
power_state = base.MappedField('PowerState',
|
||||
sys_maps.SYSTEM_POWER_STATE_MAP)
|
||||
"""The system power state"""
|
||||
|
||||
serial_number = None
|
||||
serial_number = base.Field('SerialNumber')
|
||||
"""The system serial number"""
|
||||
|
||||
sku = None
|
||||
sku = base.Field('SKU')
|
||||
"""The system stock-keeping unit"""
|
||||
|
||||
# TODO(lucasagomes): Create mappings for the system_type
|
||||
system_type = None
|
||||
system_type = base.Field('SystemType')
|
||||
"""The system type"""
|
||||
|
||||
uuid = None
|
||||
uuid = base.Field('UUID')
|
||||
"""The system UUID"""
|
||||
|
||||
memory_summary = None
|
||||
"""The summary info of memory of the system in general detail
|
||||
|
||||
It is a namedtuple containing the following:
|
||||
size_gib: The size of memory of the system in GiB. This signifies
|
||||
the total installed, operating system-accessible memory (RAM),
|
||||
measured in GiB.
|
||||
health: The overall health state of memory. This signifies
|
||||
health state of memory along with its dependent resources.
|
||||
"""
|
||||
memory_summary = MemorySummaryField('MemorySummary')
|
||||
"""The summary info of memory of the system in general detail"""
|
||||
|
||||
_processors = None # ref to ProcessorCollection instance
|
||||
|
||||
_actions = ActionsField('Actions', required=True)
|
||||
|
||||
def __init__(self, connector, identity, redfish_version=None):
|
||||
"""A class representing a ComputerSystem
|
||||
|
||||
@ -100,56 +133,9 @@ class System(base.ResourceBase):
|
||||
"""
|
||||
super(System, self).__init__(connector, identity, redfish_version)
|
||||
|
||||
def _parse_attributes(self):
|
||||
self.asset_tag = self.json.get('AssetTag')
|
||||
self.bios_version = self.json.get('BiosVersion')
|
||||
self.description = self.json.get('Description')
|
||||
self.hostname = self.json.get('HostName')
|
||||
self.identity = self.json.get('Id')
|
||||
self.indicator_led = self.json.get('IndicatorLED')
|
||||
self.manufacturer = self.json.get('Manufacturer')
|
||||
self.name = self.json.get('Name')
|
||||
self.part_number = self.json.get('PartNumber')
|
||||
self.serial_number = self.json.get('SerialNumber')
|
||||
self.sku = self.json.get('SKU')
|
||||
self.system_type = self.json.get('SystemType')
|
||||
self.uuid = self.json.get('UUID')
|
||||
self.power_state = sys_maps.SYSTEM_POWER_STATE_MAP.get(
|
||||
self.json.get('PowerState'))
|
||||
|
||||
# Parse the boot attribute
|
||||
self.boot = {}
|
||||
boot_attr = self.json.get('Boot')
|
||||
if boot_attr is not None:
|
||||
self.boot['target'] = sys_maps.BOOT_SOURCE_TARGET_MAP.get(
|
||||
boot_attr.get('BootSourceOverrideTarget'))
|
||||
self.boot['enabled'] = sys_maps.BOOT_SOURCE_ENABLED_MAP.get(
|
||||
boot_attr.get('BootSourceOverrideEnabled'))
|
||||
self.boot['mode'] = sys_maps.BOOT_SOURCE_MODE_MAP.get(
|
||||
boot_attr.get('BootSourceOverrideMode'))
|
||||
|
||||
# Parse memory_summary attribute
|
||||
self.memory_summary = None
|
||||
memory_summary_attr = self.json.get('MemorySummary')
|
||||
if memory_summary_attr is not None:
|
||||
memory_size_gib = memory_summary_attr.get('TotalSystemMemoryGiB')
|
||||
try:
|
||||
memory_health = memory_summary_attr['Status']['HealthRollup']
|
||||
except KeyError:
|
||||
memory_health = None
|
||||
self.memory_summary = MemorySummary(size_gib=memory_size_gib,
|
||||
health=memory_health)
|
||||
|
||||
# Reset processor related attributes
|
||||
self._processors = None
|
||||
|
||||
def _get_reset_action_element(self):
|
||||
actions = self.json.get('Actions')
|
||||
if not actions:
|
||||
raise exceptions.MissingAttributeError(attribute='Actions',
|
||||
resource=self._path)
|
||||
|
||||
reset_action = actions.get('#ComputerSystem.Reset')
|
||||
reset_action = self._actions.reset
|
||||
# TODO(dtantsur): make this check also declarative?
|
||||
if not reset_action:
|
||||
raise exceptions.MissingActionError(action='#ComputerSystem.Reset',
|
||||
resource=self._path)
|
||||
@ -162,26 +148,14 @@ class System(base.ResourceBase):
|
||||
"""
|
||||
reset_action = self._get_reset_action_element()
|
||||
|
||||
allowed_values = reset_action.get('ResetType@Redfish.AllowableValues')
|
||||
if not allowed_values:
|
||||
if not reset_action.allowed_values:
|
||||
LOG.warning('Could not figure out the allowed values for the '
|
||||
'reset system action for System %s', self.identity)
|
||||
return set(sys_maps.RESET_SYSTEM_VALUE_MAP_REV)
|
||||
|
||||
return set([sys_maps.RESET_SYSTEM_VALUE_MAP[v] for v in
|
||||
set(sys_maps.RESET_SYSTEM_VALUE_MAP).
|
||||
intersection(allowed_values)])
|
||||
|
||||
def _get_reset_system_path(self):
|
||||
reset_action = self._get_reset_action_element()
|
||||
|
||||
target_uri = reset_action.get('target')
|
||||
if not target_uri:
|
||||
raise exceptions.MissingAttributeError(
|
||||
attribute='Actions/ComputerSystem.Reset/target',
|
||||
resource=self._path)
|
||||
|
||||
return target_uri
|
||||
intersection(reset_action.allowed_values)])
|
||||
|
||||
def reset_system(self, value):
|
||||
"""Reset the system.
|
||||
@ -196,7 +170,7 @@ class System(base.ResourceBase):
|
||||
parameter='value', value=value, valid_values=valid_resets)
|
||||
|
||||
value = sys_maps.RESET_SYSTEM_VALUE_MAP_REV[value]
|
||||
target_uri = self._get_reset_system_path()
|
||||
target_uri = self._get_reset_action_element().target_uri
|
||||
|
||||
# TODO(lucasagomes): Check the return code and response body ?
|
||||
# Probably we should call refresh() as well.
|
||||
@ -207,15 +181,7 @@ class System(base.ResourceBase):
|
||||
|
||||
:returns: A set with the allowed values.
|
||||
"""
|
||||
boot = self.json.get('Boot')
|
||||
if not boot:
|
||||
raise exceptions.MissingAttributeError(attribute='Boot',
|
||||
resource=self._path)
|
||||
|
||||
allowed_values = boot.get(
|
||||
'BootSourceOverrideTarget@Redfish.AllowableValues')
|
||||
|
||||
if not allowed_values:
|
||||
if not self.boot.allowed_values:
|
||||
LOG.warning('Could not figure out the allowed values for '
|
||||
'configuring the boot source for System %s',
|
||||
self.identity)
|
||||
@ -223,7 +189,7 @@ class System(base.ResourceBase):
|
||||
|
||||
return set([sys_maps.BOOT_SOURCE_TARGET_MAP[v] for v in
|
||||
set(sys_maps.BOOT_SOURCE_TARGET_MAP).
|
||||
intersection(allowed_values)])
|
||||
intersection(self.boot.allowed_values)])
|
||||
|
||||
def set_system_boot_source(self, target,
|
||||
enabled=sys_cons.BOOT_SOURCE_ENABLED_ONCE,
|
||||
@ -300,6 +266,10 @@ class System(base.ResourceBase):
|
||||
|
||||
return self._processors
|
||||
|
||||
def refresh(self):
|
||||
super(System, self).refresh()
|
||||
self._processors = None
|
||||
|
||||
|
||||
class SystemCollection(base.ResourceCollectionBase):
|
||||
|
||||
|
@ -55,37 +55,49 @@ class SystemTestCase(base.TestCase):
|
||||
self.sys_inst.uuid)
|
||||
self.assertEqual(sushy.SYSTEM_POWER_STATE_ON,
|
||||
self.sys_inst.power_state)
|
||||
self.assertEqual((96, "OK"),
|
||||
self.sys_inst.memory_summary)
|
||||
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
||||
self.assertEqual("OK", self.sys_inst.memory_summary.health)
|
||||
self.assertIsNone(self.sys_inst._processors)
|
||||
|
||||
def test__parse_attributes_missing_actions(self):
|
||||
self.sys_inst.json.pop('Actions')
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError, 'attribute Actions',
|
||||
self.sys_inst._parse_attributes)
|
||||
|
||||
def test__parse_attributes_missing_boot(self):
|
||||
self.sys_inst.json.pop('Boot')
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError, 'attribute Boot',
|
||||
self.sys_inst._parse_attributes)
|
||||
|
||||
def test__parse_attributes_missing_reset_target(self):
|
||||
self.sys_inst.json['Actions']['#ComputerSystem.Reset'].pop(
|
||||
'target')
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError,
|
||||
'attribute Actions/#ComputerSystem.Reset/target',
|
||||
self.sys_inst._parse_attributes)
|
||||
|
||||
def test_get__reset_action_element(self):
|
||||
value = self.sys_inst._get_reset_action_element()
|
||||
expected = {
|
||||
"target": "/redfish/v1/Systems/437XR1138R2/Actions/"
|
||||
self.assertEqual("/redfish/v1/Systems/437XR1138R2/Actions/"
|
||||
"ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": [
|
||||
"On",
|
||||
value.target_uri)
|
||||
self.assertEqual(["On",
|
||||
"ForceOff",
|
||||
"GracefulShutdown",
|
||||
"GracefulRestart",
|
||||
"ForceRestart",
|
||||
"Nmi",
|
||||
"ForceOn"
|
||||
]}
|
||||
self.assertEqual(expected, value)
|
||||
|
||||
def test_get__reset_action_element_missing_actions_attr(self):
|
||||
self.sys_inst._json.pop('Actions')
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError, 'attribute Actions',
|
||||
self.sys_inst._get_reset_action_element)
|
||||
],
|
||||
value.allowed_values)
|
||||
|
||||
def test_get__reset_action_element_missing_reset_action(self):
|
||||
action = '#ComputerSystem.Reset'
|
||||
self.sys_inst._json['Actions'].pop(action)
|
||||
self.sys_inst._actions.reset = None
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingActionError, 'action %s' % action,
|
||||
exceptions.MissingActionError, 'action #ComputerSystem.Reset',
|
||||
self.sys_inst._get_reset_action_element)
|
||||
|
||||
def test_get_allowed_reset_system_values(self):
|
||||
@ -101,11 +113,9 @@ class SystemTestCase(base.TestCase):
|
||||
self.assertIsInstance(values, set)
|
||||
|
||||
@mock.patch.object(system.LOG, 'warning', autospec=True)
|
||||
@mock.patch.object(system.System, '_get_reset_action_element',
|
||||
autospec=True)
|
||||
def test_get_allowed_reset_system_values_no_values_specified(
|
||||
self, mock_get_reset_action, mock_log):
|
||||
mock_get_reset_action.return_value = {}
|
||||
self, mock_log):
|
||||
self.sys_inst._actions.reset.allowed_values = {}
|
||||
values = self.sys_inst.get_allowed_reset_system_values()
|
||||
# Assert it returns all values if it can't get the specific ones
|
||||
expected = set([sushy.RESET_GRACEFUL_SHUTDOWN,
|
||||
@ -120,22 +130,6 @@ class SystemTestCase(base.TestCase):
|
||||
self.assertIsInstance(values, set)
|
||||
self.assertEqual(1, mock_log.call_count)
|
||||
|
||||
def test__get_reset_system_path(self):
|
||||
value = self.sys_inst._get_reset_system_path()
|
||||
expected = (
|
||||
'/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset')
|
||||
self.assertEqual(expected, value)
|
||||
|
||||
@mock.patch.object(system.System, '_get_reset_action_element',
|
||||
autospec=True)
|
||||
def test__get_reset_system_path_missing_target_attr(
|
||||
self, mock_get_reset_action):
|
||||
mock_get_reset_action.return_value = {}
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError,
|
||||
'attribute Actions/ComputerSystem.Reset/target',
|
||||
self.sys_inst._get_reset_system_path)
|
||||
|
||||
def test_reset_system(self):
|
||||
self.sys_inst.reset_system(sushy.RESET_FORCE_OFF)
|
||||
self.sys_inst._conn.post.assert_called_once_with(
|
||||
@ -161,17 +155,10 @@ class SystemTestCase(base.TestCase):
|
||||
self.assertEqual(expected, values)
|
||||
self.assertIsInstance(values, set)
|
||||
|
||||
def test_get_allowed_system_boot_source_values_missing_boot_attr(self):
|
||||
self.sys_inst._json.pop('Boot')
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MissingAttributeError, 'attribute Boot',
|
||||
self.sys_inst.get_allowed_system_boot_source_values)
|
||||
|
||||
@mock.patch.object(system.LOG, 'warning', autospec=True)
|
||||
def test_get_allowed_system_boot_source_values_no_values_specified(
|
||||
self, mock_log):
|
||||
self.sys_inst._json['Boot'].pop(
|
||||
'BootSourceOverrideTarget@Redfish.AllowableValues')
|
||||
self.sys_inst.boot.allowed_values = None
|
||||
values = self.sys_inst.get_allowed_system_boot_source_values()
|
||||
# Assert it returns all values if it can't get the specific ones
|
||||
expected = set([sushy.BOOT_SOURCE_TARGET_NONE,
|
||||
@ -229,11 +216,6 @@ class SystemTestCase(base.TestCase):
|
||||
self.sys_inst._get_processor_collection_path)
|
||||
|
||||
def test_memory_summary_missing_attr(self):
|
||||
self.assertIsInstance(self.sys_inst.memory_summary,
|
||||
system.MemorySummary)
|
||||
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
||||
self.assertEqual("OK", self.sys_inst.memory_summary.health)
|
||||
|
||||
# | GIVEN |
|
||||
self.sys_inst._json['MemorySummary']['Status'].pop('HealthRollup')
|
||||
# | WHEN |
|
||||
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
|
||||
from sushy import exceptions
|
||||
@ -123,3 +125,114 @@ class ResourceCollectionBaseTestCase(base.TestCase):
|
||||
self.assertTrue(isinstance(val, TestResource))
|
||||
self.assertTrue(val.identity in member_ids)
|
||||
self.assertEqual('1.0.x', val.redfish_version)
|
||||
|
||||
|
||||
TEST_JSON = {
|
||||
'String': 'a string',
|
||||
'Integer': '42',
|
||||
'List': ['a string', 42],
|
||||
'Nested': {
|
||||
'String': 'another string',
|
||||
'Integer': 0,
|
||||
'Object': {
|
||||
'Field': 'field value'
|
||||
},
|
||||
'Mapped': 'raw'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MAPPING = {
|
||||
'raw': 'real'
|
||||
}
|
||||
|
||||
|
||||
class NestedTestField(resource_base.CompositeField):
|
||||
string = resource_base.Field('String', required=True)
|
||||
integer = resource_base.Field('Integer', adapter=int)
|
||||
nested_field = resource_base.Field(['Object', 'Field'], required=True)
|
||||
mapped = resource_base.MappedField('Mapped', MAPPING)
|
||||
non_existing = resource_base.Field('NonExisting', default=3.14)
|
||||
|
||||
|
||||
class ComplexResource(resource_base.ResourceBase):
|
||||
string = resource_base.Field('String', required=True)
|
||||
integer = resource_base.Field('Integer', adapter=int)
|
||||
nested = NestedTestField('Nested')
|
||||
non_existing_nested = NestedTestField('NonExistingNested')
|
||||
non_existing_mapped = resource_base.MappedField('NonExistingMapped',
|
||||
MAPPING)
|
||||
|
||||
|
||||
class FieldTestCase(base.TestCase):
|
||||
def setUp(self):
|
||||
super(FieldTestCase, self).setUp()
|
||||
self.conn = mock.Mock()
|
||||
self.json = copy.deepcopy(TEST_JSON)
|
||||
self.conn.get.return_value.json.return_value = self.json
|
||||
self.test_resource = ComplexResource(self.conn,
|
||||
redfish_version='1.0.x')
|
||||
|
||||
def test_ok(self):
|
||||
self.assertEqual('a string', self.test_resource.string)
|
||||
self.assertEqual(42, self.test_resource.integer)
|
||||
self.assertEqual('another string', self.test_resource.nested.string)
|
||||
self.assertEqual(0, self.test_resource.nested.integer)
|
||||
self.assertEqual('field value', self.test_resource.nested.nested_field)
|
||||
self.assertEqual('real', self.test_resource.nested.mapped)
|
||||
self.assertEqual(3.14, self.test_resource.nested.non_existing)
|
||||
self.assertIsNone(self.test_resource.non_existing_nested)
|
||||
self.assertIsNone(self.test_resource.non_existing_mapped)
|
||||
|
||||
def test_missing_required(self):
|
||||
del self.json['String']
|
||||
self.assertRaisesRegex(exceptions.MissingAttributeError,
|
||||
'String', self.test_resource.refresh)
|
||||
|
||||
def test_missing_nested_required(self):
|
||||
del self.json['Nested']['String']
|
||||
self.assertRaisesRegex(exceptions.MissingAttributeError,
|
||||
'Nested/String', self.test_resource.refresh)
|
||||
|
||||
def test_missing_nested_required2(self):
|
||||
del self.json['Nested']['Object']['Field']
|
||||
self.assertRaisesRegex(exceptions.MissingAttributeError,
|
||||
'Nested/Object/Field',
|
||||
self.test_resource.refresh)
|
||||
|
||||
def test_malformed_int(self):
|
||||
self.json['Integer'] = 'banana'
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MalformedAttributeError,
|
||||
'attribute Integer is malformed.*invalid literal for int',
|
||||
self.test_resource.refresh)
|
||||
|
||||
def test_malformed_nested_int(self):
|
||||
self.json['Nested']['Integer'] = 'banana'
|
||||
self.assertRaisesRegex(
|
||||
exceptions.MalformedAttributeError,
|
||||
'attribute Nested/Integer is malformed.*invalid literal for int',
|
||||
self.test_resource.refresh)
|
||||
|
||||
def test_mapping_missing(self):
|
||||
self.json['Nested']['Mapped'] = 'banana'
|
||||
self.test_resource.refresh()
|
||||
|
||||
self.assertIsNone(self.test_resource.nested.mapped)
|
||||
|
||||
def test_composite_field_as_mapping(self):
|
||||
field = self.test_resource.nested
|
||||
keys = {'string', 'integer', 'nested_field', 'mapped', 'non_existing'}
|
||||
values = {'another string', 0, 'field value', 'real', 3.14}
|
||||
|
||||
self.assertEqual(keys, set(iter(field)))
|
||||
self.assertEqual(keys, set(field.keys()))
|
||||
self.assertEqual(values, set(field.values()))
|
||||
self.assertEqual(3.14, field['non_existing'])
|
||||
self.assertEqual(3.14, field.get('non_existing'))
|
||||
self.assertIsNone(field.get('foobar'))
|
||||
# Check KeyError from undefined fields
|
||||
self.assertRaisesRegex(KeyError, 'foobar', lambda: field['foobar'])
|
||||
# Regular attributes cannot be accessed via mapping
|
||||
self.assertRaisesRegex(KeyError, '_load', lambda: field['_load'])
|
||||
self.assertRaisesRegex(KeyError, '__init__', lambda: field['__init__'])
|
||||
|
Loading…
Reference in New Issue
Block a user