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:
parent
01569d5974
commit
6bc8f689b0
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Reset": {
|
||||
"type": "On"
|
||||
"Type": "On"
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"}})
|
||||
|
Loading…
Reference in New Issue
Block a user