Merge "Align api /nodes with api-ref spec"

This commit is contained in:
Jenkins 2017-02-11 01:46:44 +00:00 committed by Gerrit Code Review
commit 0bda7f9558
5 changed files with 563 additions and 115 deletions

View File

@ -6,6 +6,7 @@
"target_boot_source": "Pxe",
"health_status" : "ok",
"name" : "Server-1",
"description": "Description for this node",
"pooled_group_id" : "11z23344-0099-7766-5544-33225511",
"metadata" : {
"system_nic" : [

View File

@ -17,7 +17,9 @@ import logging
from flask import request
from flask_restful import abort
from flask_restful import Resource
from six.moves import http_client
from valence.common import utils
from valence.redfish import redfish
LOG = logging.getLogger(__name__)
@ -26,22 +28,26 @@ LOG = logging.getLogger(__name__)
class NodesList(Resource):
def get(self):
return redfish.nodes_list(request.args)
return utils.make_response(http_client.OK,
redfish.list_nodes())
def post(self):
return redfish.compose_node(request.get_json())
return utils.make_response(
http_client.OK, redfish.compose_node(request.get_json()))
class Nodes(Resource):
def get(self, nodeid):
return redfish.get_nodebyid(nodeid)
return utils.make_response(http_client.OK,
redfish.get_node_by_id(nodeid))
def delete(self, nodeid):
return redfish.delete_composednode(nodeid)
return utils.make_response(http_client.OK,
redfish.delete_composednode(nodeid))
class NodesStorage(Resource):
def get(self, nodeid):
return abort(501)
return abort(http_client.NOT_IMPLEMENTED)

View File

@ -17,10 +17,12 @@ import json
import logging
import os
import flask
import requests
from requests import auth
from six.moves import http_client
from valence.api import link
from valence.common import constants
from valence.common import exception
from valence.common import utils
@ -283,11 +285,143 @@ def get_systembyid(systemid):
return systems_list({"Id": systemid})
def get_nodebyid(nodeid):
node = nodes_list({"Id": nodeid})
if not node:
raise exception.NotFound(detail='Node: %s not found' % nodeid)
return node[0]
def show_cpu_details(cpu_url):
"""Get processor details .
:param cpu_url: relative redfish url to processor,
e.g /redfish/v1/Systems/1/Processors/1.
:returns: dict of processor detail.
"""
resp = send_request(cpu_url)
if resp.status_code != http_client.OK:
# Raise exception if don't find processor
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
respdata = resp.json()
cpu_details = {
"instruction_set": respdata["InstructionSet"],
"model": respdata["Model"],
"speed_mhz": respdata["MaxSpeedMHz"],
"total_core": respdata["TotalCores"]
}
return cpu_details
def show_ram_details(ram_url):
"""Get memory details .
:param ram_url: relative redfish url to memory,
e.g /redfish/v1/Systems/1/Memory/1.
:returns: dict of memory detail.
"""
resp = send_request(ram_url)
if resp.status_code != http_client.OK:
# Raise exception if don't find memory
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
respdata = resp.json()
ram_details = {
"data_width_bit": respdata["DataWidthBits"],
"speed_mhz": respdata["OperatingSpeedMHz"],
"total_memory_mb": respdata["CapacityMiB"]
}
return ram_details
def show_network_details(network_url):
"""Get network interface details .
:param ram_url: relative redfish url to network interface,
e.g /redfish/v1/Systems/1/EthernetInterfaces/1.
:returns: dict of network interface detail.
"""
resp = send_request(network_url)
if resp.status_code != http_client.OK:
# Raise exception if don't find network interface
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
respdata = resp.json()
network_details = {
"speed_mbps": respdata["SpeedMbps"],
"mac": respdata["MACAddress"],
"status": respdata["Status"]["State"],
"ipv4": [{
"address": ipv4["Address"],
"subnet_mask": ipv4["SubnetMask"],
"gateway": ipv4["Gateway"]
} for ipv4 in respdata["IPv4Addresses"]]
}
if respdata["VLANs"]:
# Get vlan info
vlan_url_list = urls2list(respdata["VLANs"]["@odata.id"])
network_details["vlans"] = []
for url in vlan_url_list:
vlan_info = send_request(url).json()
network_details["vlans"].append({
"vlanid": vlan_info["VLANId"],
"status": vlan_info["Status"]["State"]
})
return network_details
def get_node_by_id(node_index, show_detail=True):
"""Get composed node details of specific index.
:param node_index: numeric index of new composed node.
:param show_detail: show more node detail when set to True.
:returns: node detail info.
"""
nodes_base_url = get_base_resource_url('Nodes')
node_url = os.path.normpath('/'.join([nodes_base_url, node_index]))
resp = send_request(node_url)
LOG.debug(resp.status_code)
if resp.status_code != http_client.OK:
# Raise exception if don't find node
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
respdata = resp.json()
node_detail = {
"name": respdata["Name"],
"node_power_state": respdata["PowerState"],
"links": [
link.Link.make_link('self', flask.request.url_root,
'nodes/' + respdata["UUID"], '').as_dict(),
link.Link.make_link('bookmark', flask.request.url_root,
'nodes/' + respdata["UUID"], '',
bookmark=True).as_dict()
]
}
if show_detail:
node_detail.update({
"index": node_index,
"description": respdata["Description"],
"node_state": respdata["ComposedNodeState"],
"boot_source": respdata["Boot"]["BootSourceOverrideTarget"],
"target_boot_source": respdata["Boot"]["BootSourceOverrideTarget"],
"health_status": respdata["Status"]["Health"],
# TODO(lin.yang): "pooled_group_id" is used to check whether
# resource can be assigned to composed node, which should be
# supported after PODM API v2.1 released.
"pooled_group_id": None,
"metadata": {
"processor": [show_cpu_details(i["@odata.id"])
for i in respdata["Links"]["Processors"]],
"memory": [show_ram_details(i["@odata.id"])
for i in respdata["Links"]["Memory"]],
"network": [show_network_details(i["@odata.id"])
for i in respdata["Links"]["EthernetInterfaces"]]
}
})
return node_detail
def build_hierarchy_tree():
@ -309,39 +443,62 @@ def build_hierarchy_tree():
def compose_node(request_body):
"""Compose new node through podm api.
:param request_body: The request content to compose new node, which should
follow podm format. Valence api directly pass it to
podm right now.
:returns: The numeric index of new composed node.
"""
# Get url of allocating resource to node
nodes_url = get_base_resource_url('Nodes')
headers = {'Content-type': 'application/json'}
nodes_resp = send_request(nodes_url, 'GET', headers=headers)
if nodes_resp.status_code != http_client.OK:
resp = send_request(nodes_url, 'GET')
if resp.status_code != http_client.OK:
LOG.error('Unable to query ' + nodes_url)
raise exception.RedfishException(nodes_resp.json(),
status_code=nodes_resp.status_code)
nodes_json = json.loads(nodes_resp.content)
allocate_url = nodes_json['Actions']['#ComposedNodeCollection.Allocate'][
'target']
resp = send_request(allocate_url, 'POST', headers=headers,
json=request_body)
if resp.status_code == http_client.CREATED:
allocated_node = resp.headers['Location']
node_resp = send_request(allocated_node, "GET", headers=headers)
LOG.debug('Successfully allocated node:' + allocated_node)
node_json = json.loads(node_resp.content)
assemble_url = node_json['Actions']['#ComposedNode.Assemble']['target']
LOG.debug('Assembling Node: ' + assemble_url)
assemble_resp = send_request(assemble_url, "POST", headers=headers)
LOG.debug(assemble_resp.status_code)
if assemble_resp.status_code == http_client.NO_CONTENT:
LOG.debug('Successfully assembled node: ' + allocated_node)
return {"node": allocated_node}
else:
parts = allocated_node.split('/')
node_id = parts[-1]
delete_composednode(node_id)
raise exception.RedfishException(assemble_resp.json(),
status_code=resp.status_code)
else:
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
respdata = resp.json()
allocate_url = respdata['Actions']['#ComposedNodeCollection.Allocate'][
'target']
# Allocate resource to this node
LOG.debug('Allocating Node: {0}'.format(request_body))
allocate_resp = send_request(allocate_url, 'POST',
headers={'Content-type': 'application/json'},
json=request_body)
if allocate_resp.status_code != http_client.CREATED:
# Raise exception if allocation failed
raise exception.RedfishException(allocate_resp.json(),
status_code=allocate_resp.status_code)
# Allocated node successfully
# node_url -- relative redfish url e.g redfish/v1/Nodes/1
node_url = allocate_resp.headers['Location'].lstrip(cfg.podm_url)
# node_index -- numeric index of new node e.g 1
node_index = node_url.split('/')[-1]
LOG.debug('Successfully allocated node:' + node_url)
# Get url of assembling node
resp = send_request(node_url, "GET")
respdata = resp.json()
assemble_url = respdata['Actions']['#ComposedNode.Assemble']['target']
# Assemble node
LOG.debug('Assembling Node: {0}'.format(assemble_url))
assemble_resp = send_request(assemble_url, "POST")
if assemble_resp.status_code != http_client.NO_CONTENT:
# Delete node if assemble failed
delete_composednode(node_index)
raise exception.RedfishException(assemble_resp.json(),
status_code=resp.status_code)
else:
# Assemble successfully
LOG.debug('Successfully assembled node: ' + node_url)
# Return new composed node index
return get_node_by_id(node_index, show_detail=False)
def delete_composednode(nodeid):
@ -351,80 +508,24 @@ def delete_composednode(nodeid):
if resp.status_code == http_client.NO_CONTENT:
# we should return 200 status code instead of 204, because 204 means
# 'No Content', the message in resp_dict will be ignored in that way
resp_dict = exception.confirmation(confirm_detail="DELETED")
return utils.make_response(http_client.OK, resp_dict)
return exception.confirmation(
confirm_code="DELETED",
confirm_detail="This composed node has been deleted successfully.")
else:
raise exception.RedfishException(resp.json(),
status_code=resp.status_code)
def nodes_list(filters={}):
def list_nodes():
# list of nodes with hardware details needed for flavor creation
LOG.debug(filters)
lst_nodes = []
# TODO(lin.yang): support filter when list nodes
nodes = []
nodes_url = get_base_resource_url("Nodes")
nodeurllist = urls2list(nodes_url)
# podmtree = build_hierarchy_tree()
# podmtree.writeHTML("0","/tmp/a.html")
node_url_list = urls2list(nodes_url)
for lnk in nodeurllist:
filterPassed = True
resp = send_request(lnk)
if resp.status_code != http_client.OK:
LOG.info("Error in fetching Node details " + lnk)
else:
node = resp.json()
for url in node_url_list:
node_index = url.split('/')[-1]
nodes.append(get_node_by_id(node_index, show_detail=False))
if any(filters):
filterPassed = utils.match_conditions(node, filters)
LOG.info("FILTER PASSED" + str(filterPassed))
if not filterPassed:
continue
nodeid = lnk.split("/")[-1]
nodeuuid = node['UUID']
nodelocation = node['AssetTag']
# podmtree.getPath(lnk) commented as location should be
# computed using other logic.consult Chester
nodesystemurl = node["Links"]["ComputerSystem"]["@odata.id"]
cpu = {}
ram = 0
nw = 0
storage = system_storage_details(nodesystemurl)
cpu = system_cpu_details(nodesystemurl)
if "Memory" in node:
ram = node["Memory"]["TotalSystemMemoryGiB"]
if ("EthernetInterfaces" in node["Links"] and
node["Links"]["EthernetInterfaces"]):
nw = len(node["Links"]["EthernetInterfaces"])
bmcip = "127.0.0.1" # system['Oem']['Dell_G5MC']['BmcIp']
bmcmac = "00:00:00:00:00" # system['Oem']['Dell_G5MC']['BmcMac']
node = {"id": nodeid, "cpu": cpu,
"ram": ram, "storage": storage,
"nw": nw, "location": nodelocation,
"uuid": nodeuuid, "bmcip": bmcip, "bmcmac": bmcmac}
# filter based on RAM, CPU, NETWORK..etc
if 'ram' in filters:
filterPassed = (True
if int(ram) >= int(filters['ram'])
else False)
# filter based on RAM, CPU, NETWORK..etc
if 'nw' in filters:
filterPassed = (True
if int(nw) >= int(filters['nw'])
else False)
# filter based on RAM, CPU, NETWORK..etc
if 'storage' in filters:
filterPassed = (True
if int(storage) >= int(filters['storage'])
else False)
if filterPassed:
lst_nodes.append(node)
return lst_nodes
return nodes

View File

@ -51,6 +51,107 @@ def fake_service_root():
}
def fake_nodes_root():
return {
"@odata.context": "/redfish/v1/$metadata#Nodes",
"@odata.id": "/redfish/v1/Nodes",
"@odata.type": "#ComposedNodeCollection.ComposedNodeCollection",
"Name": "Composed Nodes Collection",
"Members@odata.count": 1,
"Members": [{
"@odata.id": "/redfish/v1/Nodes/14"
}],
"Actions": {
"#ComposedNodeCollection.Allocate": {
"target": "/redfish/v1/Nodes/Actions/Allocate"
}
}
}
def fake_node_detail():
return {
"@odata.context": "/redfish/v1/$metadata#Nodes/Members/$entity",
"@odata.id": "/redfish/v1/Nodes/6",
"@odata.type": "#ComposedNode.1.0.0.ComposedNode",
"Id": "6",
"Name": "test",
"Description": "",
"SystemType": "Logical",
"AssetTag": "",
"Manufacturer": "",
"Model": "",
"SKU": "",
"SerialNumber": "",
"PartNumber": "",
"UUID": "deba2630-d2af-11e6-a65f-4d709ab9a725",
"HostName": "web-srv344",
"PowerState": "On",
"BiosVersion": "P79 v1.00 (09/20/2013)",
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
},
"Processors": {
"Count": 1,
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
}
},
"Memory": {
"TotalSystemMemoryGiB": 8,
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
}
},
"ComposedNodeState": "PoweredOff",
"Boot": {
"BootSourceOverrideEnabled": "Continuous",
"BootSourceOverrideTarget": "Hdd",
"BootSourceOverrideTarget@Redfish.AllowableValues": [
"None", "Pxe", "Floppy", "Cd", "Usb", "Hdd", "BiosSetup",
"Utilities", "Diags", "UefiTarget"]
},
"Oem": {},
"Links": {
"ComputerSystem": {
"@odata.id": "/redfish/v1/Systems/1"
},
"Processors": [{
"@odata.id": "/redfish/v1/Systems/1/Processors/1"
}],
"Memory": [{
"@odata.id": "/redfish/v1/Systems/1/Memory/1"
}],
"EthernetInterfaces": [{
"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces/2"
}],
"LocalDrives": [],
"RemoteDrives": [],
"ManagedBy": [{
"@odata.id": "/redfish/v1/Managers/1"
}],
"Oem": {}
},
"Actions": {
"#ComposedNode.Reset": {
"target": "/redfish/v1/Nodes/6/Actions/ComposedNode.Reset",
"ResetType@DMTF.AllowableValues": [
"On", "ForceOff", "GracefulShutdown", "ForceRestart",
"Nmi", "GracefulRestart", "ForceOn", "PushPowerButton"]
},
"#ComposedNode.Assemble": {
"target": "/redfish/v1/Nodes/6/Actions/ComposedNode.Assemble"
}
}
}
def fake_chassis_list():
return [
{
@ -135,6 +236,50 @@ def fake_simple_storage():
}
def fake_processor():
return {
"InstructionSet": "x86-64",
"Model": "Intel(R) Core(TM) i7-4790",
"MaxSpeedMHz": 3700,
"TotalCores": 8,
}
def fake_memory():
return {
"DataWidthBits": 0,
"OperatingSpeedMHz": 2400,
"CapacityMiB": 8192
}
def fake_network_interface():
return {
"MACAddress": "e9:47:d3:60:64:66",
"SpeedMbps": 100,
"Status": {
"State": "Enabled"
},
"IPv4Addresses": [{
"Address": "192.168.0.10",
"SubnetMask": "255.255.252.0",
"Gateway": "192.168.0.1",
}],
"VLANs": {
"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces/2/VLANs"
}
}
def fake_vlan():
return {
"VLANId": 99,
"Status": {
"State": "Enabled",
}
}
def fake_system_ethernet_interfaces():
return {
"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces",
@ -163,3 +308,36 @@ def fake_delete_composednode_fail():
}]
}
}
def fake_allocate_node_conflict():
return {
"error": {
"code": "Base.1.0.ResourcesStateMismatch",
"message": "Conflict during allocation",
"@Message.ExtendedInfo": [{
"Message": "There are no computer systems available for this "
"allocation request."
}, {
"Message": "Available assets count after applying filters: ["
"available: 0 -> status: 0 -> resource: 0 -> "
"chassis: 0 -> processors: 0 -> memory: 0 -> "
"local drives: 0 -> ethernet interfaces: 0]"
}]
}
}
def fake_assemble_node_failed():
return {
"error": {
"code": "Base.1.0.InvalidPayload",
"message": "Request payload is invalid or missing",
"@Message.ExtendedInfo": [{
"Message": "Assembly action could not be completed!"
}, {
"Message": "Assembly failed: Only composed node in ALLOCATED "
"state can be assembled"
}]
}
}

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
from unittest import TestCase
import mock
@ -189,24 +190,80 @@ class TestRedfish(TestCase):
result = redfish.system_storage_details("/redfish/v1/Systems/test")
self.assertEqual(expected, result)
@mock.patch('valence.common.utils.make_response')
@mock.patch('valence.redfish.redfish.send_request')
def test_show_cpu_details(self, mock_request):
mock_request.return_value = fakes.mock_request_get(
fakes.fake_processor(), http_client.OK)
expected = {
"instruction_set": "x86-64",
"model": "Intel(R) Core(TM) i7-4790",
"speed_mhz": 3700,
"total_core": 8,
}
result = redfish.show_cpu_details("/redfish/v1/Systems/1/Processors/1")
self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.send_request')
def test_show_memory_details(self, mock_request):
mock_request.return_value = fakes.mock_request_get(
fakes.fake_memory(), http_client.OK)
expected = {
"data_width_bit": 0,
"speed_mhz": 2400,
"total_memory_mb": 8192
}
result = redfish.show_ram_details("/redfish/v1/Systems/1/Memory/1")
self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.urls2list')
@mock.patch('valence.redfish.redfish.send_request')
def test_show_network_interface_details(self, mock_request, mock_url2list):
mock_request.side_effect = [
fakes.mock_request_get(fakes.fake_network_interface(),
http_client.OK),
fakes.mock_request_get(fakes.fake_vlan(),
http_client.OK)
]
mock_url2list.return_value = [
"redfish/v1/Systems/1/EthernetInterfaces/2/VLANs/1"]
expected = {
"mac": "e9:47:d3:60:64:66",
"speed_mbps": 100,
"status": "Enabled",
"ipv4": [{
"address": "192.168.0.10",
"subnet_mask": "255.255.252.0",
"gateway": "192.168.0.1",
}],
'vlans': [{
'status': 'Enabled',
'vlanid': 99
}]
}
result = redfish.show_network_details(
"/redfish/v1/Systems/1/EthernetInterfaces/1")
self.assertEqual(expected, result)
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_delete_composednode_ok(self, mock_request, mock_get_url,
mock_make_response):
def test_delete_composednode_ok(self, mock_request, mock_get_url):
mock_get_url.return_value = '/redfish/v1/Nodes'
delete_result = fakes.fake_delete_composednode_ok()
fake_delete_response = fakes.mock_request_get(delete_result,
http_client.NO_CONTENT)
mock_request.return_value = fake_delete_response
redfish.delete_composednode(101)
result = redfish.delete_composednode(101)
mock_request.assert_called_with('/redfish/v1/Nodes/101', 'DELETE')
expected_content = {
"code": "",
"detail": "DELETED",
expected = {
"code": "DELETED",
"detail": "This composed node has been deleted successfully.",
"request_id": exception.FAKE_REQUEST_ID,
}
mock_make_response.assert_called_with(http_client.OK, expected_content)
self.assertEqual(expected, result)
@mock.patch('valence.common.utils.make_response')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@ -260,3 +317,108 @@ class TestRedfish(TestCase):
mock_get.asset_called_once_with('url',
auth=auth.HTTPBasicAuth('username',
'password'))
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_allocate_node_conflict(self, mock_request, mock_get_url):
"""Test allocate resource conflict when compose node"""
mock_get_url.return_value = '/redfish/v1/Nodes'
# Fake response for getting nodes root
fake_node_root_resp = fakes.mock_request_get(fakes.fake_nodes_root(),
http_client.OK)
# Fake response for allocating node
fake_node_allocation_conflict = \
fakes.mock_request_get(fakes.fake_allocate_node_conflict(),
http_client.CONFLICT)
mock_request.side_effect = [fake_node_root_resp,
fake_node_allocation_conflict]
with self.assertRaises(exception.RedfishException) as context:
redfish.compose_node({"name": "test_node"})
self.assertTrue("There are no computer systems available for this "
"allocation request." in str(context.exception.detail))
@mock.patch('valence.redfish.redfish.delete_composednode')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_assemble_node_failed(self, mock_request, mock_get_url,
mock_delete_node):
"""Test allocate resource conflict when compose node"""
mock_get_url.return_value = '/redfish/v1/Nodes'
# Fake response for getting nodes root
fake_node_root_resp = fakes.mock_request_get(fakes.fake_nodes_root(),
http_client.OK)
# Fake response for allocating node
fake_node_allocation_conflict = mock.MagicMock()
fake_node_allocation_conflict.status_code = http_client.CREATED
fake_node_allocation_conflict.headers['Location'] = \
os.path.normpath("/".join([cfg.podm_url, 'redfish/v1/Nodes/1']))
# Fake response for getting url of node assembling
fake_node_detail = fakes.mock_request_get(fakes.fake_node_detail(),
http_client.OK)
# Fake response for assembling node
fake_node_assemble_failed = fakes.mock_request_get(
fakes.fake_assemble_node_failed(), http_client.BAD_REQUEST)
mock_request.side_effect = [fake_node_root_resp,
fake_node_allocation_conflict,
fake_node_detail,
fake_node_assemble_failed]
with self.assertRaises(exception.RedfishException):
redfish.compose_node({"name": "test_node"})
mock_delete_node.assert_called_once()
@mock.patch('valence.redfish.redfish.get_node_by_id')
@mock.patch('valence.redfish.redfish.delete_composednode')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
@mock.patch('valence.redfish.redfish.send_request')
def test_assemble_node_success(self, mock_request, mock_get_url,
mock_delete_node, mock_get_node_by_id):
"""Test compose node successfully"""
mock_get_url.return_value = '/redfish/v1/Nodes'
# Fake response for getting nodes root
fake_node_root_resp = fakes.mock_request_get(fakes.fake_nodes_root(),
http_client.OK)
# Fake response for allocating node
fake_node_allocation_conflict = mock.MagicMock()
fake_node_allocation_conflict.status_code = http_client.CREATED
fake_node_allocation_conflict.headers['Location'] = \
os.path.normpath("/".join([cfg.podm_url, 'redfish/v1/Nodes/1']))
# Fake response for getting url of node assembling
fake_node_detail = fakes.mock_request_get(fakes.fake_node_detail(),
http_client.OK)
# Fake response for assembling node
fake_node_assemble_failed = fakes.mock_request_get(
{}, http_client.NO_CONTENT)
mock_request.side_effect = [fake_node_root_resp,
fake_node_allocation_conflict,
fake_node_detail,
fake_node_assemble_failed]
redfish.compose_node({"name": "test_node"})
mock_delete_node.assert_not_called()
mock_get_node_by_id.assert_called_once()
@mock.patch('valence.redfish.redfish.get_node_by_id')
@mock.patch('valence.redfish.redfish.urls2list')
@mock.patch('valence.redfish.redfish.get_base_resource_url')
def test_list_node(self, mock_get_url, mock_url2list, mock_get_node_by_id):
"""Test list node"""
mock_get_url.return_value = '/redfish/v1/Nodes'
mock_url2list.return_value = ['redfish/v1/Nodes/1']
mock_get_node_by_id.side_effect = ["node1_detail"]
result = redfish.list_nodes()
mock_get_node_by_id.assert_called_with("1", show_detail=False)
self.assertEqual(["node1_detail"], result)