diff --git a/rsd_lib/resources/node/node.py b/rsd_lib/resources/node/node.py index 0db9d64..03ef426 100644 --- a/rsd_lib/resources/node/node.py +++ b/rsd_lib/resources/node/node.py @@ -45,13 +45,21 @@ class DetachEndpointActionField(base.CompositeField): target_uri = base.Field('target', required=True) -class ActionsField(base.CompositeField): +class ComposeNodeActionField(base.CompositeField): + target_uri = base.Field('target', required=True) + + +class NodeActionsField(base.CompositeField): reset = common.ResetActionField('#ComposedNode.Reset') assemble = AssembleActionField('#ComposedNode.Assemble') attach_endpoint = AttachEndpointActionField('#ComposedNode.AttachEndpoint') detach_endpoint = DetachEndpointActionField('#ComposedNode.DetachEndpoint') +class NodeCollectionActionsField(base.CompositeField): + compose = ComposeNodeActionField('#ComposedNodeCollection.Allocate') + + class BootField(base.CompositeField): allowed_values = base.Field( 'BootSourceOverrideTarget@Redfish.AllowableValues', @@ -112,7 +120,7 @@ class Node(base.ResourceBase): _processors = None # ref to ProcessorCollection instance - _actions = ActionsField('Actions', required=True) + _actions = NodeActionsField('Actions', required=True) def __init__(self, connector, identity, redfish_version=None): """A class representing a ComposedNode @@ -248,6 +256,16 @@ class Node(base.ResourceBase): return self._processors + def delete_node(self): + """Delete (disassemble) the node. + + When this action is called several tasks are performed. A graceful + shutdown is sent to the computer system, all VLANs except reserved ones + are removed from associated ethernet switch ports, the computer system + is deallocated and the remote target is deallocated. + """ + self._conn.delete(self.path) + def refresh(self): super(Node, self).refresh() self._processors = None @@ -255,6 +273,8 @@ class Node(base.ResourceBase): class NodeCollection(base.ResourceCollectionBase): + _actions = NodeCollectionActionsField('Actions', required=True) + @property def _resource_type(self): return Node @@ -269,3 +289,22 @@ class NodeCollection(base.ResourceCollectionBase): """ super(NodeCollection, self).__init__(connector, path, redfish_version) + + def _get_compose_action_element(self): + compose_action = self._actions.compose + if not compose_action: + raise exceptions.MissingActionError( + action='#ComposedNodeCollection.Allocate', + resource=self._path) + return compose_action + + def compose_node(self, properties={}): + """Compose a node from RackScale hardware + + :param properties: The properties requested for node composition + :returns: The location of the composed node + """ + target_uri = self._get_compose_action_element().target_uri + resp = self._conn.post(target_uri, data=properties) + LOG.info("Node created at %s", resp.headers['Location']) + return resp.headers['Location'] diff --git a/rsd_lib/tests/unit/fakes/__init__.py b/rsd_lib/tests/unit/fakes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rsd_lib/tests/unit/fakes/request_fakes.py b/rsd_lib/tests/unit/fakes/request_fakes.py new file mode 100644 index 0000000..1019b1e --- /dev/null +++ b/rsd_lib/tests/unit/fakes/request_fakes.py @@ -0,0 +1,39 @@ +# Copyright 2017 Intel, 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. + + +def fake_request_get(json_data): + + class MockResponse(object): + def __init__(self, json_data): + self.json_data = json_data + + def json(self): + return self.json_data + + return MockResponse(json_data) + + +def fake_request_post(json_data, headers=None): + + class MockResponse(object): + def __init__(self, json_data, headers): + self.json_data = json_data + self.headers = headers + + def json(self): + return self.json_data + + return MockResponse(json_data, headers) diff --git a/rsd_lib/tests/unit/resources/node/test_node.py b/rsd_lib/tests/unit/resources/node/test_node.py index 01502c4..a54c5af 100644 --- a/rsd_lib/tests/unit/resources/node/test_node.py +++ b/rsd_lib/tests/unit/resources/node/test_node.py @@ -22,6 +22,7 @@ import testtools from rsd_lib.resources.node import constants as node_cons from rsd_lib.resources.node import node +from rsd_lib.tests.unit.fakes import request_fakes class NodeTestCase(testtools.TestCase): @@ -311,6 +312,10 @@ class NodeTestCase(testtools.TestCase): self.node_inst.processors.summary) self.conn.get.return_value.json.assert_not_called() + def test_delete_node(self): + self.node_inst.delete_node() + self.node_inst._conn.delete.assert_called_once() + class NodeCollectionTestCase(testtools.TestCase): @@ -319,7 +324,10 @@ class NodeCollectionTestCase(testtools.TestCase): self.conn = mock.Mock() with open('rsd_lib/tests/unit/json_samples/node_collection.json', 'r') as f: - self.conn.get.return_value.json.return_value = json.loads(f.read()) + self.conn.get.return_value = request_fakes.fake_request_get( + json.loads(f.read())) + self.conn.post.return_value = request_fakes.fake_request_post( + None, headers={"Location": "Test"}) self.node_col = node.NodeCollection( self.conn, '/redfish/v1/Nodes', redfish_version='1.0.2') @@ -345,3 +353,28 @@ class NodeCollectionTestCase(testtools.TestCase): redfish_version=self.node_col.redfish_version) self.assertIsInstance(members, list) self.assertEqual(1, len(members)) + + def test__get_compose_action_element(self): + value = self.node_col._get_compose_action_element() + self.assertEqual('/redfish/v1/Nodes/Actions/Allocate', + value.target_uri) + + def test_compose_node_no_properties(self): + self.node_col.compose_node() + self.node_col._conn.post.assert_called_once_with( + '/redfish/v1/Nodes/Actions/Allocate', data={}) + + def test_compose_node_properties(self): + props = { + 'Name': 'test', + 'Description': 'this is a test node', + 'Processors': [{ + 'TotalCores': 2 + }], + 'Memory': [{ + 'CapacityMiB': 16000 + }] + } + self.node_col.compose_node(properties=props) + self.node_col._conn.post.assert_called_once_with( + '/redfish/v1/Nodes/Actions/Allocate', data=props)