Add ExpEther Driver in Valence

This commit adds ExpEther driver support inside valence to be able
to manage NEC's ExpEther hardware.
This would fit into valence support of multi-podm architecture.

Change-Id: Ie8eb4d50e88d52aefc3732fd07cd8ed1ad5aee6f
Implements: blueprint add-expether-driver
This commit is contained in:
akhiljain23 2018-03-16 16:58:43 +05:30 committed by Akhil jain
parent ecd965149b
commit a5cc27bf41
18 changed files with 1172 additions and 77 deletions

View File

@ -68,3 +68,4 @@ valence.provision.driver =
valence.podmanager.driver =
redfishv1 = valence.podmanagers.podm_base:PodManagerBase
expether = valence.podmanagers.expether_manager:ExpEtherManager

View File

@ -17,3 +17,7 @@ PODM_AUTH_BASIC_TYPE = 'basic'
PODM_STATUS_ONLINE = 'Online'
PODM_STATUS_OFFLINE = 'Offline'
PODM_STATUS_UNKNOWN = "Unknown"
HTTP_HEADERS = {"Content-type": "application/json"}
DEVICE_STATES = {'ALLOCATED': 'allocated', 'FREE': 'free'}

View File

@ -110,6 +110,10 @@ class RedfishException(ValenceError):
request_id)
class ExpEtherException(ValenceError):
_msg_fmt = "ExpEther Exception"
class ResourceExists(ValenceError):
status = http_client.CONFLICT
_msg_fmt = "Resource Already Exists"

View File

@ -0,0 +1,71 @@
# Copyright (c) 2017 NEC, Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import requests
from six.moves import http_client
LOG = logging.getLogger(__name__)
OK = http_client.OK
CREATED = http_client.CREATED
NO_CONTENT = http_client.NO_CONTENT
def get(url, http_auth, **kwargs):
try:
return requests.request('GET', url, verify=False, auth=http_auth,
**kwargs)
except requests.exceptions.RequestException as ex:
LOG.error(ex)
raise ex
def patch(url, http_auth, **kwargs):
try:
return requests.request('PATCH', url, verify=False, auth=http_auth,
**kwargs)
except requests.exceptions.RequestException as ex:
LOG.error(ex)
raise ex
def post(url, http_auth, data=None, **kwargs):
try:
return requests.request('POST', url, data=data, verify=False,
auth=http_auth, **kwargs)
except requests.exceptions.RequestException as ex:
LOG.error(ex)
raise ex
def delete(url, http_auth, **kwargs):
try:
return requests.request('DELETE', url, verify=False, auth=http_auth,
**kwargs)
except requests.exceptions.RequestException as ex:
LOG.error(ex)
raise ex
def put(url, http_auth, **kwargs):
headers = {"Content-Type": "application/json"}
try:
return requests.request('PUT', url, verify=False, headers=headers,
auth=http_auth, **kwargs)
except requests.exceptions.RequestException as ex:
LOG.error(ex)
raise ex

View File

