Support post action to composed node

Added api entry v1/node/<uuid>/action to allow user to post reset
action to composed node. The available reset type contains
Power On/Off, Force Shutdown/Restart, etc, which may vary because of
different podm.

Partially-Implements blueprint node-action
Closes-Bug: #1659932

Change-Id: Idb23bdde02318c70e046e92dd1d474cba54c645e
This commit is contained in:
Lin Yang 2017-02-01 18:17:29 -08:00
parent 01569d5974
commit 6bc8f689b0
7 changed files with 199 additions and 1 deletions

View File

@ -1,5 +1,5 @@
{
"Reset": {
"type": "On"
"Type": "On"
}
}

View File

@ -70,6 +70,9 @@ api.add_resource(v1_nodes.Nodes, '/v1/nodes', endpoint='nodes')
api.add_resource(v1_nodes.Node,
'/v1/nodes/<string:node_uuid>',
endpoint='node')
api.add_resource(v1_nodes.NodeAction,
'/v1/nodes/<string:node_uuid>/action',
endpoint='node_action')
api.add_resource(v1_nodes.NodesStorage,
'/v1/nodes/<string:nodeid>/storages',
endpoint='nodes_storages')

View File

@ -44,6 +44,14 @@ class Node(Resource):
http_client.OK, nodes.Node.delete_composed_node(node_uuid))
class NodeAction(Resource):
def post(self, node_uuid):
return utils.make_response(
http_client.OK,
nodes.Node.node_action(node_uuid, request.get_json()))
class NodesStorage(Resource):
def get(self, nodeid):

View File

