diff --git a/requirements.txt b/requirements.txt index 60ded62..8e23ce6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ pbr>=2.0 # Apache-2.0 sushy>=0.1.0 # Apache-2.0 +validictory>=1.0.0 diff --git a/rsd_lib/resources/node/node.py b/rsd_lib/resources/node/node.py index 50f8d73..808de7e 100644 --- a/rsd_lib/resources/node/node.py +++ b/rsd_lib/resources/node/node.py @@ -14,6 +14,7 @@ # under the License. import logging +import validictory from sushy import exceptions from sushy.resources import base @@ -23,6 +24,7 @@ from sushy import utils from rsd_lib.resources.node import constants as node_cons from rsd_lib.resources.node import mappings as node_maps +from rsd_lib.resources.node import schemas as node_schemas LOG = logging.getLogger(__name__) @@ -389,13 +391,73 @@ class NodeCollection(base.ResourceCollectionBase): resource=self._path) return compose_action - def compose_node(self, properties={}): + 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): + + request = {} + + if name is not None: + request['Name'] = name + if description is not None: + request['Description'] = description + + if processor_req is not None: + validictory.validate(processor_req, + node_schemas.processor_req_schema, + required_by_default=False) + request['Processors'] = processor_req + + if memory_req is not None: + validictory.validate(memory_req, + node_schemas.memory_req_schema, + required_by_default=False) + request['Memory'] = memory_req + + if remote_drive_req is not None: + validictory.validate(remote_drive_req, + node_schemas.remote_drive_req_schema, + required_by_refault=False) + request['RemoteDrives'] = remote_drive_req + + if local_drive_req is not None: + validictory.validate(local_drive_req, + node_schemas.local_drive_req_schema, + required_by_default=False) + request['LocalDrives'] = local_drive_req + + if ethernet_interface_req is not None: + validictory.validate(ethernet_interface_req, + node_schemas.ethernet_interface_req_schema, + required_by_default=False) + request['EthernetInterfaces'] = ethernet_interface_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): """Compose a node from RackScale hardware - :param properties: The properties requested for node composition + :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 :returns: The location of the composed node """ 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) resp = self._conn.post(target_uri, data=properties) LOG.info("Node created at %s", resp.headers['Location']) node_url = resp.headers['Location'] diff --git a/rsd_lib/resources/node/schemas.py b/rsd_lib/resources/node/schemas.py new file mode 100644 index 0000000..fddb0a1 --- /dev/null +++ b/rsd_lib/resources/node/schemas.py @@ -0,0 +1,173 @@ +# Copyright (c) 2017 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'] + }, + 'Resource': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + }, + 'Chassis': { + 'type': 'object', + 'properties': { + '@odata.id': {'type': 'string'} + } + } + }, + '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'}, + 'iSCSIAddress': {'type': 'string'}, + 'Master': { + 'type': 'object', + 'properties': { + 'Type': { + 'type': 'string', + 'enum': ['Snapshot', 'Clone'] + }, + 'Address': { + '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, + }] +} diff --git a/rsd_lib/tests/unit/resources/node/test_node.py b/rsd_lib/tests/unit/resources/node/test_node.py index e787c25..20bf424 100644 --- a/rsd_lib/tests/unit/resources/node/test_node.py +++ b/rsd_lib/tests/unit/resources/node/test_node.py @@ -14,11 +14,12 @@ # under the License. import json - import mock +import testtools +import validictory + from sushy import exceptions from sushy.resources.system import system -import testtools from rsd_lib.resources.node import constants as node_cons from rsd_lib.resources.node import node @@ -392,24 +393,32 @@ class NodeCollectionTestCase(testtools.TestCase): self.assertEqual('/redfish/v1/Nodes/Actions/Allocate', value.target_uri) - def test_compose_node_no_properties(self): + def test_compose_node_no_reqs(self): result = self.node_col.compose_node() self.node_col._conn.post.assert_called_once_with( '/redfish/v1/Nodes/Actions/Allocate', data={}) self.assertEqual(result, '/redfish/v1/Nodes/1') - def test_compose_node_properties(self): - props = { + def test_compose_node_reqs(self): + reqs = { 'Name': 'test', 'Description': 'this is a test node', 'Processors': [{ - 'TotalCores': 2 + 'TotalCores': 4 }], 'Memory': [{ - 'CapacityMiB': 16000 + 'CapacityMiB': 8000 }] } - result = self.node_col.compose_node(properties=props) + result = self.node_col.compose_node( + name='test', description='this is a test node', + processor_req=[{'TotalCores': 4}], + memory_req=[{'CapacityMiB': 8000}]) self.node_col._conn.post.assert_called_once_with( - '/redfish/v1/Nodes/Actions/Allocate', data=props) + '/redfish/v1/Nodes/Actions/Allocate', data=reqs) self.assertEqual(result, '/redfish/v1/Nodes/1') + + def test_compose_node_invalid_reqs(self): + self.assertRaises(validictory.validator.FieldValidationError, + self.node_col.compose_node, + processor_req='invalid')