diff --git a/rsd_lib/resources/v2_3/node/node.py b/rsd_lib/resources/v2_3/node/node.py index 199094b..58db090 100644 --- a/rsd_lib/resources/v2_3/node/node.py +++ b/rsd_lib/resources/v2_3/node/node.py @@ -13,14 +13,22 @@ # License for the specific language governing permissions and limitations # under the License. +from jsonschema import validate +import logging + from sushy import exceptions from sushy.resources import base from rsd_lib.resources.v2_1.node import node as v2_1_node +from rsd_lib.resources.v2_2.node import node as v2_2_node from rsd_lib.resources.v2_3.node import attach_action_info +from rsd_lib.resources.v2_3.node import schemas as node_schemas from rsd_lib import utils as rsd_lib_utils +LOG = logging.getLogger(__name__) + + class AttachEndpointActionField(base.CompositeField): target_uri = base.Field('target', required=True) action_info_path = base.Field('@Redfish.ActionInfo', @@ -147,7 +155,7 @@ class Node(v2_1_node.Node): self._actions.detach_endpoint.action_info = None -class NodeCollection(v2_1_node.NodeCollection): +class NodeCollection(v2_2_node.NodeCollection): @property def _resource_type(self): @@ -163,3 +171,107 @@ class NodeCollection(v2_1_node.NodeCollection): the object according to schema of the given version. """ super(NodeCollection, self).__init__(connector, path, redfish_version) + + def _create_compose_request(self, name=None, description=None, + processor_req=None, memory_req=None, + remote_drive_req=None, local_drive_req=None, + ethernet_interface_req=None, + security_req=None, total_system_core_req=None, + total_system_memory_req=None): + + request = {} + + if name is not None: + request['Name'] = name + if description is not None: + request['Description'] = description + + if processor_req is not None: + validate(processor_req, + node_schemas.processor_req_schema) + request['Processors'] = processor_req + + if memory_req is not None: + validate(memory_req, + node_schemas.memory_req_schema) + request['Memory'] = memory_req + + if remote_drive_req is not None: + validate(remote_drive_req, + node_schemas.remote_drive_req_schema) + request['RemoteDrives'] = remote_drive_req + + if local_drive_req is not None: + validate(local_drive_req, + node_schemas.local_drive_req_schema) + request['LocalDrives'] = local_drive_req + + if ethernet_interface_req is not None: + validate(ethernet_interface_req, + node_schemas.ethernet_interface_req_schema) + request['EthernetInterfaces'] = ethernet_interface_req + + if security_req is not None: + validate(security_req, + node_schemas.security_req_schema) + request['Security'] = security_req + + if total_system_core_req is not None: + validate(total_system_core_req, + node_schemas.total_system_core_req_schema) + request['TotalSystemCoreCount'] = total_system_core_req + + if total_system_memory_req is not None: + validate(total_system_memory_req, + node_schemas.total_system_memory_req_schema) + request['TotalSystemMemoryMiB'] = total_system_memory_req + + return request + + def compose_node(self, name=None, description=None, + processor_req=None, memory_req=None, + remote_drive_req=None, local_drive_req=None, + ethernet_interface_req=None, security_req=None, + total_system_core_req=None, total_system_memory_req=None): + """Compose a node from RackScale hardware + + :param name: Name of node + :param description: Description of node + :param processor_req: JSON for node processors + :param memory_req: JSON for node memory modules + :param remote_drive_req: JSON for node remote drives + :param local_drive_req: JSON for node local drives + :param ethernet_interface_req: JSON for node ethernet ports + :param security_req: JSON for node security requirements + :param total_system_core_req: Total processor cores available in + composed node + :param total_system_memory_req: Total memory available in composed node + :returns: The location of the composed node + + When the 'processor_req' is not none: it need a computer system + contains processors whose each processor meet all conditions in the + value. + + When the 'total_system_core_req' is not none: it need a computer + system contains processors whose cores sum up to number equal or + greater than 'total_system_core_req'. + + When both values are not none: it need meet all conditions. + + 'memory_req' and 'total_system_memory_req' is the same. + """ + target_uri = self._get_compose_action_element().target_uri + properties = self._create_compose_request( + name=name, description=description, + processor_req=processor_req, + memory_req=memory_req, + remote_drive_req=remote_drive_req, + local_drive_req=local_drive_req, + ethernet_interface_req=ethernet_interface_req, + security_req=security_req, + total_system_core_req=total_system_core_req, + total_system_memory_req=total_system_memory_req) + resp = self._conn.post(target_uri, data=properties) + LOG.info("Node created at %s", resp.headers['Location']) + node_url = resp.headers['Location'] + return node_url[node_url.find(self._path):] diff --git a/rsd_lib/resources/v2_3/node/schemas.py b/rsd_lib/resources/v2_3/node/schemas.py new file mode 100644 index 0000000..ca989b4 --- /dev/null +++ b/rsd_lib/resources/v2_3/node/schemas.py @@ -0,0 +1,220 @@ +# Copyright (c) 2018 Intel, Corp. +# +# 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. + +processor_req_schema = { + 'type': 'array', + 'items': [{ + 'type': 'object', + 'properties': { + 'Model': {'type': 'string'}, + 'TotalCores': {'type': 'number'}, + 'AchievableSpeedMHz': {'type': 'number'}, + 'InstructionSet': { + 'type': 'string', + 'enum': ['x86', 'x86-64', 'IA-64', 'ARM-A32', + 'ARM-A64', 'MIPS32', 'MIPS64', 'OEM'] + }, + 'Oem': { + 'type': 'object', + 'properties': { + 'Brand': { + 'type': 'string', + 'enum': ['E3', 'E5', 'E7', 'X3', 'X5', 'X7', 'I3', + 'I5', 'I7', 'Silver', 'Gold', 'Platinum', + 'Unknown'] + }, + 'Capabilities': { + 'type': 'array', + 'items': [{'type': 'string'}] + } + } + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'Chassis': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'ProcessorType': { + 'type': 'string', + 'enum': ['CPU', 'FPGA', 'GPU', 'DSP', 'Accelerator', 'OEM'] + } + }, + 'additionalProperties': False, + }] +} + +memory_req_schema = { + 'type': 'array', + 'items': [{ + 'type': 'object', + 'properties': { + 'CapacityMiB': {'type': 'number'}, + 'MemoryDeviceType': { + 'type': 'string', + 'enum': ['DDR', 'DDR2', 'DDR3', 'DDR4', 'DDR4_SDRAM', + 'DDR4E_SDRAM', 'LPDDR4_SDRAM', 'DDR3_SDRAM', + 'LPDDR3_SDRAM', 'DDR2_SDRAM', 'DDR2_SDRAM_FB_DIMM', + 'DDR2_SDRAM_FB_DIMM_PROBE', 'DDR_SGRAM', + 'DDR_SDRAM', 'ROM', 'SDRAM', 'EDO', + 'FastPageMode', 'PipelinedNibble'] + }, + 'SpeedMHz': {'type': 'number'}, + 'Manufacturer': {'type': 'string'}, + 'DataWidthBits': {'type': 'number'}, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'Chassis': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + }] +} + +remote_drive_req_schema = { + 'type': 'array', + 'items': [{ + 'type': 'object', + 'properties': { + 'CapacityGiB': {'type': 'number'}, + 'Protocol': { + 'type': 'string', + 'enum': ['iSCSI', 'NVMeOverFabrics'] + }, + 'Master': { + 'type': 'object', + 'properties': { + 'Type': { + 'type': 'string', + 'enum': ['Snapshot', 'Clone'] + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + } + } + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + }] +} + +local_drive_req_schema = { + 'type': 'array', + 'items': [{ + 'type': 'object', + 'properties': { + 'CapacityGiB': {'type': 'number'}, + 'Type': { + 'type': 'string', + 'enum': ['HDD', 'SSD'] + }, + 'MinRPM': {'type': 'number'}, + 'SerialNumber': {'type': 'string'}, + 'Interface': { + 'type': 'string', + 'enum': ['SAS', 'SATA', 'NVMe'] + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'Chassis': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'FabricSwitch': {'type': 'boolean'} + }, + 'additionalProperties': False, + }] +} + +ethernet_interface_req_schema = { + 'type': 'array', + 'items': [{ + 'type': 'object', + 'properties': { + 'SpeedMbps': {'type': 'number'}, + 'PrimaryVLAN': {'type': 'number'}, + 'VLANs': { + 'type': 'array', + 'additionalItems': { + 'type': 'object', + 'properties': { + 'VLANId': {'type': 'number'}, + 'Tagged': {'type': 'boolean'} + } + } + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'Chassis': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + } + }, + 'additionalProperties': False, + }] +} + +security_req_schema = { + 'type': 'object', + 'properties': { + 'TpmPresent': {'type': 'boolean'}, + 'TpmInterfaceType': {'type': 'string'}, + 'TxtEnabled': {'type': 'boolean'}, + 'ClearTPMOnDelete': {'type': 'boolean'} + }, + 'additionalProperties': False, +} + +total_system_core_req_schema = { + 'type': 'number' +} + +total_system_memory_req_schema = { + 'type': 'number' +} diff --git a/rsd_lib/tests/unit/resources/v2_3/node/test_node.py b/rsd_lib/tests/unit/resources/v2_3/node/test_node.py index 74bab27..6b73c69 100644 --- a/rsd_lib/tests/unit/resources/v2_3/node/test_node.py +++ b/rsd_lib/tests/unit/resources/v2_3/node/test_node.py @@ -253,30 +253,275 @@ class NodeCollectionTestCase(testtools.TestCase): '/redfish/v1/Nodes/Actions/Allocate', data={}) self.assertEqual(result, '/redfish/v1/Nodes/1') - def test_compose_node_reqs(self): + def test_compose_node(self): reqs = { 'Name': 'test', 'Description': 'this is a test node', 'Processors': [{ - 'TotalCores': 4 + 'TotalCores': 4, + 'ProcessorType': 'FPGA', + 'Oem': { + 'Brand': 'Platinum', + 'Capabilities': ['sse'] + } }], 'Memory': [{ 'CapacityMiB': 8000 }], + 'RemoteDrives': [{ + 'CapacityGiB': 80, + 'Protocol': 'NVMeOverFabrics', + 'Master': { + 'Type': 'Snapshot', + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }, + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }], + 'Security': { + 'TpmPresent': True, + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': True, + 'ClearTPMOnDelete': True + }, 'TotalSystemCoreCount': 8, 'TotalSystemMemoryMiB': 16000 } result = self.node_col.compose_node( name='test', description='this is a test node', - processor_req=[{'TotalCores': 4}], + processor_req=[{ + 'TotalCores': 4, + 'ProcessorType': 'FPGA', + 'Oem': { + 'Brand': 'Platinum', + 'Capabilities': ['sse'] + } + }], memory_req=[{'CapacityMiB': 8000}], + remote_drive_req=[{ + 'CapacityGiB': 80, + 'Protocol': 'NVMeOverFabrics', + 'Master': { + 'Type': 'Snapshot', + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }, + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }], + security_req={ + 'TpmPresent': True, + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': True, + 'ClearTPMOnDelete': True + }, total_system_core_req=8, total_system_memory_req=16000) self.node_col._conn.post.assert_called_once_with( '/redfish/v1/Nodes/Actions/Allocate', data=reqs) self.assertEqual(result, '/redfish/v1/Nodes/1') - def test_compose_node_invalid_reqs(self): - self.assertRaises(jsonschema.exceptions.ValidationError, - self.node_col.compose_node, - processor_req='invalid') + def test_compose_node_with_invalid_reqs(self): + # Wrong processor type + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'invalid' is not one of \['CPU', 'FPGA', 'GPU', 'DSP', " + "'Accelerator', 'OEM'\]")): + + self.node_col.compose_node( + name='test', description='this is a test node', + processor_req=[{ + 'TotalCores': 4, + 'ProcessorType': 'invalid'}]) + + # Wrong processor Oem Brand + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'invalid' is not one of \['E3', 'E5'")): + + self.node_col.compose_node( + name='test', description='this is a test node', + processor_req=[{ + 'TotalCores': 4, + 'Oem': { + 'Brand': 'invalid', + 'Capabilities': ['sse'] + } + }]) + + # Wrong processor Oem Capabilities + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'sse' is not of type 'array'")): + + self.node_col.compose_node( + name='test', description='this is a test node', + processor_req=[{ + 'TotalCores': 4, + 'Oem': { + 'Brand': 'E3', + 'Capabilities': 'sse' + } + }]) + + # Wrong processor Oem Capabilities + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("0 is not of type 'string'")): + + self.node_col.compose_node( + name='test', description='this is a test node', + processor_req=[{ + 'TotalCores': 4, + 'Oem': { + 'Brand': 'E3', + 'Capabilities': [0] + } + }]) + + # Wrong remote drive CapacityGiB + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'invalid' is not of type 'number'")): + + self.node_col.compose_node( + name='test', description='this is a test node', + remote_drive_req=[{ + 'CapacityGiB': 'invalid', + 'Protocol': 'NVMeOverFabrics', + 'Master': { + 'Type': 'Snapshot', + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/' + '102' + } + }, + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }]) + + # Wrong remote drive Protocol + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'invalid' is not one of \['iSCSI', 'NVMeOverFabrics'\]")): + + self.node_col.compose_node( + name='test', description='this is a test node', + remote_drive_req=[{ + 'CapacityGiB': 80, + 'Protocol': 'invalid', + 'Master': { + 'Type': 'Snapshot', + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/' + '102' + } + }, + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }]) + + # Wrong remote drive Master Type + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("'invalid' is not one of \['Snapshot', 'Clone'\]")): + + self.node_col.compose_node( + name='test', description='this is a test node', + remote_drive_req=[{ + 'CapacityGiB': 80, + 'Protocol': 'iSCSI', + 'Master': { + 'Type': 'invalid', + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/' + '102' + } + }, + 'Resource': { + '@odata.id': + '/redfish/v1/StorageServices/NVMeoE1/Volumes/102' + } + }]) + + # Wrong security parameter "TpmPresent" + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + "'invalid' is not of type 'boolean'"): + self.node_col.compose_node( + name='test', description='this is a test node', + security_req={ + 'TpmPresent': 'invalid', + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': True, + 'ClearTPMOnDelete': True + }) + + # Wrong security parameter "TpmInterfaceType" + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + "True is not of type 'string'"): + self.node_col.compose_node( + name='test', description='this is a test node', + security_req={ + 'TpmPresent': False, + 'TpmInterfaceType': True, + 'TxtEnabled': True, + 'ClearTPMOnDelete': True + }) + + # Wrong security parameter "TxtEnabled" + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + "'invalid' is not of type 'boolean'"): + self.node_col.compose_node( + name='test', description='this is a test node', + security_req={ + 'TpmPresent': True, + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': 'invalid', + 'ClearTPMOnDelete': True + }) + + # Wrong security parameter "ClearTPMOnDelete" + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + "'invalid' is not of type 'boolean'"): + self.node_col.compose_node( + name='test', description='this is a test node', + security_req={ + 'TpmPresent': True, + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': True, + 'ClearTPMOnDelete': 'invalid' + }) + + # Wrong additional security parameter + with self.assertRaisesRegex( + jsonschema.exceptions.ValidationError, + ("Additional properties are not allowed \('invalid-key' was " + "unexpected\)")): + self.node_col.compose_node( + name='test', description='this is a test node', + security_req={ + 'TpmPresent': True, + 'TpmInterfaceType': 'TPM2_0', + 'TxtEnabled': False, + 'invalid-key': 'invalid-value' + })