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')
|
'resource %(resource)s')
|
||||||
|
|
||||||
|
|
||||||
|
class MalformedAttributeError(SushyError):
|
||||||
|
message = ('The attribute %(attribute)s is malformed in the '
|
||||||
|
'resource %(resource)s: %(error)s')
|
||||||
|
|
||||||
|
|
||||||
class MissingActionError(SushyError):
|
class MissingActionError(SushyError):
|
||||||
message = ('The action %(action)s is missing from the '
|
message = ('The action %(action)s is missing from the '
|
||||||
'resource %(resource)s')
|
'resource %(resource)s')
|
||||||
|
|
|
@ -14,16 +14,169 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import collections
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from sushy import exceptions
|
||||||
from sushy import utils
|
from sushy import utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class ResourceBase(object):
|
class ResourceBase(object):
|
||||||
|
|
||||||
|
@ -46,6 +199,12 @@ class ResourceBase(object):
|
||||||
self.redfish_version = redfish_version
|
self.redfish_version = redfish_version
|
||||||
self.refresh()
|
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):
|
def refresh(self):
|
||||||
"""Refresh the resource
|
"""Refresh the resource
|
||||||
|
|
||||||
|
@ -69,22 +228,15 @@ class ResourceBase(object):
|
||||||
def path(self):
|
def path(self):
|
||||||
return self._path
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class ResourceCollectionBase(ResourceBase):
|
class ResourceCollectionBase(ResourceBase):
|
||||||
|
|
||||||
name = None
|
name = Field('Name')
|
||||||
"""The name of the collection"""
|
"""The name of the collection"""
|
||||||
|
|
||||||
members_identities = None
|
members_identities = Field('Members', default=[],
|
||||||
|
adapter=utils.get_members_identities)
|
||||||
"""A tuple with the members identities"""
|
"""A tuple with the members identities"""
|
||||||
|
|
||||||
def __init__(self, connector, path, redfish_version=None):
|
def __init__(self, connector, path, redfish_version=None):
|
||||||
|
@ -99,6 +251,9 @@ class ResourceCollectionBase(ResourceBase):
|
||||||
"""
|
"""
|
||||||
super(ResourceCollectionBase, self).__init__(connector, path,
|
super(ResourceCollectionBase, self).__init__(connector, path,
|
||||||
redfish_version)
|
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
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -109,14 +264,6 @@ class ResourceCollectionBase(ResourceBase):
|
||||||
collection contains.
|
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):
|
def get_member(self, identity):
|
||||||
"""Given the identity return a ``_resource_type`` object
|
"""Given the identity return a ``_resource_type`` object
|
||||||
|
|
||||||
|
|
|
@ -26,36 +26,37 @@ LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Processor(base.ResourceBase):
|
class Processor(base.ResourceBase):
|
||||||
|
|
||||||
identity = None
|
identity = base.Field('Id', required=True)
|
||||||
"""The processor identity string"""
|
"""The processor identity string"""
|
||||||
|
|
||||||
socket = None
|
socket = base.Field('Socket')
|
||||||
"""The socket or location of the processor"""
|
"""The socket or location of the processor"""
|
||||||
|
|
||||||
# TODO(deray): Create mappings for the processor_type
|
# TODO(deray): Create mappings for the processor_type
|
||||||
processor_type = None
|
processor_type = base.Field('ProcessorType')
|
||||||
"""The type of processor"""
|
"""The type of processor"""
|
||||||
|
|
||||||
processor_architecture = None
|
processor_architecture = base.MappedField(
|
||||||
|
'ProcessorArchitecture', sys_maps.PROCESSOR_ARCH_VALUE_MAP)
|
||||||
"""The architecture of the processor"""
|
"""The architecture of the processor"""
|
||||||
|
|
||||||
# TODO(deray): Create mappings for the instruction_set
|
# TODO(deray): Create mappings for the instruction_set
|
||||||
instruction_set = None
|
instruction_set = base.Field('InstructionSet')
|
||||||
"""The instruction set of the processor"""
|
"""The instruction set of the processor"""
|
||||||
|
|
||||||
manufacturer = None
|
manufacturer = base.Field('Manufacturer')
|
||||||
"""The processor manufacturer"""
|
"""The processor manufacturer"""
|
||||||
|
|
||||||
model = None
|
model = base.Field('Model')
|
||||||
"""The product model number of this device"""
|
"""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."""
|
"""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"""
|
"""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"""
|
"""The total number of execution threads supported by this processor"""
|
||||||
|
|
||||||
def __init__(self, connector, identity, redfish_version=None):
|
def __init__(self, connector, identity, redfish_version=None):
|
||||||
|
@ -68,20 +69,6 @@ class Processor(base.ResourceBase):
|
||||||
"""
|
"""
|
||||||
super(Processor, self).__init__(connector, identity, redfish_version)
|
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):
|
class ProcessorCollection(base.ResourceCollectionBase):
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sushy import exceptions
|
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 mappings as sys_maps
|
||||||
from sushy.resources.system import processor
|
from sushy.resources.system import processor
|
||||||
|
|
||||||
# Representation of Memory information summary
|
|
||||||
MemorySummary = collections.namedtuple('MemorySummary',
|
|
||||||
['size_gib', 'health'])
|
|
||||||
LOG = logging.getLogger(__name__)
|
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):
|
class System(base.ResourceBase):
|
||||||
|
|
||||||
asset_tag = None
|
asset_tag = base.Field('AssetTag')
|
||||||
"""The system asset tag"""
|
"""The system asset tag"""
|
||||||
|
|
||||||
bios_version = None
|
bios_version = base.Field('BiosVersion')
|
||||||
"""The system BIOS version"""
|
"""The system BIOS version"""
|
||||||
|
|
||||||
boot = None
|
boot = BootField('Boot', required=True)
|
||||||
"""A dictionary containg the current boot device, frequency and mode"""
|
"""A dictionary containg the current boot device, frequency and mode"""
|
||||||
|
|
||||||
description = None
|
description = base.Field('Description')
|
||||||
"""The system description"""
|
"""The system description"""
|
||||||
|
|
||||||
hostname = None
|
hostname = base.Field('HostName')
|
||||||
"""The system hostname"""
|
"""The system hostname"""
|
||||||
|
|
||||||
identity = None
|
identity = base.Field('Id', required=True)
|
||||||
"""The system identity string"""
|
"""The system identity string"""
|
||||||
|
|
||||||
# TODO(lucasagomes): Create mappings for the indicator_led
|
# TODO(lucasagomes): Create mappings for the indicator_led
|
||||||
indicator_led = None
|
indicator_led = base.Field('IndicatorLED')
|
||||||
"""Whether the indicator LED is lit or off"""
|
"""Whether the indicator LED is lit or off"""
|
||||||
|
|
||||||
manufacturer = None
|
manufacturer = base.Field('Manufacturer')
|
||||||
"""The system manufacturer"""
|
"""The system manufacturer"""
|
||||||
|
|
||||||
name = None
|
name = base.Field('Name')
|
||||||
"""The system name"""
|
"""The system name"""
|
||||||
|
|
||||||
part_number = None
|
part_number = base.Field('PartNumber')
|
||||||
"""The system part number"""
|
"""The system part number"""
|
||||||
|
|
||||||
power_state = None
|
power_state = base.MappedField('PowerState',
|
||||||
|
sys_maps.SYSTEM_POWER_STATE_MAP)
|
||||||
"""The system power state"""
|
"""The system power state"""
|
||||||
|
|
||||||
serial_number = None
|
serial_number = base.Field('SerialNumber')
|
||||||
"""The system serial number"""
|
"""The system serial number"""
|
||||||
|
|
||||||
sku = None
|
sku = base.Field('SKU')
|
||||||
"""The system stock-keeping unit"""
|
"""The system stock-keeping unit"""
|
||||||
|
|
||||||
# TODO(lucasagomes): Create mappings for the system_type
|
# TODO(lucasagomes): Create mappings for the system_type
|
||||||
system_type = None
|
system_type = base.Field('SystemType')
|
||||||
"""The system type"""
|
"""The system type"""
|
||||||
|
|
||||||
uuid = None
|
uuid = base.Field('UUID')
|
||||||
"""The system UUID"""
|
"""The system UUID"""
|
||||||
|
|
||||||
memory_summary = None
|
memory_summary = MemorySummaryField('MemorySummary')
|
||||||
"""The summary info of memory of the system in general detail
|
"""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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_processors = None # ref to ProcessorCollection instance
|
_processors = None # ref to ProcessorCollection instance
|
||||||
|
|
||||||
|
_actions = ActionsField('Actions', required=True)
|
||||||
|
|
||||||
def __init__(self, connector, identity, redfish_version=None):
|
def __init__(self, connector, identity, redfish_version=None):
|
||||||
"""A class representing a ComputerSystem
|
"""A class representing a ComputerSystem
|
||||||
|
|
||||||
|
@ -100,56 +133,9 @@ class System(base.ResourceBase):
|
||||||
"""
|
"""
|
||||||
super(System, self).__init__(connector, identity, redfish_version)
|
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):
|
def _get_reset_action_element(self):
|
||||||
actions = self.json.get('Actions')
|
reset_action = self._actions.reset
|
||||||
if not actions:
|
# TODO(dtantsur): make this check also declarative?
|
||||||
raise exceptions.MissingAttributeError(attribute='Actions',
|
|
||||||
resource=self._path)
|
|
||||||
|
|
||||||
reset_action = actions.get('#ComputerSystem.Reset')
|
|
||||||
if not reset_action:
|
if not reset_action:
|
||||||
raise exceptions.MissingActionError(action='#ComputerSystem.Reset',
|
raise exceptions.MissingActionError(action='#ComputerSystem.Reset',
|
||||||
resource=self._path)
|
resource=self._path)
|
||||||
|
@ -162,26 +148,14 @@ class System(base.ResourceBase):
|
||||||
"""
|
"""
|
||||||
reset_action = self._get_reset_action_element()
|
reset_action = self._get_reset_action_element()
|
||||||
|
|
||||||
allowed_values = reset_action.get('ResetType@Redfish.AllowableValues')
|
if not reset_action.allowed_values:
|
||||||
if not allowed_values:
|
|
||||||
LOG.warning('Could not figure out the allowed values for the '
|
LOG.warning('Could not figure out the allowed values for the '
|
||||||
'reset system action for System %s', self.identity)
|
'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_REV)
|
||||||
|
|
||||||
return set([sys_maps.RESET_SYSTEM_VALUE_MAP[v] for v in
|
return set([sys_maps.RESET_SYSTEM_VALUE_MAP[v] for v in
|
||||||
set(sys_maps.RESET_SYSTEM_VALUE_MAP).
|
set(sys_maps.RESET_SYSTEM_VALUE_MAP).
|
||||||
intersection(allowed_values)])
|
intersection(reset_action.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
|
|
||||||
|
|
||||||
def reset_system(self, value):
|
def reset_system(self, value):
|
||||||
"""Reset the system.
|
"""Reset the system.
|
||||||
|
@ -196,7 +170,7 @@ class System(base.ResourceBase):
|
||||||
parameter='value', value=value, valid_values=valid_resets)
|
parameter='value', value=value, valid_values=valid_resets)
|
||||||
|
|
||||||
value = sys_maps.RESET_SYSTEM_VALUE_MAP_REV[value]
|
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 ?
|
# TODO(lucasagomes): Check the return code and response body ?
|
||||||
# Probably we should call refresh() as well.
|
# Probably we should call refresh() as well.
|
||||||
|
@ -207,15 +181,7 @@ class System(base.ResourceBase):
|
||||||
|
|
||||||
:returns: A set with the allowed values.
|
:returns: A set with the allowed values.
|
||||||
"""
|
"""
|
||||||
boot = self.json.get('Boot')
|
if not self.boot.allowed_values:
|
||||||
if not boot:
|
|
||||||
raise exceptions.MissingAttributeError(attribute='Boot',
|
|
||||||
resource=self._path)
|
|
||||||
|
|
||||||
allowed_values = boot.get(
|
|
||||||
'BootSourceOverrideTarget@Redfish.AllowableValues')
|
|
||||||
|
|
||||||
if not allowed_values:
|
|
||||||
LOG.warning('Could not figure out the allowed values for '
|
LOG.warning('Could not figure out the allowed values for '
|
||||||
'configuring the boot source for System %s',
|
'configuring the boot source for System %s',
|
||||||
self.identity)
|
self.identity)
|
||||||
|
@ -223,7 +189,7 @@ class System(base.ResourceBase):
|
||||||
|
|
||||||
return set([sys_maps.BOOT_SOURCE_TARGET_MAP[v] for v in
|
return set([sys_maps.BOOT_SOURCE_TARGET_MAP[v] for v in
|
||||||
set(sys_maps.BOOT_SOURCE_TARGET_MAP).
|
set(sys_maps.BOOT_SOURCE_TARGET_MAP).
|
||||||
intersection(allowed_values)])
|
intersection(self.boot.allowed_values)])
|
||||||
|
|
||||||
def set_system_boot_source(self, target,
|
def set_system_boot_source(self, target,
|
||||||
enabled=sys_cons.BOOT_SOURCE_ENABLED_ONCE,
|
enabled=sys_cons.BOOT_SOURCE_ENABLED_ONCE,
|
||||||
|
@ -300,6 +266,10 @@ class System(base.ResourceBase):
|
||||||
|
|
||||||
return self._processors
|
return self._processors
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
super(System, self).refresh()
|
||||||
|
self._processors = None
|
||||||
|
|
||||||
|
|
||||||
class SystemCollection(base.ResourceCollectionBase):
|
class SystemCollection(base.ResourceCollectionBase):
|
||||||
|
|
||||||
|
|
|
@ -55,37 +55,49 @@ class SystemTestCase(base.TestCase):
|
||||||
self.sys_inst.uuid)
|
self.sys_inst.uuid)
|
||||||
self.assertEqual(sushy.SYSTEM_POWER_STATE_ON,
|
self.assertEqual(sushy.SYSTEM_POWER_STATE_ON,
|
||||||
self.sys_inst.power_state)
|
self.sys_inst.power_state)
|
||||||
self.assertEqual((96, "OK"),
|
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
|
||||||
self.sys_inst.memory_summary)
|
self.assertEqual("OK", self.sys_inst.memory_summary.health)
|
||||||
self.assertIsNone(self.sys_inst._processors)
|
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):
|
def test_get__reset_action_element(self):
|
||||||
value = self.sys_inst._get_reset_action_element()
|
value = self.sys_inst._get_reset_action_element()
|
||||||
expected = {
|
self.assertEqual("/redfish/v1/Systems/437XR1138R2/Actions/"
|
||||||
"target": "/redfish/v1/Systems/437XR1138R2/Actions/"
|
"ComputerSystem.Reset",
|
||||||
"ComputerSystem.Reset",
|
value.target_uri)
|
||||||
"ResetType@Redfish.AllowableValues": [
|
self.assertEqual(["On",
|
||||||
"On",
|
"ForceOff",
|
||||||
"ForceOff",
|
"GracefulShutdown",
|
||||||
"GracefulShutdown",
|
"GracefulRestart",
|
||||||
"GracefulRestart",
|
"ForceRestart",
|
||||||
"ForceRestart",
|
"Nmi",
|
||||||
"Nmi",
|
"ForceOn"
|
||||||
"ForceOn"
|
],
|
||||||
]}
|
value.allowed_values)
|
||||||
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)
|
|
||||||
|
|
||||||
def test_get__reset_action_element_missing_reset_action(self):
|
def test_get__reset_action_element_missing_reset_action(self):
|
||||||
action = '#ComputerSystem.Reset'
|
self.sys_inst._actions.reset = None
|
||||||
self.sys_inst._json['Actions'].pop(action)
|
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
exceptions.MissingActionError, 'action %s' % action,
|
exceptions.MissingActionError, 'action #ComputerSystem.Reset',
|
||||||
self.sys_inst._get_reset_action_element)
|
self.sys_inst._get_reset_action_element)
|
||||||
|
|
||||||
def test_get_allowed_reset_system_values(self):
|
def test_get_allowed_reset_system_values(self):
|
||||||
|
@ -101,11 +113,9 @@ class SystemTestCase(base.TestCase):
|
||||||
self.assertIsInstance(values, set)
|
self.assertIsInstance(values, set)
|
||||||
|
|
||||||
@mock.patch.object(system.LOG, 'warning', autospec=True)
|
@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(
|
def test_get_allowed_reset_system_values_no_values_specified(
|
||||||
self, mock_get_reset_action, mock_log):
|
self, mock_log):
|
||||||
mock_get_reset_action.return_value = {}
|
self.sys_inst._actions.reset.allowed_values = {}
|
||||||
values = self.sys_inst.get_allowed_reset_system_values()
|
values = self.sys_inst.get_allowed_reset_system_values()
|
||||||
# Assert it returns all values if it can't get the specific ones
|
# Assert it returns all values if it can't get the specific ones
|
||||||
expected = set([sushy.RESET_GRACEFUL_SHUTDOWN,
|
expected = set([sushy.RESET_GRACEFUL_SHUTDOWN,
|
||||||
|
@ -120,22 +130,6 @@ class SystemTestCase(base.TestCase):
|
||||||
self.assertIsInstance(values, set)
|
self.assertIsInstance(values, set)
|
||||||
self.assertEqual(1, mock_log.call_count)
|
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):
|
def test_reset_system(self):
|
||||||
self.sys_inst.reset_system(sushy.RESET_FORCE_OFF)
|
self.sys_inst.reset_system(sushy.RESET_FORCE_OFF)
|
||||||
self.sys_inst._conn.post.assert_called_once_with(
|
self.sys_inst._conn.post.assert_called_once_with(
|
||||||
|
@ -161,17 +155,10 @@ class SystemTestCase(base.TestCase):
|
||||||
self.assertEqual(expected, values)
|
self.assertEqual(expected, values)
|
||||||
self.assertIsInstance(values, set)
|
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)
|
@mock.patch.object(system.LOG, 'warning', autospec=True)
|
||||||
def test_get_allowed_system_boot_source_values_no_values_specified(
|
def test_get_allowed_system_boot_source_values_no_values_specified(
|
||||||
self, mock_log):
|
self, mock_log):
|
||||||
self.sys_inst._json['Boot'].pop(
|
self.sys_inst.boot.allowed_values = None
|
||||||
'BootSourceOverrideTarget@Redfish.AllowableValues')
|
|
||||||
values = self.sys_inst.get_allowed_system_boot_source_values()
|
values = self.sys_inst.get_allowed_system_boot_source_values()
|
||||||
# Assert it returns all values if it can't get the specific ones
|
# Assert it returns all values if it can't get the specific ones
|
||||||
expected = set([sushy.BOOT_SOURCE_TARGET_NONE,
|
expected = set([sushy.BOOT_SOURCE_TARGET_NONE,
|
||||||
|
@ -229,11 +216,6 @@ class SystemTestCase(base.TestCase):
|
||||||
self.sys_inst._get_processor_collection_path)
|
self.sys_inst._get_processor_collection_path)
|
||||||
|
|
||||||
def test_memory_summary_missing_attr(self):
|
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 |
|
# | GIVEN |
|
||||||
self.sys_inst._json['MemorySummary']['Status'].pop('HealthRollup')
|
self.sys_inst._json['MemorySummary']['Status'].pop('HealthRollup')
|
||||||
# | WHEN |
|
# | WHEN |
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from sushy import exceptions
|
from sushy import exceptions
|
||||||
|
@ -123,3 +125,114 @@ class ResourceCollectionBaseTestCase(base.TestCase):
|
||||||
self.assertTrue(isinstance(val, TestResource))
|
self.assertTrue(isinstance(val, TestResource))
|
||||||
self.assertTrue(val.identity in member_ids)
|
self.assertTrue(val.identity in member_ids)
|
||||||
self.assertEqual('1.0.x', val.redfish_version)
|
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