Add node composition functionality
Add the ability to compose and delete nodes to the node collection class. Change-Id: I8eabb15ce0f164807ef8f08c3e67fb25b32fb2ac Implements-Blueprint: node-composition
This commit is contained in:
parent
834e77e6d6
commit
7bb501c0f1
@ -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']
|
||||
|
0
rsd_lib/tests/unit/fakes/__init__.py
Normal file
0
rsd_lib/tests/unit/fakes/__init__.py
Normal file
39
rsd_lib/tests/unit/fakes/request_fakes.py
Normal file
39
rsd_lib/tests/unit/fakes/request_fakes.py
Normal file
@ -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)
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user