Support set boot source of composed node

This patch allow user to set boot source of composed node. Two
parameters are needed in request body,
"Target" - the boot source to be used at next boot
"Enabled" - options "Once" and "Continuous" are supported, which
indicate whether this action is only one time change

Partially-Implements blueprint node-action

Change-Id: I46342c884e3cb8f00069e7e2c10c878582a2b185
This commit is contained in:
Lin Yang 2017-02-13 16:26:45 -08:00
parent 6bc8f689b0
commit be919e10a5
6 changed files with 157 additions and 9 deletions

View File

@ -0,0 +1,6 @@
{
"Boot": {
"Enabled": "Once",
"Target": "Pxe"
}
}

View File

@ -111,10 +111,17 @@ node_attach_type:
in: body in: body
required: true required: true
type: string type: string
node_boot_enabled:
description: |
The value of this describe the state of boot source override feature,
including "Once", "Continuous".
in: body
required: true
type: string
node_boot_source: node_boot_source:
description: | description: |
The value of this contain the source to boot the composed node, including The value of this contain the source to boot the composed node, including
"pxe", "hdd", "none" "Pxe", "Hdd", "None".
in: body in: body
required: true required: true
type: string type: string
@ -168,7 +175,7 @@ node_power_state:
"On", "Off", "PoweringOn" ,"PoweringOff" "On", "Off", "PoweringOn" ,"PoweringOff"
in: body in: body
required: true required: true
type: string type: string
node_processor_asset: node_processor_asset:
description: | description: |
Processor asset (Object) info. Processor asset (Object) info.
@ -208,11 +215,11 @@ node_reset_type:
type: string type: string
node_state: node_state:
description: | description: |
The current composed node state including The current composed node state including
"Assembling", "Allocating", "Assembling" ,"Failed", "Assembled" "Assembling", "Allocating", "Assembling" ,"Failed", "Assembled"
in: body in: body
required: true required: true
type: string type: string
node_storage_asset: node_storage_asset:
description: | description: |
Storage asset (Object) info. Storage asset (Object) info.

View File

@ -212,12 +212,12 @@ Request
- node_ident: node_ident - node_ident: node_ident
Node reset Node action
=========== ===========
.. rest_method:: POST /v1/nodes/{node_ident}/action .. rest_method:: POST /v1/nodes/{node_ident}/action
Send a POST (reset) cmd to a composed node. Send a POST cmd to a composed node, includes reset node state and set boot source.
Normal response codes: 204 Normal response codes: 204
@ -231,10 +231,17 @@ Request
- node_ident: node_ident - node_ident: node_ident
- reset_type: node_reset_type - reset_type: node_reset_type
- boot_enabled: node_boot_enabled
- boot_source: node_boot_source
**Example POST action cmd for composed node :** **Example reset state for composed node :**
.. literalinclude:: mockup/node-post-action-request.json .. literalinclude:: mockup/node-reset-state-request.json
:language: javascript
**Example set boot source for composed node :**
.. literalinclude:: mockup/node-set-boot-source-request.json
:language: javascript :language: javascript
Response Response

View File

