Merge "Align api /nodes with api-ref spec"
This commit is contained in:
commit
0bda7f9558
@ -6,6 +6,7 @@
|
|||||||
"target_boot_source": "Pxe",
|
"target_boot_source": "Pxe",
|
||||||
"health_status" : "ok",
|
"health_status" : "ok",
|
||||||
"name" : "Server-1",
|
"name" : "Server-1",
|
||||||
|
"description": "Description for this node",
|
||||||
"pooled_group_id" : "11z23344-0099-7766-5544-33225511",
|
"pooled_group_id" : "11z23344-0099-7766-5544-33225511",
|
||||||
"metadata" : {
|
"metadata" : {
|
||||||
"system_nic" : [
|
"system_nic" : [
|
||||||
|
@ -17,7 +17,9 @@ import logging
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_restful import abort
|
from flask_restful import abort
|
||||||
from flask_restful import Resource
|
from flask_restful import Resource
|
||||||
|
from six.moves import http_client
|
||||||
|
|
||||||
|
from valence.common import utils
|
||||||
from valence.redfish import redfish
|
from valence.redfish import redfish
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -26,22 +28,26 @@ LOG = logging.getLogger(__name__)
|
|||||||
class NodesList(Resource):
|
class NodesList(Resource):
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return redfish.nodes_list(request.args)
|
return utils.make_response(http_client.OK,
|
||||||
|
redfish.list_nodes())
|
||||||
|
|
||||||
def post(self):
|
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):
|
class Nodes(Resource):
|
||||||
|
|
||||||
def get(self, nodeid):
|
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):
|
def delete(self, nodeid):
|
||||||
return redfish.delete_composednode(nodeid)
|
return utils.make_response(http_client.OK,
|
||||||
|
redfish.delete_composednode(nodeid))
|
||||||
|
|
||||||
|
|
||||||
class NodesStorage(Resource):
|
class NodesStorage(Resource):
|
||||||
|
|
||||||
def get(self, nodeid):
|
def get(self, nodeid):
|
||||||
return abort(501)
|
return abort(http_client.NOT_IMPLEMENTED)
|
||||||
|
@ -17,10 +17,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import flask
|
||||||
import requests
|
import requests
|
||||||
from requests import auth
|
from requests import auth
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
|
||||||
|
from valence.api import link
|
||||||
from valence.common import constants
|
from valence.common import constants
|
||||||
from valence.common import exception
|
from valence.common import exception
|
||||||
from valence.common import utils
|
from valence.common import utils
|
||||||
@ -283,11 +285,143 @@ def get_systembyid(systemid):
|
|||||||
return systems_list({"Id": systemid})
|
return systems_list({"Id": systemid})
|
||||||
|
|
||||||
|
|
||||||
def get_nodebyid(nodeid):
|
def show_cpu_details(cpu_url):
|
||||||
node = nodes_list({"Id": nodeid})
|
"""Get processor details .
|
||||||
if not node:
|
|
||||||
raise exception.NotFound(detail='Node: %s not found' % nodeid)
|
:param cpu_url: relative redfish url to processor,
|
||||||
return node[0]
|
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():
|
def build_hierarchy_tree():
|
||||||
@ -309,39 +443,62 @@ def build_hierarchy_tree():
|
|||||||
|
|
||||||
|
|
||||||
def compose_node(request_body):
|
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')
|
nodes_url = get_base_resource_url('Nodes')
|
||||||
headers = {'Content-type': 'application/json'}
|
resp = send_request(nodes_url, 'GET')
|
||||||
nodes_resp = send_request(nodes_url, 'GET', headers=headers)
|
if resp.status_code != http_client.OK:
|
||||||
if nodes_resp.status_code != http_client.OK:
|
|
||||||
LOG.error('Unable to query ' + nodes_url)
|
LOG.error('Unable to query ' + nodes_url)
|
||||||
raise exception.RedfishException(nodes_resp.json(),
|
raise exception.RedfishException(resp.json(),
|
||||||
status_code=nodes_resp.status_code)
|
status_code=resp.status_code)
|
||||||
nodes_json = json.loads(nodes_resp.content)
|
respdata = resp.json()
|
||||||
allocate_url = nodes_json['Actions']['#ComposedNodeCollection.Allocate'][
|
allocate_url = respdata['Actions']['#ComposedNodeCollection.Allocate'][
|
||||||
'target']
|
'target']
|
||||||
resp = send_request(allocate_url, 'POST', headers=headers,
|
|
||||||
|
# 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)
|
json=request_body)
|
||||||
if resp.status_code == http_client.CREATED:
|
if allocate_resp.status_code != http_client.CREATED:
|
||||||
allocated_node = resp.headers['Location']
|
# Raise exception if allocation failed
|
||||||
node_resp = send_request(allocated_node, "GET", headers=headers)
|
raise exception.RedfishException(allocate_resp.json(),
|
||||||
LOG.debug('Successfully allocated node:' + allocated_node)
|
status_code=allocate_resp.status_code)
|
||||||
node_json = json.loads(node_resp.content)
|
|
||||||
assemble_url = node_json['Actions']['#ComposedNode.Assemble']['target']
|
# Allocated node successfully
|
||||||
LOG.debug('Assembling Node: ' + assemble_url)
|
# node_url -- relative redfish url e.g redfish/v1/Nodes/1
|
||||||
assemble_resp = send_request(assemble_url, "POST", headers=headers)
|
node_url = allocate_resp.headers['Location'].lstrip(cfg.podm_url)
|
||||||
LOG.debug(assemble_resp.status_code)
|
# node_index -- numeric index of new node e.g 1
|
||||||
if assemble_resp.status_code == http_client.NO_CONTENT:
|
node_index = node_url.split('/')[-1]
|
||||||
LOG.debug('Successfully assembled node: ' + allocated_node)
|
LOG.debug('Successfully allocated node:' + node_url)
|
||||||
return {"node": allocated_node}
|
|
||||||
else:
|
# Get url of assembling node
|
||||||
parts = allocated_node.split('/')
|
resp = send_request(node_url, "GET")
|
||||||
node_id = parts[-1]
|
respdata = resp.json()
|
||||||
delete_composednode(node_id)
|
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(),
|
raise exception.RedfishException(assemble_resp.json(),
|
||||||
status_code=resp.status_code)
|
status_code=resp.status_code)
|
||||||
else:
|
else:
|
||||||
raise exception.RedfishException(resp.json(),
|
# Assemble successfully
|
||||||
status_code=resp.status_code)
|
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):
|
def delete_composednode(nodeid):
|
||||||
@ -351,80 +508,24 @@ def delete_composednode(nodeid):
|
|||||||
if resp.status_code == http_client.NO_CONTENT:
|
if resp.status_code == http_client.NO_CONTENT:
|
||||||
# we should return 200 status code instead of 204, because 204 means
|
# 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
|
# 'No Content', the message in resp_dict will be ignored in that way
|
||||||
resp_dict = exception.confirmation(confirm_detail="DELETED")
|
return exception.confirmation(
|
||||||
return utils.make_response(http_client.OK, resp_dict)
|
confirm_code="DELETED",
|
||||||
|
confirm_detail="This composed node has been deleted successfully.")
|
||||||
else:
|
else:
|
||||||
raise exception.RedfishException(resp.json(),
|
raise exception.RedfishException(resp.json(),
|
||||||
status_code=resp.status_code)
|
status_code=resp.status_code)
|
||||||
|
|
||||||
|
|
||||||
def nodes_list(filters={}):
|
def list_nodes():
|
||||||
# list of nodes with hardware details needed for flavor creation
|
# 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")
|
nodes_url = get_base_resource_url("Nodes")
|
||||||
nodeurllist = urls2list(nodes_url)
|
node_url_list = urls2list(nodes_url)
|
||||||
# podmtree = build_hierarchy_tree()
|
|
||||||
# podmtree.writeHTML("0","/tmp/a.html")
|
|
||||||
|
|
||||||
for lnk in nodeurllist:
|
for url in node_url_list:
|
||||||
filterPassed = True
|
node_index = url.split('/')[-1]
|
||||||
resp = send_request(lnk)
|
nodes.append(get_node_by_id(node_index, show_detail=False))
|
||||||
if resp.status_code != http_client.OK:
|
|
||||||
LOG.info("Error in fetching Node details " + lnk)
|
|
||||||
else:
|
|
||||||
node = resp.json()
|
|
||||||
|
|
||||||
if any(filters):
|
return nodes
|
||||||
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
|
|
||||||
|
@ -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():
|
def fake_chassis_list():
|
||||||
return [
|
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():
|
def fake_system_ethernet_interfaces():
|
||||||
return {
|
return {
|
||||||
"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces",
|
"@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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
@ -189,24 +190,80 @@ class TestRedfish(TestCase):
|
|||||||
result = redfish.system_storage_details("/redfish/v1/Systems/test")
|
result = redfish.system_storage_details("/redfish/v1/Systems/test")
|
||||||
self.assertEqual(expected, result)
|
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.get_base_resource_url')
|
||||||
@mock.patch('valence.redfish.redfish.send_request')
|
@mock.patch('valence.redfish.redfish.send_request')
|
||||||
def test_delete_composednode_ok(self, mock_request, mock_get_url,
|
def test_delete_composednode_ok(self, mock_request, mock_get_url):
|
||||||
mock_make_response):
|
|
||||||
mock_get_url.return_value = '/redfish/v1/Nodes'
|
mock_get_url.return_value = '/redfish/v1/Nodes'
|
||||||
delete_result = fakes.fake_delete_composednode_ok()
|
delete_result = fakes.fake_delete_composednode_ok()
|
||||||
fake_delete_response = fakes.mock_request_get(delete_result,
|
fake_delete_response = fakes.mock_request_get(delete_result,
|
||||||
http_client.NO_CONTENT)
|
http_client.NO_CONTENT)
|
||||||
mock_request.return_value = fake_delete_response
|
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')
|
mock_request.assert_called_with('/redfish/v1/Nodes/101', 'DELETE')
|
||||||
expected_content = {
|
expected = {
|
||||||
"code": "",
|
"code": "DELETED",
|
||||||
"detail": "DELETED",
|
"detail": "This composed node has been deleted successfully.",
|
||||||
"request_id": exception.FAKE_REQUEST_ID,
|
"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.common.utils.make_response')
|
||||||
@mock.patch('valence.redfish.redfish.get_base_resource_url')
|
@mock.patch('valence.redfish.redfish.get_base_resource_url')
|
||||||
@ -260,3 +317,108 @@ class TestRedfish(TestCase):
|
|||||||
mock_get.asset_called_once_with('url',
|
mock_get.asset_called_once_with('url',
|
||||||
auth=auth.HTTPBasicAuth('username',
|
auth=auth.HTTPBasicAuth('username',
|
||||||
'password'))
|
'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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user