Add system simple storage resource support

Adds the simple storage resource of Redfish standard schema.
This new resource represents the properties of a storage
controller and its directly-attached devices. This patch
introduces the property ``max_size_bytes`` of
SimpleStorageCollection resource to expose the size of the
largest storage size available among all directly attached
devices available to the System.

Also brought in the common 'Status' (comprising of Health,
HealthRollup and State sub-fields) field and refactored the code
base to use that field across all the Redfish resources.

Story: 1668487
Task: 23041

Change-Id: I512c2507bf78f4a9cf1e2525fd685836387a7581
This commit is contained in:
Debayan Ray 2018-04-16 10:51:58 +00:00
parent fdeb8b8d44
commit d44059483c
19 changed files with 394 additions and 85 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Adds the "SimpleStorage" to the library. It also provides the max size
available (in bytes) among all its directly attached devices.

View File

@ -17,6 +17,7 @@ import logging
import pbr.version
from sushy.main import Sushy
from sushy.resources.constants import * # noqa
from sushy.resources.system.constants import * # noqa
from sushy.resources.manager.constants import * # noqa

View File

@ -11,6 +11,7 @@
# under the License.
from sushy.resources import base
from sushy.resources import mappings as res_maps
class ActionField(base.CompositeField):
@ -23,7 +24,22 @@ class ResetActionField(ActionField):
class IdRefField(base.CompositeField):
"""Reference to the resource for updating settings"""
"""Reference to the resource odata identity field."""
resource_uri = base.Field('@odata.id')
"""The unique identifier for a resource"""
class StatusField(base.CompositeField):
"""This Field describes the status of a resource and its children.
This field shall contain any state or health properties of a resource.
"""
health = base.MappedField('Health', res_maps.HEALTH_VALUE_MAP)
"""Represents health of resource w/o considering its dependent resources"""
health_rollup = base.MappedField('HealthRollup', res_maps.HEALTH_VALUE_MAP)
"""Represents health state of resource and its dependent resources"""
state = base.MappedField('State', res_maps.STATE_VALUE_MAP)
"""Indicates the known state of the resource, such as if it is enabled."""

View File

@ -0,0 +1,32 @@
# 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.
# Values comes from the Redfish System json-schema 1.0.0:
# http://redfish.dmtf.org/schemas/v1/Resource.json
# Health related constants.
HEALTH_OK = 'ok'
HEALTH_WARNING = 'warning'
HEALTH_CRITICAL = 'critical'
# State related constants.
STATE_ENABLED = 'enabled'
STATE_DISABLED = 'disabled'
STATE_STANDBYOFFLINE = 'standby offline'
STATE_STANDBYSPARE = 'standby spare'
STATE_INTEST = 'in test'
STATE_STARTING = 'starting'
STATE_ABSENT = 'absent'
STATE_UNAVAILABLEOFFLINE = 'unavailable offline'
STATE_DEFERRING = 'deferring'
STATE_QUIESCED = 'quiesced'
STATE_UPDATING = 'updating'

View File

@ -0,0 +1,36 @@
# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# 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 import constants as res_cons
from sushy import utils
STATE_VALUE_MAP = {
'Enabled': res_cons.STATE_ENABLED,
'Disabled': res_cons.STATE_DISABLED,
'Absent': res_cons.STATE_ABSENT,
}
STATE_VALUE_MAP_REV = (
utils.revert_dictionary(STATE_VALUE_MAP))
HEALTH_VALUE_MAP = {
'OK': res_cons.HEALTH_OK,
'Warning': res_cons.HEALTH_WARNING,
'Critical': res_cons.HEALTH_CRITICAL
}
HEALTH_VALUE_MAP_REV = (
utils.revert_dictionary(HEALTH_VALUE_MAP))

View File

@ -122,10 +122,3 @@ PROCESSOR_ARCH_IA_64 = 'Intel Itanium'
PROCESSOR_ARCH_ARM = 'ARM'
PROCESSOR_ARCH_MIPS = 'MIPS'
PROCESSOR_ARCH_OEM = 'OEM-defined'
# Health related constants.
HEALTH_STATE_ENABLED = 'enabled'
HEALTH_STATE_DISABLED = 'disabled'
HEALTH_OK = 'ok'
HEALTH_WARNING = 'warning'
HEALTH_CRITICAL = 'critical'

