From a5cc27bf4144365928abca51b3f4f364406d84cc Mon Sep 17 00:00:00 2001 From: akhiljain23 Date: Fri, 16 Mar 2018 16:58:43 +0530 Subject: [PATCH] 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 --- setup.cfg | 1 + valence/common/constants.py | 4 + valence/common/exception.py | 4 + valence/common/http_adapter.py | 71 +++ valence/controller/nodes.py | 34 +- valence/controller/podmanagers.py | 8 +- valence/controller/pooled_devices.py | 4 +- valence/db/models.py | 6 + valence/podmanagers/expether_manager.py | 537 ++++++++++++++++++ valence/podmanagers/podm_base.py | 2 +- valence/redfish/redfish.py | 42 +- valence/tests/unit/controller/test_nodes.py | 40 +- .../unit/controller/test_pooled_devices.py | 2 +- valence/tests/unit/fakes/expether_fakes.py | 40 ++ valence/tests/unit/fakes/flavor_fakes.py | 12 + .../unit/podmanagers/test_expether_manager.py | 392 +++++++++++++ valence/tests/unit/redfish/test_redfish.py | 36 +- valence/validation/schemas.py | 14 + 18 files changed, 1172 insertions(+), 77 deletions(-) create mode 100644 valence/common/http_adapter.py create mode 100644 valence/podmanagers/expether_manager.py create mode 100644 valence/tests/unit/fakes/expether_fakes.py create mode 100644 valence/tests/unit/podmanagers/test_expether_manager.py diff --git a/setup.cfg b/setup.cfg index 0cc3e3a..0e817c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,3 +68,4 @@ valence.provision.driver = valence.podmanager.driver = redfishv1 = valence.podmanagers.podm_base:PodManagerBase + expether = valence.podmanagers.expether_manager:ExpEtherManager diff --git a/valence/common/constants.py b/valence/common/constants.py index 1ad3d67..7a2f02b 100644 --- a/valence/common/constants.py +++ b/valence/common/constants.py @@ -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'} diff --git a/valence/common/exception.py b/valence/common/exception.py index c088839..2d416a4 100644 --- a/valence/common/exception.py +++ b/valence/common/exception.py @@ -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" diff --git a/valence/common/http_adapter.py b/valence/common/http_adapter.py new file mode 100644 index 0000000..6fad000 --- /dev/null +++ b/valence/common/http_adapter.py @@ -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 diff --git a/valence/controller/nodes.py b/valence/controller/nodes.py index efad262..10b4f95 100644 --- a/valence/controller/nodes.py +++ b/valence/controller/nodes.py @@ -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, diff --git a/valence/controller/podmanagers.py b/valence/controller/podmanagers.py index 7cdedf1..e790755 100644 --- a/valence/controller/podmanagers.py +++ b/valence/controller/podmanagers.py @@ -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) diff --git a/valence/controller/pooled_devices.py b/valence/controller/pooled_devices.py index 0fccd8e..eff887b 100644 --- a/valence/controller/pooled_devices.py +++ b/valence/controller/pooled_devices.py @@ -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 diff --git a/valence/db/models.py b/valence/db/models.py index a45cc24..d2b8c8f 100644 --- a/valence/db/models.py +++ b/valence/db/models.py @@ -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 } } diff --git a/valence/podmanagers/expether_manager.py b/valence/podmanagers/expether_manager.py new file mode 100644 index 0000000..912a3dd --- /dev/null +++ b/valence/podmanagers/expether_manager.py @@ -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') diff --git a/valence/podmanagers/podm_base.py b/valence/podmanagers/podm_base.py index e131628..5029201 100644 --- a/valence/podmanagers/podm_base.py +++ b/valence/podmanagers/podm_base.py @@ -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 diff --git a/valence/redfish/redfish.py b/valence/redfish/redfish.py index fe2f4e8..c33808e 100644 --- a/valence/redfish/redfish.py +++ b/valence/redfish/redfish.py @@ -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') diff --git a/valence/tests/unit/controller/test_nodes.py b/valence/tests/unit/controller/test_nodes.py index db863a4..8e753f0 100644 --- a/valence/tests/unit/controller/test_nodes.py +++ b/valence/tests/unit/controller/test_nodes.py @@ -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) diff --git a/valence/tests/unit/controller/test_pooled_devices.py b/valence/tests/unit/controller/test_pooled_devices.py index fbe79ac..c316ae1 100644 --- a/valence/tests/unit/controller/test_pooled_devices.py +++ b/valence/tests/unit/controller/test_pooled_devices.py @@ -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) diff --git a/valence/tests/unit/fakes/expether_fakes.py b/valence/tests/unit/fakes/expether_fakes.py new file mode 100644 index 0000000..221bbbe --- /dev/null +++ b/valence/tests/unit/fakes/expether_fakes.py @@ -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"} diff --git a/valence/tests/unit/fakes/flavor_fakes.py b/valence/tests/unit/fakes/flavor_fakes.py index d0556a4..1aeba0f 100644 --- a/valence/tests/unit/fakes/flavor_fakes.py +++ b/valence/tests/unit/fakes/flavor_fakes.py @@ -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"] } } } diff --git a/valence/tests/unit/podmanagers/test_expether_manager.py b/valence/tests/unit/podmanagers/test_expether_manager.py new file mode 100644 index 0000000..7fd4750 --- /dev/null +++ b/valence/tests/unit/podmanagers/test_expether_manager.py @@ -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') diff --git a/valence/tests/unit/redfish/test_redfish.py b/valence/tests/unit/redfish/test_redfish.py index f067b91..18430d2 100644 --- a/valence/tests/unit/redfish/test_redfish.py +++ b/valence/tests/unit/redfish/test_redfish.py @@ -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) diff --git a/valence/validation/schemas.py b/valence/validation/schemas.py index 58837a2..989678f 100644 --- a/valence/validation/schemas.py +++ b/valence/validation/schemas.py @@ -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, },