Add System Processor/Memory info

This patch adds 2 new attributes to System:

  memory_summary: <type: namedtuple>
    The 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
      `health`: The overall health state of memory.

  processor_summary: <type: namedtuple>
    The processors of the system in general detail. It is a
    namedtuple containing the following:

      `count`: The number of processors in the system. To be
               precise, it is number of logical CPUs (threads).
      `architecture`: The architecture of the processor/s.

This processor and memory information will be directly consumed
by ironic inspection.

Change-Id: I19acd74e26a861147df747f3d3f34852decf5403
This commit is contained in:
Debayan Ray 2017-03-16 09:53:20 -04:00
parent 3a6e665962
commit 18febd060f
11 changed files with 572 additions and 3 deletions

View File

@ -72,6 +72,12 @@ To use sushy in a project:
# Get a list of allowed boot source target values
print(sys_inst.get_allowed_system_boot_source_values())
# Get the memory summary
print(sys_inst.memory_summary)
# Get the processor summary
print(sys_inst.processors.summary)
If you do not have any real baremetal machine that supports the Redfish
protocol you can look at the :ref:`contributing` page to learn how to

View File

@ -96,3 +96,15 @@ BOOT_SOURCE_MODE_UEFI = 'uefi'
BOOT_SOURCE_ENABLED_ONCE = 'once'
BOOT_SOURCE_ENABLED_CONTINUOUS = 'continuous'
BOOT_SOURCE_ENABLED_DISABLED = 'disabled'
# Processor related constants
# Values comes from the Redfish Processor json-schema 1.0.0:
# http://redfish.dmtf.org/schemas/v1/Processor.v1_0_0.json
# Processor Architecture constants
PROCESSOR_ARCH_x86 = 'x86 or x86-64'
PROCESSOR_ARCH_IA_64 = 'Intel Itanium'
PROCESSOR_ARCH_ARM = 'ARM'
PROCESSOR_ARCH_MIPS = 'MIPS'
PROCESSOR_ARCH_OEM = 'OEM-defined'

View File

@ -71,3 +71,14 @@ BOOT_SOURCE_ENABLED_MAP = {
}
BOOT_SOURCE_ENABLED_MAP_REV = utils.revert_dictionary(BOOT_SOURCE_ENABLED_MAP)
PROCESSOR_ARCH_VALUE_MAP = {
'x86': sys_cons.PROCESSOR_ARCH_x86,
'IA-64': sys_cons.PROCESSOR_ARCH_IA_64,
'ARM': sys_cons.PROCESSOR_ARCH_ARM,
'MIPS': sys_cons.PROCESSOR_ARCH_MIPS,
'OEM': sys_cons.PROCESSOR_ARCH_OEM,
}
PROCESSOR_ARCH_VALUE_MAP_REV = (
utils.revert_dictionary(PROCESSOR_ARCH_VALUE_MAP))

View File

@ -0,0 +1,144 @@
# 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.
import collections
import logging
from sushy.resources import base
from sushy.resources.system import mappings as sys_maps
# Representation of Summary of Processor information
ProcessorSummary = collections.namedtuple('ProcessorSummary',
['count', 'architecture'])
LOG = logging.getLogger(__name__)
class Processor(base.ResourceBase):
identity = None
"""The processor identity string"""
socket = None
"""The socket or location of the processor"""
# TODO(deray): Create mappings for the processor_type
processor_type = None
"""The type of processor"""
processor_architecture = None
"""The architecture of the processor"""
# TODO(deray): Create mappings for the instruction_set
instruction_set = None
"""The instruction set of the processor"""
manufacturer = None
"""The processor manufacturer"""
model = None
"""The product model number of this device"""
max_speed_mhz = None
"""The maximum clock speed of the processor in MHz."""
total_cores = None
"""The total number of cores contained in this processor"""
total_threads = None
"""The total number of execution threads supported by this processor"""
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a Processor
:param connector: A Connector instance
:param identity: The identity of the processor
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of the given 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):
@property
def _resource_type(self):
return Processor
_summary = None
"""The summary of processors of the system in general detail
This has to be accessed by exposed :prop:`summary`.
"""
@property
def summary(self):
"""Property to provide ProcessorSummary info
It is calculated once when the first time it is queried. On refresh,
this property gets reset.
:returns: A namedtuple containing the following:
count: The number of processors in the system. To be precise,
it returns the number of logical CPUs (threads).
architecture: The architecture of the processor/s.
"""
if self._summary is None:
count, architecture = 0, None
for proc in self.get_members():
# Note(deray): It attempts to detect the number of CPU cores.
# It returns the number of logical CPUs.
if proc.total_threads is not None:
count += proc.total_threads
# Note(deray): Bail out of checking the architecture info
# if you have already got hold of any one of the processors'
# architecture information.
if (architecture is None
and proc.processor_architecture is not None):
architecture = proc.processor_architecture
self._summary = ProcessorSummary(count=count,
architecture=architecture)
return self._summary
def __init__(self, connector, path, redfish_version=None):
"""A class representing a ProcessorCollection
:param connector: A Connector instance
:param path: The canonical path to the Processor collection resource
:param redfish_version: The version of RedFish. Used to construct
the object according to schema of the given version.
"""
super(ProcessorCollection, self).__init__(connector, path,
redfish_version)
def refresh(self):
"""Refresh the resource"""
super(ProcessorCollection, self).refresh()
# Reset summary attribute
self._summary = None