View File

@ -16,18 +16,12 @@
import logging
from sushy.resources import base
from sushy.resources.system import constants as sys_cons
from sushy.resources.system import mappings as sys_map
from sushy.resources import common
from sushy.resources import constants as res_cons
LOG = logging.getLogger(__name__)
class HealthStatusField(base.CompositeField):
state = base.MappedField(
'State', sys_map.HEALTH_STATE_VALUE_MAP)
health = base.Field('Health')
class EthernetInterface(base.ResourceBase):
"""This class adds the EthernetInterface resource"""
@ -49,7 +43,8 @@ class EthernetInterface(base.ResourceBase):
speed_mbps = base.Field('SpeedMbps')
"""This is the current speed in Mbps of this interface."""
status = HealthStatusField("Status")
status = common.StatusField("Status")
"""Describes the status and health of this interface."""
class EthernetInterfaceCollection(base.ResourceCollectionBase):
@ -69,19 +64,15 @@ class EthernetInterfaceCollection(base.ResourceCollectionBase):
are returned.
:returns: dictionary in the format
{'aa:bb:cc:dd:ee:ff': 'Enabled',
'aa:bb:aa:aa:aa:aa': 'Disabled'}
{'aa:bb:cc:dd:ee:ff': sushy.STATE_ENABLED,
'aa:bb:aa:aa:aa:aa': sushy.STATE_DISABLED}
"""
if self._summary is None:
mac_dict = {}
for eth in self.get_members():
if (eth.mac_address is not None and eth.status is not None):
if (eth.status.health ==
sys_map.HEALTH_VALUE_MAP_REV.get(
sys_cons.HEALTH_OK)):
state = sys_map.HEALTH_STATE_VALUE_MAP_REV.get(
eth.status.state)
mac_dict[eth.mac_address] = state
if eth.mac_address is not None and eth.status is not None:
if eth.status.health == res_cons.HEALTH_OK:
mac_dict[eth.mac_address] = eth.status.state
self._summary = mac_dict
return self._summary

View File

@ -89,20 +89,3 @@ PROCESSOR_ARCH_VALUE_MAP = {
PROCESSOR_ARCH_VALUE_MAP_REV = (
utils.revert_dictionary(PROCESSOR_ARCH_VALUE_MAP))
HEALTH_STATE_VALUE_MAP = {
'Enabled': sys_cons.HEALTH_STATE_ENABLED,
'Disabled': sys_cons.HEALTH_STATE_DISABLED,
}
HEALTH_STATE_VALUE_MAP_REV = (
utils.revert_dictionary(HEALTH_STATE_VALUE_MAP))
HEALTH_VALUE_MAP = {
'OK': sys_cons.HEALTH_OK,
'Warning': sys_cons.HEALTH_WARNING,
'Critical': sys_cons.HEALTH_CRITICAL
}
HEALTH_VALUE_MAP_REV = (
utils.revert_dictionary(HEALTH_VALUE_MAP))

View File

@ -16,6 +16,7 @@ import collections
import logging
from sushy.resources import base
from sushy.resources import common
from sushy.resources.system import mappings as sys_maps
# Representation of Summary of Processor information
@ -45,18 +46,6 @@ class ProcessorIdField(base.CompositeField):
"""The processor vendor id"""
class StatusField(base.CompositeField):
health = base.Field('Health')
"""The processor health"""
health_rollup = base.Field('HealthRollup')
"""The processor health rollup"""
state = base.Field('State')
"""The processor state"""
class Processor(base.ResourceBase):
identity = base.Field('Id', required=True)
@ -89,7 +78,7 @@ class Processor(base.ResourceBase):
processor_id = ProcessorIdField('ProcessorId')
"""The processor id"""
status = StatusField('Status')
status = common.StatusField('Status')
"""The processor status"""
total_cores = base.Field('TotalCores', adapter=int)

View File

@ -0,0 +1,86 @@
# 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.
# This is referred from Redfish standard schema.
# http://redfish.dmtf.org/schemas/v1/SimpleStorage.v1_2_0.json
import logging
from sushy.resources import base
from sushy.resources import common
from sushy.resources import constants as res_cons
from sushy import utils
LOG = logging.getLogger(__name__)
class DeviceListField(base.ListField):
"""The storage device/s associated with SimpleStorage."""
name = base.Field('Name', required=True)
"""The name of the storage device"""
capacity_bytes = base.Field('CapacityBytes', adapter=utils.int_or_none)
"""The size of the storage device."""
status = common.StatusField('Status')
"""Describes the status and health of a storage device."""
class SimpleStorage(base.ResourceBase):
"""This class represents a simple storage.
It represents the properties of a storage controller and its
directly-attached devices. A storage device can be a disk drive or optical
media device.
"""
identity = base.Field('Id', required=True)
"""The SimpleStorage identity string"""
name = base.Field('Name')
"""The name of the resource"""
devices = DeviceListField('Devices', default=[])
"""The storage devices associated with this resource."""
class SimpleStorageCollection(base.ResourceCollectionBase):
"""Represents a collection of simple storage associated with system."""
_max_size_bytes = None
@property
def _resource_type(self):
return SimpleStorage
@property
def max_size_bytes(self):
"""Max size available (in bytes) among all enabled device resources.
It returns the cached value until it (or its parent resource) is
refreshed.
"""
if self._max_size_bytes is None:
self._max_size_bytes = (
utils.max_safe(device.capacity_bytes
for simpl_stor in self.get_members()
for device in simpl_stor.devices
if (device.status.state ==
res_cons.STATE_ENABLED)))
return self._max_size_bytes
def _do_refresh(self, force=False):
# Note(deray): undefine the attribute here for fresh creation in
# subsequent calls to it's exposed property.
self._max_size_bytes = None

View File

@ -63,12 +63,6 @@ class MemorySummaryField(base.CompositeField):
"""
class StatusField(base.CompositeField):
state = base.Field('State')
health = base.Field('Health')
health_rollup = base.Field('HealthRollup')
class System(base.ResourceBase):
asset_tag = base.Field('AssetTag')
@ -112,7 +106,7 @@ class System(base.ResourceBase):
sku = base.Field('SKU')
"""The system stock-keeping unit"""
status = StatusField('Status')
status = common.StatusField('Status')
"""The system status"""
# TODO(lucasagomes): Create mappings for the system_type

