diff --git a/api-ref/source/mockup/node-post-action-request.json b/api-ref/source/mockup/node-post-action-request.json index 674cbb6..ff9a539 100644 --- a/api-ref/source/mockup/node-post-action-request.json +++ b/api-ref/source/mockup/node-post-action-request.json @@ -1,5 +1,5 @@ { "Reset": { - "type": "On" + "Type": "On" } } diff --git a/valence/api/route.py b/valence/api/route.py index 57a633e..587b0c6 100644 --- a/valence/api/route.py +++ b/valence/api/route.py @@ -70,6 +70,9 @@ api.add_resource(v1_nodes.Nodes, '/v1/nodes', endpoint='nodes') api.add_resource(v1_nodes.Node, '/v1/nodes/', endpoint='node') +api.add_resource(v1_nodes.NodeAction, + '/v1/nodes//action', + endpoint='node_action') api.add_resource(v1_nodes.NodesStorage, '/v1/nodes//storages', endpoint='nodes_storages') diff --git a/valence/api/v1/nodes.py b/valence/api/v1/nodes.py index dd93e3e..79bdb4a 100644 --- a/valence/api/v1/nodes.py +++ b/valence/api/v1/nodes.py @@ -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): diff --git a/valence/controller/nodes.py b/valence/controller/nodes.py index f075c2a..9754718 100644 --- a/valence/controller/nodes.py +++ b/valence/controller/nodes.py @@ -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) diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index fb42c64..f1fcb97 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -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) diff --git a/valence/tests/unit/controller/test_nodes.py b/valence/tests/unit/controller/test_nodes.py index bc488f3..2b7ec56 100644 --- a/valence/tests/unit/controller/test_nodes.py +++ b/valence/tests/unit/controller/test_nodes.py @@ -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) diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index 2c94ab6..aae0f2b 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -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"}})