@ -97,3 +97,28 @@ class Node(object):
"""
return [cls._show_node_brief_info(node_info.as_dict())
for node_info in db_api.Connection.list_composed_nodes()]
@classmethod
def node_action(cls, node_uuid, request_body):
"""Post action to a composed node
param node_uuid: uuid of composed node
param request_body: parameter of node action
return: message of this deletion
"""
# Get node detail from db, and map node uuid to index
index = db_api.Connection.get_composed_node_by_uuid(node_uuid).index
# TODO(lin.yang): should validate request body whether follow specifc
# format, like
# {
# "Reset": {
# "Type": "On"
# }
# }
# Should rework this part after basic validation framework for api
# input is done.
# https://review.openstack.org/#/c/422547/
return redfish.node_action(index, request_body)

View File

@ -529,3 +529,72 @@ def list_nodes():
nodes.append(get_node_by_id(node_index, show_detail=False))
return nodes
def reset_node(nodeid, request):
nodes_url = get_base_resource_url("Nodes")
node_url = os.path.normpath("/".join([nodes_url, nodeid]))
resp = send_request(node_url)
if resp.status_code != http_client.OK:
# Raise exception if don't find node
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
node = resp.json()
action_type = request.get("Reset", {}).get("Type")
allowable_actions = node["Actions"]["#ComposedNode.Reset"][
"ResetType@DMTF.AllowableValues"]
if not action_type:
raise exception.BadRequest(
detail="The content of node action request is malformed. Please "
"refer to Valence api specification to correct it.")
if allowable_actions and action_type not in allowable_actions:
raise exception.BadRequest(
detail="Action type '{0}' is not in allowable action list "
"{1}.".format(action_type, allowable_actions))
target_url = node["Actions"]["#ComposedNode.Reset"]["target"]
action_resp = send_request(target_url, 'POST',
headers={'Content-type': 'application/json'},
json={"ResetType": action_type})
if action_resp.status_code != http_client.NO_CONTENT:
raise exception.RedfishException(action_resp.json(),
status_code=action_resp.status_code)
else:
# Reset node successfully
LOG.debug("Post action '{0}' to node {1} successfully."
.format(action_type, target_url))
return exception.confirmation(
confirm_code="Reset Composed Node",
confirm_detail="This composed node has been set to '{0}' "
"successfully.".format(action_type))
def node_action(nodeid, request):
# Only support one action in single request
if len(list(request.keys())) != 1:
raise exception.BadRequest(
detail="No action found or multiple actions in one single request."
" Please refer to Valence api specification to correct the"
" content of node action request.")
action = list(request.keys())[0]
# Podm support two kinds of action for composed node, assemble and reset.
# Because valence assemble node by default when compose node, so only need
# to support "Reset" action here. In case podm new version support more
# actions, use "functions" dict to drive the workflow.
functions = {"Reset": reset_node}
if action not in functions:
raise exception.BadRequest(
detail="This node action '{0}' is unsupported. Please refer to "
"Valence api specification to correct this content of node "
"action request.".format(action))
return functions[action](nodeid, request)

View File

@ -115,3 +115,16 @@ class TestAPINodes(unittest.TestCase):
result = nodes.Node.list_composed_nodes()
self.assertEqual(expected, result)
@mock.patch("valence.redfish.redfish.node_action")
@mock.patch("valence.db.api.Connection.get_composed_node_by_uuid")
def test_node_action(
self, mock_db_get_composed_node, mock_node_action):
"""Test reset composed node status"""
action = {"Reset": {"Type": "On"}}
mock_db_model = mock.MagicMock()
mock_db_model.index = "1"
mock_db_get_composed_node.return_value = mock_db_model
nodes.Node.node_action("fake_uuid", action)
mock_node_action.assert_called_once_with("1", action)

View File

@ -422,3 +422,83 @@ class TestRedfish(TestCase):
mock_get_node_by_id.assert_called_with("1", show_detail=False)
self.assertEqual(["node1_detail"], result)
@mock.patch('valence.redfish.redfish.send_request')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_reset_node_malformed_request(self, mock_get_url, mock_request):
"""Test reset node with malformed request content"""
mock_get_url.return_value = '/redfish/v1/Nodes'
mock_request.return_value = fakes.mock_request_get(
fakes.fake_node_detail(), http_client.OK)
with self.assertRaises(exception.BadRequest) as context:
redfish.reset_node("1", {"fake_request": "fake_value"})
self.assertTrue("The content of node action request is malformed. "
"Please refer to Valence api specification to correct "
"it." in str(context.exception.detail))
@mock.patch('valence.redfish.redfish.send_request')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_reset_node_wrong_request(self, mock_get_url, mock_request):
"""Test reset node with wrong action type"""
mock_get_url.return_value = '/redfish/v1/Nodes'
mock_request.return_value = fakes.mock_request_get(
fakes.fake_node_detail(), http_client.OK)
with self.assertRaises(exception.BadRequest) as context:
redfish.reset_node("1", {"Reset": {"Type": "wrong_action"}})
self.assertTrue("Action type 'wrong_action' is not in allowable action"
" list" in str(context.exception.detail))
@mock.patch('valence.redfish.redfish.send_request')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_reset_node_success(self, mock_get_url, mock_request):
"""Test successfully reset node status"""
mock_get_url.return_value = '/redfish/v1/Nodes'
fake_node_detail = fakes.mock_request_get(
fakes.fake_node_detail(), http_client.OK)
fake_node_action_resp = fakes.mock_request_get(
{}, http_client.NO_CONTENT)
mock_request.side_effect = [fake_node_detail, fake_node_action_resp]
result = redfish.reset_node("1", {"Reset": {"Type": "On"}})
expected = exception.confirmation(
confirm_code="Reset Composed Node",
confirm_detail="This composed node has been set to 'On' "
"successfully.")
self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.reset_node')
def test_node_action_malformed_request(self, mock_reset_node):
"""Test post node_action with malformed request"""
# Unsupported multiple action
with self.assertRaises(exception.BadRequest) as context:
redfish.node_action(
"1", {"Reset": {"Type": "On"}, "Assemble": {}})
self.assertTrue("No action found or multiple actions in one single "
"request. Please refer to Valence api specification "
"to correct the content of node action request."
in str(context.exception.detail))
mock_reset_node.assert_not_called()
# Unsupported action
with self.assertRaises(exception.BadRequest) as context:
redfish.node_action(
"1", {"Assemble": {}})
self.assertTrue("This node action 'Assemble' is unsupported. Please "
"refer to Valence api specification to correct this "
"content of node action request."
in str(context.exception.detail))
mock_reset_node.assert_not_called()
@mock.patch('valence.redfish.redfish.reset_node')
def test_node_action_success(self, mock_reset_node):
"""Test post node_action success"""
redfish.node_action("1", {"Reset": {"Type": "On"}})
mock_reset_node.assert_called_once_with("1", {"Reset": {"Type": "On"}})