View File

@ -0,0 +1,59 @@
{
"@odata.type": "#SimpleStorage.v1_2_0.SimpleStorage",
"Id": "1",
"Name": "Simple Storage Controller",
"Description": "System SATA",
"UefiDevicePath": "Acpi(PNP0A03,0)/Pci(1F|1)/Ata(Primary,Master)/HD(Part3, Sig00110011)",
"Status": {
"@odata.type": "#Resource.Status",
"State": "Enabled",
"Health": "OK",
"HealthRollup": "Warning"
},
"Devices": [
{
"@odata.type": "#SimpleStorage.v1_1_0.Device",
"Name": "SATA Bay 1",
"Manufacturer": "Contoso",
"Model": "3000GT8",
"CapacityBytes": 8000000000000,
"Status": {
"@odata.type": "#Resource.Status",
"State": "Enabled",
"Health": "OK"
}
},
{
"@odata.type": "#SimpleStorage.v1_1_0.Device",
"Name": "SATA Bay 2",
"Manufacturer": "Contoso",
"Model": "3000GT7",
"CapacityBytes": 4000000000000,
"Status": {
"@odata.type": "#Resource.Status",
"State": "Enabled",
"Health": "Critical"
}
},
{
"@odata.type": "#SimpleStorage.v1_1_0.Device",
"Name": "SATA Bay 3",
"CapacityBytes": 9000000000000,
"Status": {
"@odata.type": "#Resource.Status",
"State": "Absent"
}
},
{
"@odata.type": "#SimpleStorage.v1_1_0.Device",
"Name": "SATA Bay 4",
"Status": {
"@odata.type": "#Resource.Status",
"State": "Absent"
}
}
],
"@odata.context": "/redfish/v1/$metadata#SimpleStorage.SimpleStorage",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1",
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -0,0 +1,13 @@
{
"@odata.type": "#SimpleStorageCollection.SimpleStorageCollection",
"Name": "Simple Storage Collection",
"Members@odata.count": 1,
"Members": [
{
"@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1"
}
],
"@odata.context": "/redfish/v1/$metadata#SimpleStorageCollection.SimpleStorageCollection",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage",
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -23,8 +23,7 @@ class DriveTestCase(base.TestCase):
def setUp(self):
super(DriveTestCase, self).setUp()
self.conn = mock.Mock()
drive_file = 'sushy/tests/unit/json_samples/drive.json'
with open(drive_file, 'r') as f:
with open('sushy/tests/unit/json_samples/drive.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.stor_drive = drive.Drive(

View File

@ -31,8 +31,7 @@ class StorageTestCase(base.TestCase):
def setUp(self):
super(StorageTestCase, self).setUp()
self.conn = mock.Mock()
storage_file = 'sushy/tests/unit/json_samples/storage.json'
with open(storage_file, 'r') as f:
with open('sushy/tests/unit/json_samples/storage.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.storage = storage.Storage(

View File

@ -14,9 +14,8 @@ import json
import mock
from sushy.resources.system import constants as sys_cons
from sushy.resources import constants as res_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import mappings as sys_map
from sushy.tests.unit import base
@ -43,8 +42,8 @@ class EthernetInterfaceTestCase(base.TestCase):
self.assertEqual(
'12:44:6A:3B:04:11', self.sys_eth.permanent_mac_address)
self.assertEqual('12:44:6A:3B:04:11', self.sys_eth.mac_address)
self.assertEqual('enabled', self.sys_eth.status.state)
self.assertEqual('OK', self.sys_eth.status.health)
self.assertEqual(res_cons.STATE_ENABLED, self.sys_eth.status.state)
self.assertEqual(res_cons.HEALTH_OK, self.sys_eth.status.health)
self.assertEqual(1000, self.sys_eth.speed_mbps)
@ -99,9 +98,6 @@ class EthernetInterfaceCollectionTestCase(base.TestCase):
with open('sushy/tests/unit/json_samples/'
'ethernet_interfaces.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
expected_summary = {
'12:44:6A:3B:04:11':
sys_map.HEALTH_STATE_VALUE_MAP_REV.get(
sys_cons.HEALTH_STATE_ENABLED)}
expected_summary = {'12:44:6A:3B:04:11': res_cons.STATE_ENABLED}
actual_summary = self.sys_eth_col.summary
self.assertEqual(expected_summary, actual_summary)

View File

@ -17,6 +17,7 @@ import json
import mock
import sushy
from sushy.resources import constants as res_cons
from sushy.resources.system import processor
from sushy.tests.unit import base
@ -63,9 +64,11 @@ class ProcessorTestCase(base.TestCase):
self.assertEqual(3700, self.sys_processor.max_speed_mhz)
self.assertEqual(8, self.sys_processor.total_cores)
self.assertEqual(16, self.sys_processor.total_threads)
self.assertEqual('Enabled', self.sys_processor.status.state)
self.assertEqual('OK', self.sys_processor.status.health)
self.assertEqual('OK', self.sys_processor.status.health_rollup)
self.assertEqual(res_cons.STATE_ENABLED,
self.sys_processor.status.state)
self.assertEqual(res_cons.HEALTH_OK, self.sys_processor.status.health)
self.assertEqual(res_cons.HEALTH_OK,
self.sys_processor.status.health_rollup)
class ProcessorCollectionTestCase(base.TestCase):

View File

@ -0,0 +1,115 @@
# 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 import constants as res_cons
from sushy.resources.system import simple_storage
from sushy.tests.unit import base
class SimpleStorageTestCase(base.TestCase):
def setUp(self):
super(SimpleStorageTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'simple_storage.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.simpl_stor = simple_storage.SimpleStorage(
self.conn, '/redfish/v1/Systems/437XR1138R2/SimpleStorage/1',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.simpl_stor._parse_attributes()
self.assertEqual('1.0.2', self.simpl_stor.redfish_version)
self.assertEqual('1', self.simpl_stor.identity)
self.assertEqual('Simple Storage Controller', self.simpl_stor.name)
self.assertEqual(8000000000000,
self.simpl_stor.devices[0].capacity_bytes)
self.assertEqual(4000000000000,
self.simpl_stor.devices[1].capacity_bytes)
self.assertEqual(res_cons.STATE_ENABLED,
self.simpl_stor.devices[0].status.state)
self.assertEqual(res_cons.STATE_ABSENT,
self.simpl_stor.devices[2].status.state)
self.assertEqual(res_cons.HEALTH_OK,
self.simpl_stor.devices[0].status.health)
class SimpleStorageCollectionTestCase(base.TestCase):
def setUp(self):
super(SimpleStorageCollectionTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'simple_storage_collection.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.simpl_stor_col = simple_storage.SimpleStorageCollection(
self.conn, '/redfish/v1/Systems/437XR1138R2/SimpleStorage',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.simpl_stor_col._parse_attributes()
self.assertEqual((
'/redfish/v1/Systems/437XR1138R2/SimpleStorage/1',),
self.simpl_stor_col.members_identities)
@mock.patch.object(simple_storage, 'SimpleStorage', autospec=True)
def test_get_member(self, SimpleStorage_mock):
self.simpl_stor_col.get_member(
'/redfish/v1/Systems/437XR1138R2/SimpleStorage/1')
SimpleStorage_mock.assert_called_once_with(
self.simpl_stor_col._conn,
'/redfish/v1/Systems/437XR1138R2/SimpleStorage/1',
redfish_version=self.simpl_stor_col.redfish_version)
@mock.patch.object(simple_storage, 'SimpleStorage', autospec=True)
def test_get_members(self, SimpleStorage_mock):
members = self.simpl_stor_col.get_members()
calls = [
mock.call(self.simpl_stor_col._conn,
'/redfish/v1/Systems/437XR1138R2/SimpleStorage/1',
redfish_version=self.simpl_stor_col.redfish_version),
]
SimpleStorage_mock.assert_has_calls(calls)
self.assertIsInstance(members, list)
self.assertEqual(1, len(members))
def test_max_size_bytes(self):
self.assertIsNone(self.simpl_stor_col._max_size_bytes)
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/'
'simple_storage.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.assertEqual(8000000000000, self.simpl_stor_col.max_size_bytes)
# for any subsequent fetching it gets it from the cached value
self.conn.get.return_value.json.reset_mock()
self.assertEqual(8000000000000, self.simpl_stor_col.max_size_bytes)
self.conn.get.return_value.json.assert_not_called()
def test_max_size_bytes_after_refresh(self):
self.simpl_stor_col.refresh()
self.assertIsNone(self.simpl_stor_col._max_size_bytes)
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/'
'simple_storage.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.assertEqual(8000000000000, self.simpl_stor_col.max_size_bytes)

View File

@ -19,8 +19,8 @@ import mock
import sushy
from sushy import exceptions
from sushy.resources import constants as res_cons
from sushy.resources.system import bios
from sushy.resources.system import constants as sys_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import mappings as sys_map
from sushy.resources.system import processor
@ -58,9 +58,10 @@ class SystemTestCase(base.TestCase):
self.assertEqual('Physical', self.sys_inst.system_type)
self.assertEqual('38947555-7742-3448-3784-823347823834',
self.sys_inst.uuid)
self.assertEqual('Enabled', self.sys_inst.status.state)
self.assertEqual('OK', self.sys_inst.status.health)
self.assertEqual('OK', self.sys_inst.status.health_rollup)
self.assertEqual(res_cons.STATE_ENABLED, self.sys_inst.status.state)
self.assertEqual(res_cons.HEALTH_OK, self.sys_inst.status.health)
self.assertEqual(res_cons.HEALTH_OK,
self.sys_inst.status.health_rollup)
self.assertEqual(sushy.SYSTEM_POWER_STATE_ON,
self.sys_inst.power_state)
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
@ -374,9 +375,7 @@ class SystemTestCase(base.TestCase):
self.assertIsNone(self.sys_inst._ethernet_interfaces)
actual_macs = self.sys_inst.ethernet_interfaces.summary
expected_macs = (
{'12:44:6A:3B:04:11':
sys_map.HEALTH_STATE_VALUE_MAP_REV.get(
sys_cons.HEALTH_STATE_ENABLED)})
{'12:44:6A:3B:04:11': res_cons.STATE_ENABLED})
self.assertEqual(expected_macs, actual_macs)
self.assertIsInstance(self.sys_inst._ethernet_interfaces,
ethernet_interface.EthernetInterfaceCollection)