View File

@ -13,13 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import logging
from sushy import exceptions
from sushy.resources import base
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__)
@ -72,6 +77,19 @@ class System(base.ResourceBase):
uuid = None
"""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.
"""
_processors = None # ref to ProcessorCollection instance
def __init__(self, connector, identity, redfish_version=None):
"""A class representing a ComputerSystem
@ -110,6 +128,21 @@ class System(base.ResourceBase):
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:
@ -245,6 +278,28 @@ class System(base.ResourceBase):
# include a get_manager() and get_chassis() once we have an abstraction
# for those resources.
def _get_processor_collection_path(self):
"""Helper function to find the ProcessorCollection path"""
processor_col = self.json.get('Processors')
if not processor_col:
raise exceptions.MissingAttributeError(attribute='Processors',
resource=self._path)
return processor_col.get('@odata.id')
@property
def processors(self):
"""Property to provide reference to `ProcessorCollection` instance
It is calculated once when the first time it is queried. On refresh,
this property gets reset.
"""
if self._processors is None:
self._processors = processor.ProcessorCollection(
self._conn, self._get_processor_collection_path(),
redfish_version=self.redfish_version)
return self._processors
class SystemCollection(base.ResourceCollectionBase):

View File

@ -0,0 +1,28 @@
{
"@odata.type": "#Processor.v1_0_2.Processor",
"Id": "CPU1",
"Socket": "CPU 1",
"ProcessorType": "CPU",
"ProcessorArchitecture": "x86",
"InstructionSet": "x86-64",
"Manufacturer": "Intel(R) Corporation",
"Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series",
"ProcessorID": {
"VendorID": "GenuineIntel",
"IdentificationRegisters": "0x34AC34DC8901274A",
"EffectiveFamily": "0x42",
"EffectiveModel": "0x61",
"Step": "0x1",
"MicrocodeInfo": "0x429943"
},
"MaxSpeedMHz": 3700,
"TotalCores": 8,
"TotalThreads": 16,
"Status": {
"State": "Enabled",
"Health": "OK"
},
"@odata.context": "/redfish/v1/$metadata#Systems/Members/437XR1138R2/Processors/Members/$entity",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1",
"@Redfish.Copyright": "Copyright 2014-2016 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,12 @@
{
"@odata.type": "#Processor.v1_0_2.Processor",
"Id": "CPU2",
"Socket": "CPU 2",
"ProcessorType": "CPU",
"Status": {
"State": "Absent"
},
"@odata.context": "/redfish/v1/$metadata#Systems/Members/437XR1138R2/Processors/Members/$entity",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2",
"@Redfish.Copyright": "Copyright 2014-2016 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,16 @@
{
"@odata.type": "#ProcssorCollection.ProcessorCollection",
"Name": "Processors Collection",
"Members@odata.count": 2,
"Members": [
{
"@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1"
},
{
"@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2"
}
],
"@odata.context": "/redfish/v1/$metadata#Systems/Links/Members/437XR1138R2/Processors/#entity",
"@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors",
"@Redfish.Copyright": "Copyright 2014-2016 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -15,7 +15,7 @@
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollUp": "OK"
"HealthRollup": "OK"
},
"IndicatorLED": "Off",
"PowerState": "On",
@ -67,7 +67,7 @@
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollUp": "OK"
"HealthRollup": "OK"
}
},
"MemorySummary": {
@ -75,7 +75,7 @@
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollUp": "OK"
"HealthRollup": "OK"
}
},
"Bios": {

View File

@ -0,0 +1,152 @@
# 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.
import json
import mock
import sushy
from sushy.resources.system import processor
from sushy.tests.unit import base
class ProcessorTestCase(base.TestCase):
def setUp(self):
super(ProcessorTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/processor.json', 'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
self.sys_processor = processor.Processor(
self.conn, '/redfish/v1/Systems/437XR1138R2/Processors/CPU1',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.sys_processor._parse_attributes()
self.assertEqual('1.0.2', self.sys_processor.redfish_version)
self.assertEqual('CPU1', self.sys_processor.identity)
self.assertEqual('CPU 1', self.sys_processor.socket)
self.assertEqual('CPU', self.sys_processor.processor_type)
self.assertEqual(sushy.PROCESSOR_ARCH_x86,
self.sys_processor.processor_architecture)
self.assertEqual('x86-64', self.sys_processor.instruction_set)
self.assertEqual('Intel(R) Corporation',
self.sys_processor.manufacturer)
self.assertEqual('Multi-Core Intel(R) Xeon(R) processor 7xxx Series',
self.sys_processor.model)
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)
class ProcessorCollectionTestCase(base.TestCase):
def setUp(self):
super(ProcessorCollectionTestCase, self).setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'processor_collection.json', 'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
self.sys_processor_col = processor.ProcessorCollection(
self.conn, '/redfish/v1/Systems/437XR1138R2/Processors',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.sys_processor_col._parse_attributes()
self.assertEqual('1.0.2', self.sys_processor_col.redfish_version)
self.assertEqual('Processors Collection', self.sys_processor_col.name)
self.assertEqual(('/redfish/v1/Systems/437XR1138R2/Processors/CPU1',
'/redfish/v1/Systems/437XR1138R2/Processors/CPU2'),
self.sys_processor_col.members_identities)
@mock.patch.object(processor, 'Processor', autospec=True)
def test_get_member(self, mock_processor):
self.sys_processor_col.get_member(
'/redfish/v1/Systems/437XR1138R2/Processors/CPU1')
mock_processor.assert_called_once_with(
self.sys_processor_col._conn,
'/redfish/v1/Systems/437XR1138R2/Processors/CPU1',
redfish_version=self.sys_processor_col.redfish_version)
@mock.patch.object(processor, 'Processor', autospec=True)
def test_get_members(self, mock_processor):
members = self.sys_processor_col.get_members()
calls = [
mock.call(self.sys_processor_col._conn,
'/redfish/v1/Systems/437XR1138R2/Processors/CPU1',
redfish_version=self.sys_processor_col.redfish_version),
mock.call(self.sys_processor_col._conn,
'/redfish/v1/Systems/437XR1138R2/Processors/CPU2',
redfish_version=self.sys_processor_col.redfish_version)
]
mock_processor.assert_has_calls(calls)
self.assertIsInstance(members, list)
self.assertEqual(2, len(members))
def _setUp_processor_summary(self):
self.conn.get.return_value.json.reset_mock()
successive_return_values = []
with open('sushy/tests/unit/json_samples/processor.json', 'r') as f:
successive_return_values.append(json.loads(f.read()))
with open('sushy/tests/unit/json_samples/processor2.json', 'r') as f:
successive_return_values.append(json.loads(f.read()))
self.conn.get.return_value.json.side_effect = successive_return_values
def test_summary(self):
# check for the underneath variable value
self.assertIsNone(self.sys_processor_col._summary)
# | GIVEN |
self._setUp_processor_summary()
# | WHEN |
actual_summary = self.sys_processor_col.summary
# | THEN |
self.assertEqual((16, sushy.PROCESSOR_ARCH_x86),
actual_summary)
self.assertEqual(16, actual_summary.count)
self.assertEqual(sushy.PROCESSOR_ARCH_x86,
actual_summary.architecture)
# reset mock
self.conn.get.return_value.json.reset_mock()
# | WHEN & THEN |
# tests for same object on invoking subsequently
self.assertIs(actual_summary,
self.sys_processor_col.summary)
self.conn.get.return_value.json.assert_not_called()
def test_summary_on_refresh(self):
# | GIVEN |
self._setUp_processor_summary()
# | WHEN & THEN |
self.assertEqual((16, sushy.PROCESSOR_ARCH_x86),
self.sys_processor_col.summary)
self.conn.get.return_value.json.side_effect = None
# On refreshing the sys_processor_col instance...
with open('sushy/tests/unit/json_samples/'
'processor_collection.json', 'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
self.sys_processor_col.refresh()
# | WHEN & THEN |
self.assertIsNone(self.sys_processor_col._summary)
# | GIVEN |
self._setUp_processor_summary()
# | WHEN & THEN |
self.assertEqual((16, sushy.PROCESSOR_ARCH_x86),
self.sys_processor_col.summary)

View File

@ -19,6 +19,7 @@ import mock
import sushy
from sushy import exceptions
from sushy.resources.system import processor
from sushy.resources.system import system
from sushy.tests.unit import base
@ -54,6 +55,9 @@ 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.assertIsNone(self.sys_inst._processors)
def test_get__reset_action_element(self):
value = self.sys_inst._get_reset_action_element()
@ -218,6 +222,135 @@ class SystemTestCase(base.TestCase):
sushy.BOOT_SOURCE_TARGET_HDD,
enabled='invalid-enabled')
def test__get_processor_collection_path_missing_processors_attr(self):
self.sys_inst._json.pop('Processors')
self.assertRaisesRegex(
exceptions.MissingAttributeError, 'attribute Processors',
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 |
self.sys_inst._parse_attributes()
# | THEN |
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
self.assertEqual(None, self.sys_inst.memory_summary.health)
# | GIVEN |
self.sys_inst._json['MemorySummary'].pop('Status')
# | WHEN |
self.sys_inst._parse_attributes()
# | THEN |
self.assertEqual(96, self.sys_inst.memory_summary.size_gib)
self.assertEqual(None, self.sys_inst.memory_summary.health)
# | GIVEN |
self.sys_inst._json['MemorySummary'].pop('TotalSystemMemoryGiB')
# | WHEN |
self.sys_inst._parse_attributes()
# | THEN |
self.assertEqual(None, self.sys_inst.memory_summary.size_gib)
self.assertEqual(None, self.sys_inst.memory_summary.health)
# | GIVEN |
self.sys_inst._json.pop('MemorySummary')
# | WHEN |
self.sys_inst._parse_attributes()
# | THEN |
self.assertEqual(None, self.sys_inst.memory_summary)
def test_processors(self):
# check for the underneath variable value
self.assertIsNone(self.sys_inst._processors)
# | GIVEN |
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/processor_collection.json',
'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
# | WHEN |
actual_processors = self.sys_inst.processors
# | THEN |
self.assertIsInstance(actual_processors,
processor.ProcessorCollection)
self.conn.get.return_value.json.assert_called_once_with()
# reset mock
self.conn.get.return_value.json.reset_mock()
# | WHEN & THEN |
# tests for same object on invoking subsequently
self.assertIs(actual_processors,
self.sys_inst.processors)
self.conn.get.return_value.json.assert_not_called()
def test_processors_on_refresh(self):
# | GIVEN |
with open('sushy/tests/unit/json_samples/processor_collection.json',
'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
# | WHEN & THEN |
self.assertIsInstance(self.sys_inst.processors,
processor.ProcessorCollection)
# On refreshing the system instance...
with open('sushy/tests/unit/json_samples/system.json', 'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
self.sys_inst.refresh()
# | WHEN & THEN |
self.assertIsNone(self.sys_inst._processors)
# | GIVEN |
with open('sushy/tests/unit/json_samples/processor_collection.json',
'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
# | WHEN & THEN |
self.assertIsInstance(self.sys_inst.processors,
processor.ProcessorCollection)
def _setUp_processor_summary(self):
self.conn.get.return_value.json.reset_mock()
with open('sushy/tests/unit/json_samples/processor_collection.json',
'r') as f:
self.conn.get.return_value.json.return_value = json.loads(f.read())
# fetch processors for the first time
self.sys_inst.processors
successive_return_values = []
with open('sushy/tests/unit/json_samples/processor.json', 'r') as f:
successive_return_values.append(json.loads(f.read()))
with open('sushy/tests/unit/json_samples/processor2.json', 'r') as f:
successive_return_values.append(json.loads(f.read()))
self.conn.get.return_value.json.side_effect = successive_return_values
def test_processor_summary(self):
# | GIVEN |
self._setUp_processor_summary()
# | WHEN |
actual_processor_summary = self.sys_inst.processors.summary
# | THEN |
self.assertEqual((16, sushy.PROCESSOR_ARCH_x86),
actual_processor_summary)
self.assertEqual(16, actual_processor_summary.count)
self.assertEqual(sushy.PROCESSOR_ARCH_x86,
actual_processor_summary.architecture)
# reset mock
self.conn.get.return_value.json.reset_mock()
# | WHEN & THEN |
# tests for same object on invoking subsequently
self.assertIs(actual_processor_summary,
self.sys_inst.processors.summary)
self.conn.get.return_value.json.assert_not_called()
class SystemCollectionTestCase(base.TestCase):