diff --git a/delfin/drivers/pure/__init__.py b/delfin/drivers/pure/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/delfin/drivers/pure/flasharray/__init__.py b/delfin/drivers/pure/flasharray/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/delfin/drivers/pure/flasharray/consts.py b/delfin/drivers/pure/flasharray/consts.py new file mode 100644 index 000000000..7226a1c9f --- /dev/null +++ b/delfin/drivers/pure/flasharray/consts.py @@ -0,0 +1,96 @@ +# Copyright 2022 The SODA Authors. +# +# 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. +from delfin.common import constants + +# The default volume +DEFAULT_CAPACITY = 0 + +# The default speed +DEFAULT_SPEED = 0 + +# The default list_alerts time conversion +DEFAULT_LIST_ALERTS_TIME_CONVERSION = 1000 + +# The default count for the get_volumes_info function +DEFAULT_COUNT_GET_VOLUMES_INFO = 0 + +# Number of re-logins +RE_LOGIN_TIMES = 3 + +# Constant one +CONSTANT_ONE = 1 +# Constant zero +CONSTANT_ZERO = 0 + +# Success status code +SUCCESS_STATUS_CODE = 200 + +# Status code of no permission +PERMISSION_DENIED_STATUS_CODE = 401 + +# Custom token of Pure +CUSTOM_TOKEN = 'x-next-token' + +# The default get_storage model +CONTROLLER_PRIMARY = 'primary' + +# Normal value of the controller status +NORMAL_CONTROLLER_STATUS = 'ready' + +# disk type +DISK_TYPE_NVRAM = 'NVRAM' + +# The account password is incorrect during login. +LOGIN_PASSWORD_ERR = 'invalid credentials' + +# list_port: Add ":" to the WWN every 2 sequences. +SPLICE_WWN_SERIAL = 2 +SPLICE_WWN_COLON = ':' + +SEVERITY_MAP = {'fatal': constants.Severity.FATAL, + 'critical': constants.Severity.CRITICAL, + 'major': constants.Severity.MAJOR, + 'minor': constants.Severity.MINOR, + 'warning': constants.Severity.WARNING, + 'informational': constants.Severity.INFORMATIONAL, + 'NotSpecified': constants.Severity.NOT_SPECIFIED} +CATEGORY_MAP = {'fault': constants.Category.FAULT, + 'event': constants.Category.EVENT, + 'recovery': constants.Category.RECOVERY, + 'notSpecified': constants.Category.NOT_SPECIFIED} +CONTROLLER_STATUS_MAP = {'normal': constants.ControllerStatus.NORMAL, + 'ok': constants.ControllerStatus.NORMAL, + 'offline': constants.ControllerStatus.OFFLINE, + 'not_installed': constants.ControllerStatus.OFFLINE, + 'fault': constants.ControllerStatus.FAULT, + 'degraded': constants.ControllerStatus.DEGRADED, + 'unready': constants.ControllerStatus.UNKNOWN} +DISK_STATUS_MAP = {'normal': constants.DiskStatus.NORMAL, + 'healthy': constants.DiskStatus.NORMAL, + 'abnormal': constants.DiskStatus.ABNORMAL, + 'unhealthy': constants.DiskStatus.ABNORMAL, + 'offline': constants.DiskStatus.OFFLINE} +PORT_STATUS_MAP = {'ok': constants.PortHealthStatus.NORMAL, + 'not_installed': constants.PortHealthStatus.ABNORMAL + } + +PARSE_ALERT_ALERT_ID = '1.3.6.1.2.1.1.3.0' +PARSE_ALERT_STORAGE_NAME = '1.3.6.1.4.1.40482.3.1' +PARSE_ALERT_CONTROLLER_NAME = '1.3.6.1.4.1.40482.3.3' +PARSE_ALERT_ALERT_NAME = '1.3.6.1.4.1.40482.3.5' +PARSE_ALERT_DESCRIPTION = '1.3.6.1.4.1.40482.3.6' +PARSE_ALERT_SEVERITY = '1.3.6.1.4.1.40482.3.7' + +PARSE_ALERT_SEVERITY_MAP = {'1': constants.Severity.WARNING, + '2': constants.Severity.INFORMATIONAL} diff --git a/delfin/drivers/pure/flasharray/pure_flasharray.py b/delfin/drivers/pure/flasharray/pure_flasharray.py new file mode 100644 index 000000000..92feb48cf --- /dev/null +++ b/delfin/drivers/pure/flasharray/pure_flasharray.py @@ -0,0 +1,364 @@ +# Copyright 2022 The SODA Authors. +# +# 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 hashlib +import time + +from oslo_log import log + +from delfin import exception, utils +from delfin.common import constants +from delfin.drivers import driver +from delfin.drivers.pure.flasharray import rest_handler, consts +from delfin.i18n import _ + +LOG = log.getLogger(__name__) + + +class PureFlashArrayDriver(driver.StorageDriver): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rest_handler = rest_handler.RestHandler(**kwargs) + self.rest_handler.login() + + def list_volumes(self, context): + list_volumes = [] + volumes = self.rest_handler.get_volumes() + if volumes: + for volume in volumes: + volume_name = volume.get('name') + total_capacity = int(volume.get('size', + consts.DEFAULT_CAPACITY)) + used_capacity = int(volume.get('volumes', + consts.DEFAULT_CAPACITY)) + volume_dict = { + 'native_volume_id': volume_name, + 'name': volume_name, + 'total_capacity': total_capacity, + 'used_capacity': used_capacity, + 'free_capacity': total_capacity - used_capacity, + 'storage_id': self.storage_id, + 'status': constants.StorageStatus.NORMAL, + 'type': constants.VolumeType.THIN if + volume.get('thin_provisioning') is not None + else constants.VolumeType.THICK + } + list_volumes.append(volume_dict) + return list_volumes + + def add_trap_config(self, context, trap_config): + pass + + def clear_alert(self, context, alert): + pass + + def get_storage(self, context): + storages = self.rest_handler.rest_call( + self.rest_handler.REST_STORAGE_URL) + total_capacity = None + used_capacity = None + if storages: + for storage in storages: + used_capacity = int(storage.get('total', + consts.DEFAULT_CAPACITY)) + total_capacity = int(storage.get('capacity', + consts.DEFAULT_CAPACITY)) + break + raw_capacity = consts.DEFAULT_CAPACITY + disks = self.list_disks(context) + if disks: + for disk in disks: + raw_capacity = raw_capacity + disk.get('capacity') + arrays = self.rest_handler.rest_call(self.rest_handler.REST_ARRAY_URL) + storage_name = None + serial_number = None + version = None + if arrays: + storage_name = arrays.get('array_name') + serial_number = arrays.get('id') + version = arrays.get('version') + model = None + status = constants.StorageStatus.NORMAL + controllers = self.rest_handler.rest_call( + self.rest_handler.REST_CONTROLLERS_URL) + if controllers: + for controller in controllers: + if controller.get('mode') == consts.CONTROLLER_PRIMARY: + model = controller.get('model') + if controller.get('status') != \ + consts.NORMAL_CONTROLLER_STATUS: + status = constants.StorageStatus.ABNORMAL + if not all((storages, arrays, controllers)): + LOG.error('get_storage error, Unable to obtain data.') + raise exception.StorageBackendException('Unable to obtain data') + storage_result = { + 'model': model, + 'total_capacity': total_capacity, + 'raw_capacity': raw_capacity, + 'used_capacity': used_capacity, + 'free_capacity': total_capacity - used_capacity, + 'vendor': 'PURE', + 'name': storage_name, + 'serial_number': serial_number, + 'firmware_version': version, + 'status': status + } + return storage_result + + def list_alerts(self, context, query_para=None): + alerts = self.rest_handler.rest_call(self.rest_handler.REST_ALERTS_URL) + alerts_list = [] + if alerts: + for alert in alerts: + alerts_model = dict() + opened = alert.get('opened') + time_difference = time.mktime( + time.localtime()) - time.mktime(time.gmtime()) + timestamp = (int(datetime.datetime.strptime + (opened, '%Y-%m-%dT%H:%M:%SZ').timestamp() + + time_difference) * + consts.DEFAULT_LIST_ALERTS_TIME_CONVERSION) if\ + opened is not None else None + if query_para is not None: + try: + if timestamp is None or timestamp \ + < int(query_para.get('begin_time')) or \ + timestamp > int(query_para.get('end_time')): + continue + except Exception as e: + LOG.error(e) + alerts_model['occur_time'] = timestamp + alerts_model['alert_id'] = alert.get('id') + alerts_model['severity'] = consts.SEVERITY_MAP.get( + alert.get('current_severity'), + constants.Severity.NOT_SPECIFIED) + alerts_model['category'] = constants.Category.FAULT + component_name = alert.get('component_name') + alerts_model['location'] = component_name + alerts_model['type'] = constants.EventType.EQUIPMENT_ALARM + alerts_model['resource_type'] = constants.DEFAULT_RESOURCE_TYPE + event = alert.get('event') + alerts_model['alert_name'] = event + alerts_model['match_key'] = hashlib.md5(str(alert.get('id')). + encode()).hexdigest() + alerts_model['description'] = '({}:{}): {}'. \ + format(alert.get('component_type'), component_name, event) + alerts_list.append(alerts_model) + return alerts_list + + @staticmethod + def parse_alert(context, alert): + try: + alert_model = dict() + alert_model['alert_id'] = alert.get(consts.PARSE_ALERT_ALERT_ID) + alert_model['severity'] = consts.PARSE_ALERT_SEVERITY_MAP.get( + alert.get(consts.PARSE_ALERT_SEVERITY), + constants.Severity.NOT_SPECIFIED) + alert_model['category'] = constants.Category.FAULT + alert_model['occur_time'] = utils.utcnow_ms() + alert_model['description'] = '({}:{}): {}'.format(alert.get( + consts.PARSE_ALERT_STORAGE_NAME), + alert.get(consts.PARSE_ALERT_CONTROLLER_NAME), + alert.get(consts.PARSE_ALERT_DESCRIPTION)) + alert_model['location'] = alert.get( + consts.PARSE_ALERT_CONTROLLER_NAME) + alert_model['type'] = constants.EventType.EQUIPMENT_ALARM + alert_model['resource_type'] = constants.DEFAULT_RESOURCE_TYPE + alert_model['alert_name'] = alert.get( + consts.PARSE_ALERT_ALERT_NAME) + alert_model['sequence_number'] = alert.get( + consts.PARSE_ALERT_ALERT_ID) + alert_model['match_key'] = hashlib.md5(str(alert.get( + consts.PARSE_ALERT_ALERT_ID)).encode()).hexdigest() + return alert_model + except Exception as e: + LOG.error(e) + msg = (_("Failed to build alert model as some attributes missing")) + raise exception.InvalidResults(msg) + + def list_controllers(self, context): + list_controllers = [] + controllers = self.rest_handler.rest_call( + self.rest_handler.REST_CONTROLLERS_URL) + hardware = self.get_hardware() + if controllers: + for controller in controllers: + controllers_dict = dict() + controller_name = controller.get('name') + controllers_dict['name'] = controller_name + controllers_dict['status'] = consts.CONTROLLER_STATUS_MAP.get( + hardware.get(controller_name, {}).get('status'), + constants.ControllerStatus.UNKNOWN) + controllers_dict['soft_version'] = controller.get('version') + controllers_dict['storage_id'] = self.storage_id + controllers_dict['native_controller_id'] = controller_name + controllers_dict['location'] = controller_name + list_controllers.append(controllers_dict) + return list_controllers + + def list_disks(self, context): + hardware_dict = self.get_hardware() + list_disks = [] + disks = self.rest_handler.rest_call(self.rest_handler.REST_DISK_URL) + if disks: + for disk in disks: + disk_type = disk.get('type') + if disk_type == consts.DISK_TYPE_NVRAM or disk_type is None: + continue + disk_dict = dict() + drive_name = disk.get('name') + disk_dict['name'] = drive_name + physical_type = disk_type.lower() if disk_type is not None \ + else None + disk_dict['physical_type'] = physical_type \ + if physical_type in constants.DiskPhysicalType.ALL else \ + constants.DiskPhysicalType.UNKNOWN + disk_dict['status'] = consts.DISK_STATUS_MAP. \ + get(disk.get('status'), constants.DiskStatus.OFFLINE) + disk_dict['storage_id'] = self.storage_id + disk_dict['capacity'] = int(disk.get('capacity', + consts.DEFAULT_CAPACITY)) + hardware_object = hardware_dict.get(drive_name, {}) + speed = hardware_object.get('speed') + disk_dict['speed'] = int(speed) if speed is not None else None + disk_dict['model'] = hardware_object.get('model') + disk_dict['serial_number'] = hardware_object. \ + get('serial_number') + disk_dict['native_disk_id'] = drive_name + disk_dict['location'] = drive_name + disk_dict['manufacturer'] = "PURE" + disk_dict['firmware'] = "" + list_disks.append(disk_dict) + return list_disks + + def get_hardware(self): + hardware_dict = dict() + hardware = self.rest_handler.rest_call( + self.rest_handler.REST_HARDWARE_URL) + if hardware: + for hardware_value in hardware: + hardware_map = dict() + hardware_map['speed'] = hardware_value.get('speed') + hardware_map['serial_number'] = hardware_value.get('serial') + hardware_map['model'] = hardware_value.get('model') + hardware_map['status'] = hardware_value.get('status') + hardware_dict[hardware_value.get('name')] = hardware_map + return hardware_dict + + def list_ports(self, context): + list_ports = [] + networks = self.get_network() + ports = self.get_ports() + hardware_dict = self.rest_handler.rest_call( + self.rest_handler.REST_HARDWARE_URL) + if not hardware_dict: + return list_ports + for hardware in hardware_dict: + hardware_result = dict() + hardware_name = hardware.get('name') + if 'FC' in hardware_name: + hardware_result['type'] = constants.PortType.FC + elif 'ETH' in hardware_name: + hardware_result['type'] = constants.PortType.ETH + elif 'SAS' in hardware_name: + hardware_result['type'] = constants.PortType.SAS + else: + continue + hardware_result['name'] = hardware_name + hardware_result['native_port_id'] = hardware_name + hardware_result['storage_id'] = self.storage_id + hardware_result['location'] = hardware_name + speed = hardware.get('speed') + if speed is None: + hardware_result['connection_status'] = \ + constants.PortConnectionStatus.UNKNOWN + elif speed == consts.CONSTANT_ZERO: + hardware_result['connection_status'] = \ + constants.PortConnectionStatus.DISCONNECTED + hardware_result['speed'] = speed + else: + hardware_result['connection_status'] = \ + constants.PortConnectionStatus.CONNECTED + hardware_result['speed'] = int(speed) + hardware_result['health_status'] = consts.PORT_STATUS_MAP.get( + hardware.get('status'), constants.PortHealthStatus.UNKNOWN) + port = ports.get(hardware_name) + if port: + hardware_result['wwn'] = port.get('wwn') + network = networks.get(hardware_name) + if network: + hardware_result['mac_address'] = network.get('mac_address') + hardware_result['logical_type'] = network.get('logical_type') + hardware_result['ipv4_mask'] = network.get('ipv4_mask') + hardware_result['ipv4'] = network.get('ipv4') + list_ports.append(hardware_result) + return list_ports + + def get_network(self): + networks_object = dict() + networks = self.rest_handler.rest_call( + self.rest_handler.REST_NETWORK_URL) + if networks: + for network in networks: + network_dict = dict() + network_dict['mac_address'] = network.get('hwaddr') + services_list = network.get('services') + if services_list: + for services in services_list: + network_dict['logical_type'] = services if \ + services in constants.PortLogicalType.ALL else None + break + network_dict['ipv4_mask'] = network.get('netmask') + network_dict['ipv4'] = network.get('address') + network_name = network.get('name').upper() + networks_object[network_name] = network_dict + return networks_object + + def get_ports(self): + ports_dict = dict() + ports = self.rest_handler.rest_call(self.rest_handler.REST_PORT_URL) + if ports: + for port in ports: + port_dict = dict() + port_name = port.get('name') + wwn = port.get('wwn') + port_dict['wwn'] = self.get_splice_wwn(wwn) \ + if wwn is not None else port.get('iqn') + ports_dict[port_name] = port_dict + return ports_dict + + @staticmethod + def get_splice_wwn(wwn): + wwn_list = list(wwn) + wwn_splice = wwn_list[0] + for serial in range(1, len(wwn_list)): + if serial % consts.SPLICE_WWN_SERIAL == consts.CONSTANT_ZERO: + wwn_splice = '{}{}'.format(wwn_splice, consts.SPLICE_WWN_COLON) + wwn_splice = '{}{}'.format(wwn_splice, wwn_list[serial]) + return wwn_splice + + def list_storage_pools(self, context): + return [] + + def remove_trap_config(self, context, trap_config): + pass + + def reset_connection(self, context, **kwargs): + self.rest_handler.logout() + self.rest_handler.login() + + @staticmethod + def get_access_url(): + return 'https://{ip}' diff --git a/delfin/drivers/pure/flasharray/rest_handler.py b/delfin/drivers/pure/flasharray/rest_handler.py new file mode 100644 index 000000000..39570041a --- /dev/null +++ b/delfin/drivers/pure/flasharray/rest_handler.py @@ -0,0 +1,116 @@ +# Copyright 2022 The SODA Authors. +# +# 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 six +from oslo_log import log as logging + +from delfin import exception, cryptor +from delfin.drivers.pure.flasharray import consts +from delfin.drivers.utils.rest_client import RestClient + +LOG = logging.getLogger(__name__) + + +class RestHandler(RestClient): + REST_STORAGE_URL = '/api/1.17/array?space=true' + REST_ARRAY_URL = '/api/1.17/array' + REST_VOLUME_URL = '/api/1.17/volume?space=true&limit=500&token=' \ + 'aWQgPSA5ODA1Mg==' + REST_VOLUME_TOKEN_URL = '/api/1.17/volume?space=true&limit=20&token=' + REST_PORT_URL = '/api/1.17/port' + REST_NETWORK_URL = '/api/1.17/network' + REST_DISK_URL = '/api/1.17/drive' + REST_HARDWARE_URL = '/api/1.17/hardware' + REST_CONTROLLERS_URL = '/api/1.17/array?controllers=true' + REST_ALERTS_URL = '/api/1.17/message?flagged=true&open=true' + REST_AUTH_URL = '/api/1.17/auth/apitoken' + REST_SESSION_URL = '/api/1.17/auth/session' + + def __init__(self, **kwargs): + super(RestHandler, self).__init__(**kwargs) + + def login(self): + try: + data = {'username': self.rest_username, 'password': cryptor.decode( + self.rest_password)} + self.init_http_head() + token_res = self.do_call(RestHandler.REST_AUTH_URL, data, + method='POST') + if token_res.json().get('msg') == consts.LOGIN_PASSWORD_ERR: + LOG.error("Login error, Obtaining the token is abnormal. " + "status_code:%s, URL: %s", + token_res.status_code, RestHandler.REST_AUTH_URL) + raise exception.InvalidUsernameOrPassword( + 'Obtaining the token is abnormal') + if token_res.status_code != consts.SUCCESS_STATUS_CODE or not \ + token_res.json().get('api_token'): + LOG.error("Login error, Obtaining the token is abnormal. " + "status_code:%s, URL: %s", + token_res.status_code, RestHandler.REST_AUTH_URL) + raise exception.StorageBackendException( + 'Obtaining the token is abnormal') + session_res = self.do_call(RestHandler.REST_SESSION_URL, + token_res.json(), method='POST') + if session_res.status_code != consts.SUCCESS_STATUS_CODE or not \ + session_res.json().get('username'): + LOG.error("Login error, Obtaining the session is abnormal." + "status_code:%s, URL: %s", session_res.status_code, + RestHandler.REST_SESSION_URL) + raise exception.StorageBackendException( + 'Obtaining the session is abnormal.') + except Exception as e: + LOG.error("Login error: %s", six.text_type(e)) + raise e + finally: + data = None + token_res = None + + def logout(self): + res = self.do_call(RestHandler.REST_SESSION_URL, None, method='DELETE') + if res.status_code != consts.SUCCESS_STATUS_CODE\ + or not res.json().get('username'): + LOG.error("Logout error, Deleting a Token Exception." + "status_code:%s, URL: %s", + res.status_code, RestHandler.REST_SESSION_URL) + raise exception.StorageBackendException(res.text) + + def rest_call(self, url, data=None, method='GET'): + result_json = None + res = self.do_call(url, data, method) + if res.status_code == consts.SUCCESS_STATUS_CODE: + result_json = res.json() + elif res.status_code == consts.PERMISSION_DENIED_STATUS_CODE: + self.login() + the_second_time_res = self.do_call(url, data, method) + if the_second_time_res.status_code == consts.SUCCESS_STATUS_CODE: + result_json = the_second_time_res.json() + return result_json + + def get_volumes(self, url=REST_VOLUME_URL, data=None, volume_list=None, + count=consts.DEFAULT_COUNT_GET_VOLUMES_INFO): + if volume_list is None: + volume_list = [] + res = self.do_call(url, data, 'GET') + if res.status_code == consts.SUCCESS_STATUS_CODE: + result_json = res.json() + volume_list.extend(result_json) + next_token = res.headers.get(consts.CUSTOM_TOKEN) + if next_token: + url = '%s%s' % (RestHandler.REST_VOLUME_TOKEN_URL, next_token) + self.get_volumes(url, data, volume_list) + elif res.status_code == consts.PERMISSION_DENIED_STATUS_CODE: + self.login() + if count < consts.RE_LOGIN_TIMES: + count = count + consts.CONSTANT_ONE + self.get_volumes(url, data, volume_list, count) + return volume_list diff --git a/delfin/tests/unit/drivers/pure/__init__.py b/delfin/tests/unit/drivers/pure/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/delfin/tests/unit/drivers/pure/flasharray/__init__.py b/delfin/tests/unit/drivers/pure/flasharray/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/delfin/tests/unit/drivers/pure/flasharray/test_pure_flasharray.py b/delfin/tests/unit/drivers/pure/flasharray/test_pure_flasharray.py new file mode 100644 index 000000000..6c1046088 --- /dev/null +++ b/delfin/tests/unit/drivers/pure/flasharray/test_pure_flasharray.py @@ -0,0 +1,368 @@ +# Copyright 2022 The SODA Authors. +# +# 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 sys +from unittest import TestCase, mock + +import six +from oslo_log import log + +sys.modules['delfin.cryptor'] = mock.Mock() +from delfin import context +from delfin.drivers.pure.flasharray.rest_handler import RestHandler +from delfin.drivers.pure.flasharray.pure_flasharray import PureFlashArrayDriver +LOG = log.getLogger(__name__) + +ACCESS_INFO = { + "rest": { + "host": "10.0.0.1", + "port": 8443, + "username": "user", + "password": "pass" + } +} + +volumes_info = [ + { + "total": 116272464547, + "name": "oracl_ail", + "system": "", + "snapshots": 0, + "volumes": 116272464547, + "data_reduction": 1.82656654775252, + "size": 2156324555567, + "shared_space": "", + "thin_provisioning": 0.9225557589632, + "total_reduction": 18.92245232244555 + }, + { + "total": 0, + "name": "wxt1", + "system": "", + "snapshots": 0, + "volumes": 0, + "data_reduction": 1, + "size": 1073741824, + "shared_space": "", + "thin_provisioning": 1, + "total_reduction": 1 + } +] + +pool_info = [ + { + "name": "lktest", + "volumes": [ + "oracl_ail", + "wxt1", + "lktest/lk301", + "lktest/lk401", + "lktest/lk501", + ] + }, + { + "name": "ethanTestVG", + "volumes": [ + + ] + } +] +volume_info = { + "created": "2016-05-02T20:36:20Z", + "name": "oracl_ail", + "serial": "Fedd3455666y", + "size": 1073740124, + "source": "" +} +volume_info_two = { + "created": "2016-05-02T20:36:20Z", + "name": "wxt1", + "serial": "Fedd3475666y", + "size": 1073740124, + "source": "" +} +storage_info = [ + { + "parity": "0.996586544522471235", + "provisioned": "20869257625600", + "hostname": "FA-m20", + "system": 0, + "snapshots": 0, + "volumes": 227546215656, + "data_reduction": 1, + "capacity": 122276719419392, + "total": 324829845504, + "shared_space": 97544451659, + "thin_provisioning": 0.9526445631455244, + "total_reduction": 64.152236458789225 + } +] +storage_id_info = { + "array_name": "pure01", + "id": "dlmkk15xcfdf4v5", + "revision": "2016-20-29mfmkkk", + "version": "4.6.7" +} +alerts_info = [ + { + "category": "array", + "code": 42, + "actual": "", + "opened": "2018-05-12T10:55:21Z", + "component_type": "hardware", + "event": "failure", + "current_severity": "warning", + "details": "", + "expected": "", + "id": 135, + "component_name": "ct1.eth0" + }, + { + "category": "array", + "code": 13, + "actual": "", + "opened": "2018-05-12T10:55:21Z", + "component_type": "process", + "event": "server unreachable", + "current_severity": "warning", + "details": "", + "expected": "", + "id": 10088786, + "component_name": "ct1.ntpd" + } +] +parse_alert_info = { + '1.3.6.1.2.1.1.3.0': '30007589', + '1.3.6.1.4.1.40482.3.7': '2', + '1.3.6.1.4.1.40482.3.6': 'server error', + '1.3.6.1.4.1.40482.3.3': 'cto', + '1.3.6.1.4.1.40482.3.5': 'cto.server error' +} +controllers_info = [ + { + "status": "ready", + "name": "CT0", + "version": "5.3.0", + "mode": "primary", + "model": "FA-m20r2", + "type": "array_controller" + }, + { + "status": "ready", + "name": "CT1", + "version": "5.3.0", + "mode": "secondary", + "model": "FA-m20r2", + "type": "array_controller" + } +] +hardware_info = [ + { + "details": "", + "identify": "off", + "index": 0, + "name": "CTO.FC1", + "slot": "", + "speed": 0, + "status": "ok", + "temperature": "" + }, + { + "details": "", + "identify": "", + "index": 0, + "name": "CTO.ETH15", + "slot": 0, + "speed": 1000000, + "status": "ok", + "temperature": "" + } +] +drive_info = [ + { + "status": "healthy", + "protocol": "SAS", + "name": "CH0.BAY1", + "last_evac_completed": "1970-01-01T00:00:00Z", + "details": "", + "capacity": 1027895542547, + "type": "SSD", + "last_failure": "1970-01-01T00:00:00Z" + }, + { + "status": "healthy", + "protocol": "SAS", + "name": "CH0.BAY2", + "last_evac_completed": "1970-01-01T00:00:00Z", + "details": "", + "capacity": 1027895542547, + "type": "SSD", + "last_failure": "1970-01-01T00:00:00Z" + }, + { + "status": "healthy", + "protocol": "SAS", + "name": "CH0.BAY3", + "last_evac_completed": "1970-01-01T00:00:00Z", + "details": "", + "capacity": 1027895542547, + "type": "SSD", + "last_failure": "1970-01-01T00:00:00Z" + } +] +port_info = [ + { + "name": "CTO.FC1", + "failover": "", + "iqn": "iqn.2016-11-01.com.pure", + "portal": "100.12.253.23:4563", + "wwn": "43ddff45ggg4rty", + "nqn": "" + }, + { + "name": "CTO.ETH15", + "failover": "", + "iqn": "iqn.2016-11-01.com.pure", + "portal": "100.12.253.23:4563", + "wwn": None, + "nqn": None + } +] +port_network_info = [ + { + "name": "CTO.FC1", + "address": "45233662jksndj", + "speed": 12000, + "netmask": "100.12.253.23:4563", + "wwn": "43ddff45ggg4rty", + "nqn": None, + "services": [ + "management" + ] + }, + { + "name": "CTO.ETH15", + "address": "45233662jksndj", + "speed": 13000, + "netmask": "100.12.253.23:4563", + "wwn": None, + "nqn": None, + "services": [ + "management" + ] + } +] +pools_info = [ + { + "total": "", + "name": "lktest", + "snapshots": "", + "volumes": 0, + "data_reduction": 1, + "size": 5632155322, + "thin_provisioning": 1, + "total_reduction": 1 + }, + { + "total": "", + "name": "ethanTestVG", + "snapshots": "", + "volumes": 0, + "data_reduction": 1, + "size": 5632155322, + "thin_provisioning": 1, + "total_reduction": 1 + } +] +reset_connection_info = { + "username": "username", + "status": 200 +} + + +def create_driver(): + RestHandler.login = mock.Mock( + return_value={None}) + return PureFlashArrayDriver(**ACCESS_INFO) + + +class test_PureFlashArrayDriver(TestCase): + driver = create_driver() + + def test_init(self): + RestHandler.login = mock.Mock( + return_value={""}) + PureFlashArrayDriver(**ACCESS_INFO) + + def test_list_volumes(self): + RestHandler.get_volumes = mock.Mock( + side_effect=[volumes_info]) + volume = self.driver.list_volumes(context) + self.assertEqual(volume[0]['native_volume_id'], + pool_info[0].get('volumes')[0]) + + def test_get_storage(self): + RestHandler.rest_call = mock.Mock( + side_effect=[storage_info, hardware_info, drive_info, + storage_id_info, controllers_info]) + storage_object = self.driver.get_storage(context) + self.assertEqual(storage_object.get('name'), + storage_id_info.get('array_name')) + + def test_list_alerts(self): + RestHandler.rest_call = mock.Mock( + side_effect=[alerts_info]) + list_alerts = self.driver.list_alerts(context) + self.assertEqual(list_alerts[0].get('alert_id'), + alerts_info[0].get('id')) + + def test_parse_alert(self): + parse_alert = self.driver.parse_alert(context, parse_alert_info) + self.assertEqual(parse_alert.get('alert_id'), + parse_alert_info.get('1.3.6.1.2.1.1.3.0')) + + def test_list_controllers(self): + RestHandler.rest_call = mock.Mock( + side_effect=[controllers_info, hardware_info]) + list_controllers = self.driver.list_controllers(context) + self.assertEqual(list_controllers[0].get('name'), + controllers_info[0].get('name')) + + def test_list_disks(self): + RestHandler.rest_call = mock.Mock( + side_effect=[hardware_info, drive_info]) + list_disks = self.driver.list_disks(context) + self.assertEqual(list_disks[0].get('name'), + drive_info[0].get('name')) + + def test_list_ports(self): + RestHandler.rest_call = mock.Mock( + side_effect=[port_network_info, port_info, hardware_info]) + list_ports = self.driver.list_ports(context) + self.assertEqual(list_ports[0].get('name'), + hardware_info[0].get('name')) + + def test_list_storage_pools(self): + list_storage_pools = self.driver.list_storage_pools(context) + self.assertEqual(list_storage_pools, []) + + def test_reset_connection(self): + RestHandler.logout = mock.Mock(side_effect=None) + RestHandler.login = mock.Mock(side_effect=None) + username = None + try: + self.driver.reset_connection(context) + except Exception as e: + LOG.error("test_reset_connection error: %s", six.text_type(e)) + username = reset_connection_info.get('username') + self.assertEqual(username, None) diff --git a/setup.py b/setup.py index 630ca5258..ba8b5a571 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,8 @@ 'ibm storwize_svc = delfin.drivers.ibm.storwize_svc.storwize_svc:StorwizeSVCDriver', 'ibm ds8k = delfin.drivers.ibm.ds8k.ds8k:DS8KDriver', 'netapp cmode = delfin.drivers.netapp.dataontap.cluster_mode:NetAppCmodeDriver', - 'hitachi hnas = delfin.drivers.hitachi.hnas.hds_nas:HitachiHNasDriver' + 'hitachi hnas = delfin.drivers.hitachi.hnas.hds_nas:HitachiHNasDriver', + 'pure flasharray = delfin.drivers.pure.flasharray.pure_flasharray:PureFlashArrayDriver' ] }, )