diff --git a/api-ref/source/mockup/composed-node-get-response.json b/api-ref/source/mockup/composed-node-get-response.json index aa0423e..1ac9469 100644 --- a/api-ref/source/mockup/composed-node-get-response.json +++ b/api-ref/source/mockup/composed-node-get-response.json @@ -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" : [ diff --git a/valence/api/v1/nodes.py b/valence/api/v1/nodes.py index d7dba12..7552b00 100644 --- a/valence/api/v1/nodes.py +++ b/valence/api/v1/nodes.py @@ -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) diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index 635c09c..5b40ef5 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -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 diff --git a/valence/tests/unit/fakes/redfish_fakes.py b/valence/tests/unit/fakes/redfish_fakes.py index 5eb3f89..4e9bf93 100644 --- a/valence/tests/unit/fakes/redfish_fakes.py +++ b/valence/tests/unit/fakes/redfish_fakes.py @@ -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" + }] + } + } diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index 8da5ac2..f4ad1be 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -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)