@ -575,6 +575,59 @@ def reset_node(nodeid, request):
"successfully.".format(action_type)) "successfully.".format(action_type))
def set_boot_source(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()
boot_enabled = request.get("Boot", {}).get("Enabled")
boot_target = request.get("Boot", {}).get("Target")
allowable_boot_target = \
node["Boot"]["BootSourceOverrideTarget@Redfish.AllowableValues"]
if not boot_enabled or not boot_target:
raise exception.BadRequest(
detail="The content of set boot source request is malformed. "
"Please refer to Valence api specification to correct it.")
if boot_enabled not in ["Disabled", "Once", "Continuous"]:
raise exception.BadRequest(
detail="The parameter Enabled '{0}' is not in allowable list "
"['Disabled', 'Once', 'Continuous'].".format(
boot_enabled))
if allowable_boot_target and \
boot_target not in allowable_boot_target:
raise exception.BadRequest(
detail="The parameter Target '{0}' is not in allowable list "
"{1}.".format(boot_target,
allowable_boot_target))
action_resp = send_request(
node_url, 'PATCH', headers={'Content-type': 'application/json'},
json={"Boot": {"BootSourceOverrideEnabled": boot_enabled,
"BootSourceOverrideTarget": boot_target}})
if action_resp.status_code != http_client.NO_CONTENT:
raise exception.RedfishException(action_resp.json(),
status_code=action_resp.status_code)
else:
# Set boot source successfully
LOG.debug("Set boot source of composed node {0} to '{1}' with enabled "
"state '{2}' successfully."
.format(nodes_url, boot_target, boot_enabled))
return exception.confirmation(
confirm_code="Set Boot Source of Composed Node",
confirm_detail="The boot source of composed node has been set to "
"'{0}' with enabled state '{1}' successfully."
.format(boot_target, boot_enabled))
def node_action(nodeid, request): def node_action(nodeid, request):
# Only support one action in single request # Only support one action in single request
if len(list(request.keys())) != 1: if len(list(request.keys())) != 1:
@ -589,7 +642,8 @@ def node_action(nodeid, request):
# Because valence assemble node by default when compose node, so only need # Because valence assemble node by default when compose node, so only need
# to support "Reset" action here. In case podm new version support more # to support "Reset" action here. In case podm new version support more
# actions, use "functions" dict to drive the workflow. # actions, use "functions" dict to drive the workflow.
functions = {"Reset": reset_node} functions = {"Reset": reset_node,
"Boot": set_boot_source}
if action not in functions: if action not in functions:
raise exception.BadRequest( raise exception.BadRequest(

View File

@ -471,6 +471,80 @@ class TestRedfish(TestCase):
self.assertEqual(expected, result) self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.send_request')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_set_boot_source_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)
# Test no "Target" parameter
with self.assertRaises(exception.BadRequest) as context:
redfish.set_boot_source("1", {"Boot": {"Enabled": "Once"}})
self.assertTrue("The content of set boot source request is malformed. "
"Please refer to Valence api specification to correct "
"it." in str(context.exception.detail))
# Test no "Enabled" parameter
with self.assertRaises(exception.BadRequest) as context:
redfish.set_boot_source("1", {"Boot": {"Target": "Hdd"}})
self.assertTrue("The content of set boot source request is malformed. "
"Please refer to Valence api specification to correct "
"it." in str(context.exception.detail))
# Test no "Enabled" either "Target" parameter
with self.assertRaises(exception.BadRequest) as context:
redfish.set_boot_source("1", {"Boot": {}})
self.assertTrue("The content of set boot source request is malformed. "
"Please refer to Valence api specification to correct "
"it." in str(context.exception.detail))
# Test wrong "Enabled" parameter
with self.assertRaises(exception.BadRequest) as context:
redfish.set_boot_source("1", {"Boot": {"Enabled": "wrong_input",
"Target": "Hdd"}})
self.assertTrue("The parameter Enabled 'wrong_input' is not in "
"allowable list ['Disabled', 'Once', 'Continuous']."
in str(context.exception.detail))
# Test wrong "Enabled" parameter
with self.assertRaises(exception.BadRequest) as context:
redfish.set_boot_source("1", {"Boot": {"Enabled": "Once",
"Target": "wrong_input"}})
allowable_boot_target = \
(fakes.fake_node_detail()["Boot"]
["BootSourceOverrideTarget@Redfish.AllowableValues"])
self.assertTrue("The parameter Target 'wrong_input' is not in "
"allowable list {0}.".format(allowable_boot_target)
in str(context.exception.detail))
@mock.patch('valence.redfish.redfish.send_request')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_set_boot_source_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.set_boot_source(
"1", {"Boot": {"Enabled": "Once", "Target": "Hdd"}})
expected = exception.confirmation(
confirm_code="Set Boot Source of Composed Node",
confirm_detail="The boot source of composed node has been set to "
"'{0}' with enabled state '{1}' successfully."
.format("Hdd", "Once"))
self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.reset_node') @mock.patch('valence.redfish.redfish.reset_node')
def test_node_action_malformed_request(self, mock_reset_node): def test_node_action_malformed_request(self, mock_reset_node):
"""Test post node_action with malformed request""" """Test post node_action with malformed request"""