@ -49,32 +49,6 @@ class Node(object):
return {key: node_info[key] for key in node_info.keys()
if key in ["uuid", "name", "podm_id", "index", "resource_uri"]}
@staticmethod
def _create_compose_request(name, description, requirements):
request = {}
request["Name"] = name
request["Description"] = description
memory = {}
if "memory" in requirements:
if "capacity_mib" in requirements["memory"]:
memory["CapacityMiB"] = requirements["memory"]["capacity_mib"]
if "type" in requirements["memory"]:
memory["DimmDeviceType"] = requirements["memory"]["type"]
request["Memory"] = [memory]
processor = {}
if "processor" in requirements:
if "model" in requirements["processor"]:
processor["Model"] = requirements["processor"]["model"]
if "total_cores" in requirements["processor"]:
processor["TotalCores"] = (
requirements["processor"]["total_cores"])
request["Processors"] = [processor]
return request
def compose_node(self, request_body):
"""Compose new node
@ -97,10 +71,10 @@ class Node(object):
# "description" is optional
description = request_body.get("description", "")
compose_request = self._create_compose_request(name, description,
requirements)
composed_node = self.connection.compose_node(compose_request)
# Moving _create_compose_request to drivers as this can be
# vendor specific request
composed_node = self.connection.compose_node(name, description,
requirements)
composed_node["uuid"] = utils.generate_uuid()
# Only store the minimum set of composed node info into backend db,

View File

@ -80,6 +80,12 @@ def delete_podmanager(uuid):
p_nodes = db_api.Connection.list_composed_nodes({'podm_id': uuid})
# Delete the nodes w.r.t podmanager from valence DB
for node in p_nodes:
nodes.Node(node['uuid']).delete_composed_node(node['uuid'])
nodes.Node(node['uuid']).delete_composed_node()
# Delete the devices w.r.t podmanager from valence DB
devices_list = db_api.Connection.list_devices(
filters={'podm_id': uuid})
for device in devices_list:
db_api.Connection.delete_device(device['uuid'])
return db_api.Connection.delete_podmanager(uuid)

View File

@ -113,7 +113,7 @@ class PooledDevices(object):
db_api.Connection.add_device(dev)
response['status'] = 'SUCCESS'
except exception.ValenceException as e:
LOG.exception("Update devices failed with exception %s", str(e))
except exception.ValenceError:
LOG.exception("Failed to update resources from podm")
response['status'] = 'FAILED'
return response

View File

@ -186,6 +186,12 @@ class Flavor(ModelBaseWithTimeStamp):
},
'validate': types.Dict.validate
},
'pci_device': {
'type': {
'validate': types.List.validate
},
'validate': types.Dict.validate
},
'validate': types.Dict.validate
}
}

View File

@ -0,0 +1,537 @@
# Copyright (c) 2017 NEC, Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
from oslo_utils import importutils
import requests
from valence.common import constants
from valence.common import exception
from valence.common import http_adapter as http
from valence.db import api as db_api
hwdata = importutils.try_import('hwdata', default=None)
# Free devices group_id
EEIO_DEFAULT_GID = '4093'
EESV_DEFAULT_GID = '4094'
# device_type (PCI Base Class Code)
type_SSD = 0x1
type_NIC = 0x2
type_GPU = 0x3
type_MULTI = 0x4
type_SERIAL = 0xC
type_USB = 0x3
type_ACS = 0x12
type_SWITCH = 0x6
LOG = logging.getLogger(__name__)
class ExpEtherManager(object):
def __init__(self, username, password, url):
self.url = url
self.auth = requests.auth.HTTPBasicAuth(username, password)
def _send_request_to_eem(self, query, method='get', **kwargs):
"""Prepare request and send it to EEM
:param query: request URI of API
:param method: method with which request is sent
:param kwargs: additional values if any
:return: response in json format
"""
request_url = '/'.join([self.url, query])
f = getattr(http, method)
response = f(request_url, self.auth, **kwargs)
self._handle_exceptions(response)
return response.json()
def compose_node(self, name, description, requirements):
"""Compose node according to flavor
Attaches the devices as per the flavor specified and returns the
EESV node information.
:param name: name of node
:param description: description if any
:param requirements: format {'pci_device': {'type': ['NIC', 'SSD']}}
:return: allocated node info as dict
:raises: ExpEtherException , if eesv not available or requested device
is not available(e.g SSD)
"""
# New key 'PCIDevice' is added to support ExpEther devices.
# Get available EESV list and choose first available node
# TODO(ramineni): work on load balancing part
eesv_list = self._send_request_to_eem(
query="devices?status=eesv")['devices']
nodes_index = [node['index'] for node in
db_api.Connection.list_composed_nodes()]
available_eesv = None
for eesv in eesv_list:
if eesv['id'] not in nodes_index:
available_eesv = eesv
break
if available_eesv is None:
LOG.exception("EESV node not available to compose a node")
raise exception.ExpEtherException(status=404, code="40400",
detail="Node not available")
# Attach device according to flavor or properties in request
if ('pci_device' in requirements and
'type' in requirements['pci_device']):
req_pci_type = requirements['pci_device']['type']
for pci_type in req_pci_type:
dev_filters = {'pooled_group_id': EEIO_DEFAULT_GID,
'type': pci_type}
eeio_list = db_api.Connection.list_devices(dev_filters)
if not eeio_list:
LOG.exception("Device type: %s not existing in database",
pci_type)
raise exception.ExpEtherException(status=404, code="40400",
detail="Requested device"
" not available")
self.attach(device_db=eeio_list[0],
node_index=available_eesv['id'])
# If properties is empty, return the existing available eesv node.
index = available_eesv['id']
composed_node = {'name': name, 'index': index,
'resource_uri': 'devices/' + index}
return composed_node
def get_node_info(self, node_index):
"""To get existing eesv nodes details to manage it via valence
:param node_index: EESV index
:return: dict of eesv details
"""
query = 'devices/' + node_index
node_info = self._send_request_to_eem(query)['device']
if node_info['status'] == 'eeio':
LOG.exception("EEIO device id %s passed instead of eesv",
node_index)
raise exception.ExpEtherException("Not a valid EESV node %s"
% node_index)
node_detail = {'name': node_info['id'],
'resource_uri': query,
'serial_number': node_info['serial_number'],
'power_state': node_info['power_status'],
'host_model': node_info['host_model'],
'host_serial_number': node_info['host_serial_number'],
'index': node_index,
'description': node_info['model'],
'type': node_info['type'],
'mac_address': node_info['mac_address'],
'pooled_group_id': node_info['group_id']
}
return node_detail
def delete_composed_node(self, node_id):
"""Delete composed node with respective node index
Function it perform:-
Detaches all connected devices to this node
(updates group id of device in valence db and EEM to 4093)
:param node_id: index of the node
:raises ExpEtherException if detaching devices fails
"""
try:
self._detach_all_devices_from_node(node_id)
except Exception as e:
raise exception.ExpEtherException("Composed node %s deletion "
"failed with error : %s"
% (node_id, e.detail))
def node_action(self, node_index, request):
"""Attaches and detaches device to a node
:param node_index: eesv node index
:param request: Contains type_of_action(attach or detach) and
resource_id
Sample request:
{"detach":
{"resource_id": "660a95b3-adaa-42d3-ac3f-dfe7ce6c9986"}
}
"""
# NOTE: type_of_action can be attach or detach
action = list(request.keys())[0]
if action not in ['attach', 'detach']:
LOG.exception("Unsupported action: %s", action)
raise exception.BadRequest(detail="Unsupported action: " + action)
device_db = {}
if request[action].get('resource_id'):
device_uuid = request[action]['resource_id']
device_db = db_api.Connection.get_device_by_uuid(
device_uuid).as_dict()
f = getattr(self, action, None)
f(device_db, node_index)
def systems_list(self, filters=None):
"""Retrieves list of all connected eesv systems
:return: List of connected eesv's
:raises ExpEtherException if unable to fetch system details
"""
query = "devices/detail?status=eesv"
try:
results = []
eesv_list = self._send_request_to_eem(query)['devices']
for eesv in eesv_list:
results.append(self._system_dict(eesv))
return results
except exception.ExpEtherException as e:
message = "Listing eesv nodes failed with error: %s" % e.detail
raise exception.ExpEtherException(code=e.code, detail=message,
status=e.status)
def get_system_by_id(self, system_id):
"""Get system detail by system_id provided by user
:param system_id: User provided system_id
:return: eesv node info
:raises ExpEtherException if unable to fetch details
"""
query = 'devices/' + system_id
system = self._send_request_to_eem(query=query)['device']
if system['status'] == 'eeio':
LOG.exception("eeio device id %s passed instead of eesv",
system_id)
raise exception.ExpEtherException("Not a valid EESV node %s"
% system_id)
update_time = self._convert_time_format(system['update_time'])
system = {'id': system['id'],
'type': system['type'],
'pooled_group_id': system['group_id'],
'mac_address': system['mac_address'],
'serial_number': system['serial_number'],
'name': system['status'],
'power_state': system['power_status'],
'host_model': system['host_model'],
'host_serial_number': system['host_serial_number'],
'description': system['model'],
'ee_version': system['ee_version'],
'update_time': update_time
}
return system
def attach(self, device_db, node_index):
"""Attaches device to requested node
Performs two actions:
1) Sets group id of eeio device in EEM
2) Updates device info in valence DB
:param device_db: device info from valence DB
:param node_index: EESV id to which device needs to be attached
:raises BadRequest if devices is not free
"""
LOG.debug("Attach device %s to node %s", device_db['uuid'],
node_index)
device_id = device_db['properties']['device_id']
if (device_db['node_id'] is not None or
device_db['pooled_group_id'] != EEIO_DEFAULT_GID):
raise exception.BadRequest(
detail="Device %s already assigned to a different node: %s"
% (device_db['uuid'], device_db['node_id']))
node_query = 'devices/' + node_index
eesv = self._send_request_to_eem(node_query)['device']
# Set group id of eesv if it is default
if eesv['group_id'] == EESV_DEFAULT_GID:
eesv['group_id'] = self._set_gid(device_id=node_index,
group_id=None)['group_id']
LOG.warning('Group ID of an EESV %s has been updated, a reboot'
'might be required for changes to take effect',
node_index)
# Check if maximum number of devices are already connected to eesv
max_eeio_count = eesv['max_eeio_count']
query = "devices?status=eeio&group_id=" + eesv['group_id']
eeio_list = self._send_request_to_eem(query)['devices']
eeio_count = len(eeio_list)
if eeio_count >= int(max_eeio_count):
LOG.exception("Only %s devices that can be attached to node %s",
max_eeio_count, node_index)
message = ("Node %s has already maximun number of devices attached"
% node_index)
raise exception.ExpEtherException(title='Internal server error',
detail=message, status=500)
self._set_gid(device_id=device_id, group_id=eesv['group_id'])
update_dev_info = {"pooled_group_id": eesv['group_id'],
"node_id": node_index,
"state": constants.DEVICE_STATES['ALLOCATED']
}
db_api.Connection.update_device(device_db['uuid'], update_dev_info)
def detach(self, device_db, node_index=None):
"""Detaches device from requested node
Performs two function:
1) Delete group id of eeio device in EEM
2) Updates device group id in valence DB
:param device_db: Valence DB entry of device which is to be updated
:param node_index: None
"""
LOG.debug("Detach device %s from node %s", device_db['uuid'],
node_index)
device_id = device_db['properties']['device_id']
device_uuid = device_db['uuid']
if device_db['pooled_group_id'] == EEIO_DEFAULT_GID:
LOG.debug("Device %s is not attached to any node" % device_uuid)
return
self._del_gid(device_id)
update_dev_info = {"pooled_group_id": EEIO_DEFAULT_GID,
"node_id": None,
"state": constants.DEVICE_STATES['FREE']
}
db_api.Connection.update_device(device_uuid, update_dev_info)
def _handle_exceptions(self, response):
"""Handles exceptions from http requests
:param response: output of the request
:raises AuthorizationFailure: if invalid credentials are passed
ExpEtherException: if any HTTPError occurs
"""
if response.status_code == 401:
raise exception.AuthorizationFailure("Invalid credentials passed")
try:
resp = response.json()
response.raise_for_status()
except requests.exceptions.HTTPError:
detail = resp['message']
code = resp['code']
LOG.exception(detail)
raise exception.ExpEtherException(code=code, detail=detail,
status=response.status_code)
def _set_gid(self, device_id, group_id):
"""Updates the group id of the device
Set the group id of EEIO device as of EESV to which it is going to
connect.
:param device_id: the device id on which group id is to updated
:param group_id: group_id which is to be assigned
:raises ExpEtherException if any HTTPError occurs
"""
return self._send_request_to_eem("devices/" + device_id + "/group_id",
'put', json={"group_id": group_id})
def _del_gid(self, device_id):
"""Deletes the group id of the device
Sends delete request to the url of device, which updates
group id of device to 4093
:param device_id: of device which is to be detached from node
:raises ExpEtherException if any HTTPError occurs
"""
return self._send_request_to_eem("devices/" + device_id + "/group_id",
'delete')
def _get_device_type(self, pci_code):
"""Gives device type based on its PCI code
:param pci_code: PCI code of device from eem
:return: type of device(e.g SSD, NIC)
"""
class_code = int(pci_code, 0)
c0 = class_code / (256 * 256)
if c0 == type_NIC:
return 'NIC'
elif c0 == type_SSD:
return 'SSD'
elif c0 == type_GPU:
return 'GPU'
elif c0 == type_MULTI:
return 'Multi'
elif c0 == type_SERIAL: # and ccode[1] == type_USB:
return 'USB'
elif c0 == type_ACS:
return 'ACS'
elif c0 == type_SWITCH:
return 'SWITCH'
else:
return 'Unknown'
def _detach_all_devices_from_node(self, node_id):
"""Detaches all devices from the node
Fetches all connected devices to node, deletes group_id
:param node_id: index of the node
"""
db_dev_list = db_api.Connection.list_devices(
filters={'node_id': node_id})
for db_device in db_dev_list:
self.detach(db_device)
def get_status(self):
"""Checks ExpEtherManager Status
Issues command to check version of EEM.
:return: on or off status of pod_manager
:raises AuthorizationFailure: if wrong credentials are passed
ExpEtherException: if any HTTPError
"""
error_message = "unable to reach podmanager at url: {} with error: {}"
try:
self._send_request_to_eem('api_version')
return constants.PODM_STATUS_ONLINE
# check for wrong ip and offline status
except exception.AuthorizationFailure as e:
raise exception.AuthorizationFailure(
detail=error_message.format(self.url, e.detail))
except exception.ExpEtherException as e:
raise exception.ExpEtherException(
code=e.code, detail=error_message.format(self.url, e.detail),
status=e.status)
def _system_dict(self, device):
"""Converts the resource details into desired dictionary format
:param device: resource data from eem
:return: Dictionary in desired format
"""
system = {'id': device['id'],
'resource_uri': 'devices/' + device['id'],
'pooled_group_id': device['group_id'],
'type': device['type'],
'mac_address': device['mac_address'],
'host_serial_num': device['host_serial_number'],
'host_model': device['host_model']
}
return system
def _get_device_info(self, vendor_id, device_id):
"""Calculates vendor and device name
Using python-hwdata retrieve information of 40G devices
:param vendor_id: field 'pcie_vendor_id' value from EEM
:param device_id: field 'pcie_device_id' value from EEM
:return: Vendor and device name
"""
vendor_name = ''
device_name = ''
if not hwdata:
LOG.warning("hwdata module not available, unable to get the"
"vendor details")
return vendor_name, device_name
vendor_id = hex(int(vendor_id, 16))[2:].zfill(4)
device_id = hex(int(device_id, 16))[2:].zfill(4)
pci = hwdata.PCI()
vendor_name = pci.get_vendor(vendor_id)
device_name = pci.get_device(vendor_id, device_id)
return vendor_name, device_name
def get_all_devices(self):
"""Get all eeio devices connected to eem."""
devices = []
eeios = self._send_request_to_eem(query="devices/detail?status=eeio")
for eeio in eeios['devices']:
if eeio['notification_status0'][0] == 'down':
continue
extra = dict()
state, eesv_id = self._check_eeio_state(eeio['group_id'])
# If 40g, retreive device details, else None in case of 10g
device_type = None
if eeio["type"] == '40g':
device_type = self._get_device_type(eeio["pcie_class_code"])
vendor_name, device_name = self._get_device_info(
eeio["pcie_vendor_id"], eeio["pcie_device_id"])
extra['vendor_name'] = vendor_name
extra['device_name'] = device_name
properties = dict()
properties['device_id'] = eeio['id']
properties['mac_address'] = eeio['mac_address']
properties['model'] = eeio['type']
values = {"type": device_type,
"pooled_group_id": eeio['group_id'],
"node_id": eesv_id,
"resource_uri": 'devices/' + eeio['id'],
"state": state,
"extra": extra,
"properties": properties,
}
devices.append(values)
return devices
def _check_eeio_state(self, group_id):
"""Checks if eeio device is free or allocated to node
:param group_id: pooled group id of device
:return: state i.e allocated or free and eesv_id if allocated
"""
state = constants.DEVICE_STATES['FREE']
eesv_id = None
if group_id != EEIO_DEFAULT_GID:
state = constants.DEVICE_STATES['ALLOCATED']
query = "devices?status=eesv&group_id=" + group_id
result = self._send_request_to_eem(query)['devices']
if result:
eesv_id = result[0]['id']
return state, eesv_id
def get_ironic_node_params(self, node_info, **param):
"""Get ironic node params to register to ironic
:param node_info: eesv node info
:param param:
Eg:{"driver_info":
{"ipmi_address": "xxx.xxx.xx.xx",
"ipmi_username": "xxxxx",
"ipmi_password": "xxxxx"},
"mac": "11:11:11:11:11:11",
"driver": "agent_ipmitool"}
:return: node and port arguments
"""
driver = param.pop('driver', 'pxe_ipmitool')
if not param.get('driver_info'):
raise exception.ExpEtherException(
detail='Missing driver_info in params %s' % node_info['uuid'],
status=400)
driver_info = param.pop('driver_info')
node_args = {'name': node_info['name'],
'driver': driver,
'driver_info': driver_info}
if param.get('mac', None):
# MAC provided, create ironic ports
port_args = {'address': param['mac']}
return node_args, port_args
@staticmethod
def _convert_time_format(timestamp):
"""Convert time elapsed since 1/1/1970 to readable format
:param timestamp: Update time from eem device
:return: readable time format
"""
value = int(timestamp)/1000.0
return datetime.datetime.utcfromtimestamp(value).strftime(
'%Y-%m-%d %H:%M:%S')

View File

@ -33,7 +33,7 @@ class PodManagerBase(object):
return self.get_resource_info_by_url(self.podm_url)
# TODO(): use rsd_lib here
def compose_node(self, request_body):
def compose_node(self, name, description, requirements):
pass
# TODO(): use rsd_lib here

View File

@ -481,15 +481,49 @@ def build_hierarchy_tree():
return podmtree
def compose_node(request_body):
def _create_compose_request(name, description, requirements):
"""Generate compose node request following podm format
:param name: name of node
:param description: description of node if any
:param requirements: additional requirements of node if any
:return: request body to compose node
"""
request = {}
request["Name"] = name
request["Description"] = description
memory = {}
if "memory" in requirements:
if "capacity_mib" in requirements["memory"]:
memory["CapacityMiB"] = requirements["memory"]["capacity_mib"]
if "type" in requirements["memory"]:
memory["DimmDeviceType"] = requirements["memory"]["type"]
request["Memory"] = [memory]
processor = {}
if "processor" in requirements:
if "model" in requirements["processor"]:
processor["Model"] = requirements["processor"]["model"]
if "total_cores" in requirements["processor"]:
processor["TotalCores"] = (
requirements["processor"]["total_cores"])
request["Processors"] = [processor]
return request
def compose_node(name, description, requirements):
"""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.
:param name: name of node
:param description: description of node if any
:param requirements: additional requirements of node if any
:returns: The numeric index of new composed node.
"""
request_body = _create_compose_request(name, description, requirements)
# Get url of allocating resource to node
nodes_url = get_base_resource_url('Nodes')
resp = send_request(nodes_url, 'GET')

View File

@ -47,37 +47,6 @@ class TestAPINodes(unittest.TestCase):
self.assertEqual(expected,
nodes.Node._show_node_brief_info(node_info))
def test_create_compose_request(self):
name = "test_request"
description = "request for testing purposes"
requirements = {
"memory": {
"capacity_mib": "4000",
"type": "DDR3"
},
"processor": {
"model": "Intel",
"total_cores": "4"
}
}
expected = {
"Name": "test_request",
"Description": "request for testing purposes",
"Memory": [{
"CapacityMiB": "4000",
"DimmDeviceType": "DDR3"
}],
"Processors": [{
"Model": "Intel",
"TotalCores": "4"
}]
}
result = nodes.Node._create_compose_request(name,
description,
requirements)
self.assertEqual(expected, result)
@mock.patch("valence.db.api.Connection.create_composed_node")
@mock.patch("valence.common.utils.generate_uuid")
@mock.patch("valence.controller.nodes.Node.list_composed_nodes")
@ -119,7 +88,8 @@ class TestAPINodes(unittest.TestCase):
@mock.patch("valence.db.api.Connection.create_composed_node")
@mock.patch("valence.common.utils.generate_uuid")
@mock.patch("valence.podmanagers.podm_base.PodManagerBase.compose_node")
def test_compose_node(self, mock_redfish_compose_node, mock_generate_uuid,
def test_compose_node(self, mock_redfish_compose_node,
mock_generate_uuid,
mock_db_create_composed_node):
"""Test compose node successfully"""
node_hw = node_fakes.get_test_composed_node()
@ -129,13 +99,13 @@ class TestAPINodes(unittest.TestCase):
"name": node_hw["name"],
"resource_uri": node_hw["resource_uri"]}
compose_request = {'name': 'fake_name',
'description': 'fake_description'}
mock_redfish_compose_node.return_value = node_hw
uuid = 'ea8e2a25-2901-438d-8157-de7ffd68d051'
mock_generate_uuid.return_value = uuid
result = self.node_controller.compose_node(
{"name": node_hw["name"],
"description": node_hw["description"]})
result = self.node_controller.compose_node(compose_request)
expected = nodes.Node._show_node_brief_info(node_hw)
self.assertEqual(expected, result)

View File

@ -184,7 +184,7 @@ class TestPooledDevices(unittest.TestCase):
mock_device_list,
mock_pod_conn):
mock_device_list.return_value = [fakes.fake_device()]
mock_pod_conn.side_effect = exception.ValenceException('fake_detail')
mock_pod_conn.side_effect = exception.ValenceError('fake_detail')
result = pooled_devices.PooledDevices.update_device_info('podm_id')
expected = {'podm_id': 'podm_id', 'status': 'FAILED'}
self.assertEqual(result, expected)

View File

@ -0,0 +1,40 @@
def fake_eesv_list():
return {"devices": [{"id": "0x1111111111",
"status": "eesv",
"update_time": "1518999910510",
"mac_address": "11:11:11:11:11:11",
"group_id": "1234",
"type": "40g",
"power_status": "on",
"ee_version": "v1.0",
"device_id": "0x00000",
"serial_number": "abcd 01234",
"model": "ExpEther Board (40G)",
"max_eeio_count": "16",
"host_serial_number": "",
"host_model": "",
"notification_status0": ["up", "down"],
"notification_status1": ["down", "down"]},
{"id": "0x2222222222",
"status": "eesv",
"update_time": "1518999910510",
"mac_address": "22:22:22:22:22:22",
"group_id": "5678",
"type": "10g",
"power_status": "on",
"ee_version": "v1.0",
"device_id": "0x00000",
"serial_number": "abcd 01234",
"model": "ExpEther Board (10G)",
"max_eeio_count": "8",
"host_serial_number": "",
"host_model": "",
"notification_status0": ["up", "down"],
"notification_status1": ["down", "down"]}
],
"timestamp": "1521089295162"}
def fake_eesv():
return {"device": fake_eesv_list()['devices'][0],
"timestamp": "1521089295162"}

View File

@ -25,6 +25,9 @@ def fake_flavor():
"processor": {
"total_cores": "2",
"model": "Intel"
},
"pci_device": {
"type": ["SSD", "NIC"]
}
}
}
@ -47,6 +50,9 @@ def fake_flavor_list():
"processor": {
"total_cores": "10",
"model": "Intel"
},
"pci_device": {
"type": ["NIC"]
}
}
},
@ -61,6 +67,9 @@ def fake_flavor_list():
"processor": {
"total_cores": "20",
"model": "Intel"
},
"pci_device": {
"type": ["SSD"]
}
}
},
@ -75,6 +84,9 @@ def fake_flavor_list():
"processor": {
"total_cores": "30",
"model": "Intel"
},
"pci_device": {
"type": ["SSD", "NIC"]
}
}
}

View File

@ -0,0 +1,392 @@
# Copyright (c) 2017 NEC, Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import unittest
import etcd
import mock
from valence.common import exception
from valence.podmanagers import expether_manager
from valence.tests.unit.fakes import device_fakes
from valence.tests.unit.fakes import expether_fakes
from valence.tests.unit.fakes import node_fakes
class TestExpEtherManager(unittest.TestCase):
def setUp(self):
self.expether_manager = expether_manager.ExpEtherManager(
'username', 'password', 'http://fake_url')
@mock.patch('valence.db.api.Connection.list_composed_nodes')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_compose_node(self, mock_eesv_list, mock_list_composed_node):
mock_eesv_list.return_value = {"devices": [{"id": "1"}, {"id": "2"},
{"id": "3"}, {"id": "4"}],
"timestamp": "1520845301785"}
mock_list_composed_node.return_value = node_fakes.get_test_node_list()
result = self.expether_manager.compose_node('node4', '', {})
expected = {'name': 'node4', 'index': '4', 'resource_uri': 'devices/4'}
self.assertEqual(result, expected)
@mock.patch('valence.db.api.Connection.list_composed_nodes')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_compose_node_no_free_eesv(self, mock_eesv_list, mock_db_list):
mock_eesv_list.return_value = {"devices": [{"id": "1"}, {"id": "2"},
{"id": "3"}],
"timestamp": "1520845301785"}
mock_db_list.return_value = node_fakes.get_test_node_list()
self.assertRaises(exception.ExpEtherException,
self.expether_manager.compose_node,
'node4', '', {})
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.attach')
@mock.patch('valence.db.api.Connection.list_devices')
@mock.patch('valence.db.api.Connection.list_composed_nodes')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_compose_node_with_attach_pci_device(self, mock_eesv_list,
mock_list_composed_node,
mock_list_devices,
mock_attach):
mock_eesv_list.return_value = {'devices': [{'id': '1'}, {'id': '2'},
{'id': '3'}, {'id': '4'}],
'timestamp': '1520845301785'}
mock_list_composed_node.return_value = node_fakes.get_test_node_list()
db_device = device_fakes.fake_device_list()[1]
db_device2 = copy.deepcopy(db_device)
db_device2['type'] = 'SSD'
mock_list_devices.side_effect = [[db_device], [db_device2]]
result = self.expether_manager.compose_node(
'node4', '', {'pci_device': {'type': ['NIC', 'SSD']}})
expected = {'name': 'node4', 'index': '4',
'resource_uri': 'devices/4'}
self.assertEqual(result, expected)
self.assertEqual(mock_attach.call_count, 2)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._send_request_to_eem')
def test_get_node_info(self, mock_get_eesv):
mock_get_eesv.return_value = expether_fakes.fake_eesv()
result = self.expether_manager.get_node_info('0x1111111111')
expected = {'name': '0x1111111111',
'resource_uri': 'devices/0x1111111111',
'serial_number': 'abcd 01234',
'power_state': 'on',
'host_model': '',
'host_serial_number': '',
'index': '0x1111111111',
'description': 'ExpEther Board (40G)',
'type': '40g',
'mac_address': '11:11:11:11:11:11',
'pooled_group_id': '1234'
}
self.assertEqual(result, expected)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._send_request_to_eem')
def test_get_node_info_with_invalid_node(self, mock_get_eesv):
eesv_info = expether_fakes.fake_eesv()
eesv_info['device']['status'] = 'eeio'
mock_get_eesv.return_value = eesv_info
self.assertRaises(exception.ExpEtherException,
self.expether_manager.get_node_info, '0x1111111111')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._detach_all_devices_from_node')
def test_delete_composed_node(self, mock_detach_devices):
self.expether_manager.delete_composed_node('node_id')
mock_detach_devices.assert_called_once_with('node_id')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._detach_all_devices_from_node')
def test_delete_composed_node_with_exception(self, mock_detach_devices):
mock_detach_devices.side_effect = exception.ExpEtherException()
self.assertRaises(exception.ExpEtherException,
self.expether_manager.delete_composed_node,
'node_id')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.attach')
@mock.patch('valence.db.api.Connection.get_device_by_uuid')
def test_node_action_attach(self, mock_db_device, mock_attach):
device_obj = device_fakes.fake_device_obj()
mock_db_device.return_value = device_obj
request_body = {"attach": {
"resource_id": "00000000-0000-0000-0000-000000000000"}}
self.expether_manager.node_action('0x12345', request_body)
mock_attach.assert_called_once_with(device_obj.as_dict(), '0x12345')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.detach')
@mock.patch('valence.db.api.Connection.get_device_by_uuid')
def test_node_action_detach(self, mock_db_device, mock_detach):
device_obj = device_fakes.fake_device_obj()
mock_db_device.return_value = device_obj
request_body = {"detach": {
"resource_id": "00000000-0000-0000-0000-000000000000"}}
self.expether_manager.node_action('0x12345', request_body)
mock_detach.assert_called_once_with(device_obj.as_dict(), '0x12345')
def test_node_action_with_unsupported_action(self):
request_body = {"run": {
"resource_id": "00000000-0000-0000-0000-000000000000"}}
self.assertRaises(exception.BadRequest,
self.expether_manager.node_action,
'0x12345', request_body)
@mock.patch('etcd.Client.read')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.attach')
def test_node_action_with_non_existing_resource(self, mock_attach,
mock_etcd_read):
mock_etcd_read.side_effect = etcd.EtcdKeyNotFound
self.assertRaises(exception.NotFound,
self.expether_manager.node_action,
'0x12345', {"attach": {"resource_id": "000-00000"}})
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_system_list(self, mock_eesv_list):
mock_eesv_list.return_value = expether_fakes.fake_eesv_list()
result = self.expether_manager.systems_list()
expected = [{'id': '0x1111111111',
'resource_uri': 'devices/0x1111111111',
'pooled_group_id': '1234',
'type': '40g',
'mac_address': '11:11:11:11:11:11',
'host_model': '',
'host_serial_num': '',
},
{'id': '0x2222222222',
'resource_uri': 'devices/0x2222222222',
'pooled_group_id': '5678',
'type': '10g',
'mac_address': '22:22:22:22:22:22',
'host_model': '',
'host_serial_num': '',
}]
self.assertEqual(result, expected)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_system_list_with_exception(self, mock_eesv_list):
mock_eesv_list.side_effect = exception.ExpEtherException
self.assertRaises(exception.ExpEtherException,
self.expether_manager.systems_list)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_get_system_by_id(self, mock_eesv):
mock_eesv.return_value = expether_fakes.fake_eesv()
result = self.expether_manager.get_system_by_id('0x1111111111')
expected = {'id': '0x1111111111',
'type': '40g',
'pooled_group_id': '1234',
'mac_address': '11:11:11:11:11:11',
'serial_number': 'abcd 01234',
'name': 'eesv',
'power_state': 'on',
'host_model': '',
'host_serial_number': '',
'description': 'ExpEther Board (40G)',
'ee_version': 'v1.0',
'update_time': '2018-02-19 00:25:10'
}
self.assertEqual(result, expected)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_get_system_by_id_with_invalid_system(self, mock_eesv):
system = expether_fakes.fake_eesv()
system['device']['status'] = 'eeio'
mock_eesv.return_value = system
self.assertRaises(exception.ExpEtherException,
self.expether_manager.get_system_by_id,
'0x1111111111')
@mock.patch('valence.db.api.Connection.update_device')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_attach(self, mock_req_to_eem, mock_update_device):
mock_req_to_eem.side_effect = [expether_fakes.fake_eesv(),
{"devices": [{"id": "0x8cdf9d911cb0"},
{"id": "0x8cdf9d53e9d8"}],
"timestamp": "1521117379726"}, None]
device = device_fakes.fake_device()
device['pooled_group_id'] = '4093'
self.expether_manager.attach(device, '0x1111111111')
mock_update_device.assert_called_once_with(
device['uuid'], {'pooled_group_id': '1234',
'node_id': '0x1111111111',
'state': 'allocated'})
def test_attach_with_device_already_attached_to_node(self):
device = device_fakes.fake_device()
device['node_id'] = 'fake_node_id'
device['pooled_group_id'] = '1234'
self.assertRaises(exception.BadRequest, self.expether_manager.attach,
device, '0x1111111111')
def test_attach_with_device_with_non_default_gid(self):
device = device_fakes.fake_device()
device['pooled_group_id'] = '1234'
self.assertRaises(exception.BadRequest, self.expether_manager.attach,
device, '0x1111111111')
@mock.patch('valence.db.api.Connection.update_device')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._set_gid')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_attach_to_node_with_default_gid(self, mock_req_to_eem,
mock_set_gid,
mock_update_device):
device = device_fakes.fake_device()
device['pooled_group_id'] = '4093'
eesv = expether_fakes.fake_eesv()
eesv['device']['group_id'] = '4094'
mock_req_to_eem.side_effect = [eesv,
{"devices": [{"id": "0x8cdf9d911cb0"},
{"id": "0x8cdf9d53e9d8"}],
"timestamp": "1521117379726"}]
mock_set_gid.return_value = {'group_id': '678'}
self.expether_manager.attach(device, '0x1111111111')
mock_set_gid.assert_any_call(device_id='0x1111111111', group_id=None)
mock_set_gid.assert_any_call(device_id='0x7777777777', group_id='678')
@mock.patch('valence.db.api.Connection.update_device')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_attach_to_node_with_max_devices_attached(self, mock_req_to_eem,
mock_update_device):
device = device_fakes.fake_device()
device['pooled_group_id'] = '4093'
eesv = expether_fakes.fake_eesv()
eesv['device']['max_eeio_count'] = '2'
mock_req_to_eem.side_effect = [eesv,
{"devices": [{"id": "0x8cdf9d911cb0"},
{"id": "0x8cdf9d53e9d8"}],
"timestamp": "1521117379726"}]
self.assertRaises(exception.ExpEtherException,
self.expether_manager.attach,
device, '0x1111111111')
@mock.patch('valence.db.api.Connection.update_device')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager'
'._del_gid')
def test_detach(self, mock_del_gid, mock_update_device):
device = device_fakes.fake_device()
self.expether_manager.detach(device)
mock_del_gid.assert_called_once_with(device['properties']['device_id'])
mock_update_device.assert_called_once_with(device['uuid'],
{'pooled_group_id': '4093',
'node_id': None,
'state': 'free'})
def test_detach_with_device_already_detached(self):
device = device_fakes.fake_device()
device['pooled_group_id'] = '4093'
device['node_id'] = None
self.expether_manager.detach(device)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test__set_gid(self, mock_request_to_eem):
self.expether_manager._set_gid('dev_id', 'gid')
mock_request_to_eem.assert_called_once_with('devices/dev_id/group_id',
'put',
json={'group_id': 'gid'})
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test__del_gid(self, mock_request_to_eem):
self.expether_manager._del_gid('dev_id')
mock_request_to_eem.assert_called_once_with('devices/dev_id/group_id',
'delete')
def test__get_device_type(self):
result = self.expether_manager._get_device_type('0x20000')
self.assertEqual(result, 'NIC')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.detach')
@mock.patch('valence.db.api.Connection.list_devices')
def test__detach_all_devices_from_node(self, mock_db_devices, mock_detach):
dev_list = device_fakes.fake_device_list()
mock_db_devices.return_value = dev_list
self.expether_manager._detach_all_devices_from_node('node_id')
mock_detach.assert_has_calls([mock.call(dev_list[0]),
mock.call(dev_list[1])])
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_get_status(self, mock_request_to_eem):
result = self.expether_manager.get_status()
self.assertEqual(result, 'Online')
mock_request_to_eem.assert_called_once_with('api_version')
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_get_status_with_auth_exception(self, mock_request_to_eem):
mock_request_to_eem.side_effect = exception.AuthorizationFailure
self.assertRaises(exception.AuthorizationFailure,
self.expether_manager.get_status)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test_get_status_with_expether_exception(self, mock_request_to_eem):
mock_request_to_eem.side_effect = exception.ExpEtherException
self.assertRaises(exception.ExpEtherException,
self.expether_manager.get_status)
def test__system_dict(self):
result = self.expether_manager._system_dict(
expether_fakes.fake_eesv()['device'])
expected = {'id': '0x1111111111',
'resource_uri': 'devices/0x1111111111',
'pooled_group_id': '1234',
'type': '40g',
'mac_address': '11:11:11:11:11:11',
'host_serial_num': '',
'host_model': ''
}
self.assertEqual(result, expected)
@mock.patch('valence.podmanagers.expether_manager.ExpEtherManager.'
'_send_request_to_eem')
def test__check_eeio_state(self, mock_request_to_eem):
mock_request_to_eem.return_value = {"devices": [{"id": "012345678"}],
"timestamp": "1521178770450"}
state, eesv_id = self.expether_manager._check_eeio_state('1234')
self.assertEqual(state, 'allocated')
self.assertEqual(eesv_id, '012345678')
def test_get_ironic_node_params(self):
request_body = {"driver_info": {"ipmi_address": "xxx.xxx.xx.xx",
"ipmi_username": "xxxxx",
"ipmi_password": "xxxxx"},
"mac": "11:11:11:11:11:11",
"driver": "agent_ipmitool"}
node, port = self.expether_manager.get_ironic_node_params(
{'name': 'node1'}, **request_body)
expected_node = {'name': 'node1',
'driver': 'agent_ipmitool',
'driver_info': request_body['driver_info']}
self.assertEqual(node, expected_node)
self.assertEqual(port, {'address': '11:11:11:11:11:11'})
def test__convert_time_format(self):
result = self.expether_manager._convert_time_format('1518999910510')
self.assertEqual(result, '2018-02-19 00:25:10')

View File

@ -345,7 +345,7 @@ class TestRedfish(TestCase):
fake_node_allocation_conflict]
with self.assertRaises(exception.RedfishException) as context:
redfish.compose_node({"name": "test_node"})
redfish.compose_node('test_node', '', {})
self.assertTrue("There are no computer systems available for this "
"allocation request." in str(context.exception.detail))
@ -381,7 +381,7 @@ class TestRedfish(TestCase):
fake_node_assemble_failed]
with self.assertRaises(exception.RedfishException):
redfish.compose_node({"name": "test_node"})
redfish.compose_node('test_node', '', {})
mock_delete_node.assert_called_once()
@ -416,7 +416,7 @@ class TestRedfish(TestCase):
fake_node_detail,
fake_node_assemble_failed]
redfish.compose_node({"name": "test_node"})
redfish.compose_node('test_node', '', {})
mock_delete_node.assert_not_called()
mock_get_node_by_id.assert_called_once()
@ -666,3 +666,33 @@ class TestRedfish(TestCase):
]
result = redfish.show_rack("2")
self.assertEqual(expected, result)
def test__create_compose_request(self):
name = "test_request"
description = "request for testing purposes"
requirements = {
"memory": {
"capacity_mib": "4000",
"type": "DDR3"
},
"processor": {
"model": "Intel",
"total_cores": "4"
}
}
expected = {
"Name": "test_request",
"Description": "request for testing purposes",
"Memory": [{
"CapacityMiB": "4000",
"DimmDeviceType": "DDR3"
}],
"Processors": [{
"Model": "Intel",
"TotalCores": "4"
}]
}
result = redfish._create_compose_request(name, description,
requirements)
self.assertEqual(expected, result)

View File

@ -38,6 +38,13 @@ flavor_schema = {
},
'additionalProperties': False,
},
'pci_device': {
'type': 'object',
'properties': {
'type': {'type': 'array'}
},
'additionalProperties': False,
},
},
'additionalProperties': False,
},
@ -122,6 +129,13 @@ compose_node_with_properties = {
},
'additionalProperties': False,
},
'pci_device': {
'type': 'object',
'properties': {
'type': {'type': 'array'}
},
'additionalProperties': False,
},
},
'additionalProperties': False,
},