diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index 76f93e7d28..3954cd0982 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -112,6 +112,7 @@ Share backends hdfs_native_driver hds_hnas_driver hpe_3par_driver + tegile_driver Indices and tables ------------------ diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index 41975b2503..ed7ef62496 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -69,6 +69,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | CephFS Native | DHSS = False (M) | \- | M | M | M | \- | \- | +----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ +| Tegile | DHSS = False (M) | \- | M | M | M | M | \- | ++----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ .. note:: @@ -118,6 +120,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ | CephFS Native | \- | \- | \- | CEPH(M) | \- | \- | \- | \- | +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ +| Tegile | NFS (M) |NFS (M),CIFS (M)| \- | \- | NFS (M) |NFS (M),CIFS (M)| \- | \- | ++----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ Mapping of share drivers and security services support ------------------------------------------------------ @@ -161,4 +165,6 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | CephFS Native | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Tegile | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ diff --git a/doc/source/devref/tegile_driver.rst b/doc/source/devref/tegile_driver.rst new file mode 100644 index 0000000000..55a4e6edd4 --- /dev/null +++ b/doc/source/devref/tegile_driver.rst @@ -0,0 +1,129 @@ +.. + Copyright (c) 2016 Tegile Systems Inc. + All Rights Reserved. + + 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. + +Tegile Driver +============= + +The Tegile Manila driver uses Tegile IntelliFlash Arrays to provide shared +filesystems to OpenStack. + +The Tegile Driver interfaces with a Tegile Array via the REST API. + +Requirements +------------ + +- Tegile IntelliFlash version 3.5.1 +- For using CIFS, Active Directory must be configured in the Tegile Array. + +Supported Operations +-------------------- + +The following operations are supported on a Tegile Array: + +* Create CIFS/NFS Share +* Delete CIFS/NFS Share +* Allow CIFS/NFS Share access + * Only IP access type is supported for NFS + * USER access type is supported for NFS and CIFS + * RW and RO access supported +* Deny CIFS/NFS Share access + * IP access type is supported for NFS + * USER access type is supported for NFS and CIFS +* Create snapshot +* Delete snapshot +* Extend share +* Shrink share +* Create share from snapshot + +Backend Configuration +--------------------- + +The following parameters need to be configured in the [DEFAULT] +section of */etc/manila/manila.conf*: + ++-----------------------------------------------------------------------------------------------------------------------------------+ +| [DEFAULT] | ++============================+======================================================================================================+ +| **Option** | **Description** | ++----------------------------+-----------+------------------------------------------------------------------------------------------+ +| enabled_share_backends | Name of the section on manila.conf used to specify a backend. | +| | E.g. *enabled_share_backends = tegileNAS* | ++----------------------------+------------------------------------------------------------------------------------------------------+ +| enabled_share_protocols | Specify a list of protocols to be allowed for share creation. For Tegile driver this can be: | +| | *NFS* or *CIFS* or *NFS, CIFS*. | ++----------------------------+------------------------------------------------------------------------------------------------------+ + +The following parameters need to be configured in the [backend] section of */etc/manila/manila.conf*: + ++-------------------------------------------------------------------------------------------------------------------------------------+ +| [tegileNAS] | ++===============================+=====================================================================================================+ +| **Option** | **Description** | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| share_backend_name | A name for the backend. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| share_driver | Python module path. For Tegile driver this must be: | +| | *manila.share.drivers.tegile.tegile.TegileShareDriver*. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| driver_handles_share_servers| DHSS, Driver working mode. For Tegile driver **this must be**: | +| | *False*. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| tegile_nas_server | Tegile array IP to connect from the Manila node. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| tegile_nas_login | This field is used to provide username credential to Tegile array. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| tegile_nas_password | This field is used to provide password credential to Tegile array. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ +| tegile_default_project | This field can be used to specify the default project in Tegile array where shares are created. | +| | This field is optional. | ++-------------------------------+-----------------------------------------------------------------------------------------------------+ + +Below is an example of a valid configuration of Tegile driver: + +| ``[DEFAULT]`` +| ``enabled_share_backends = tegileNAS`` +| ``enabled_share_protocols = NFS,CIFS`` + +| ``[tegileNAS]`` +| ``driver_handles_share_servers = False`` +| ``share_backend_name = tegileNAS`` +| ``share_driver = manila.share.drivers.tegile.tegile.TegileShareDriver`` +| ``tegile_nas_server = 10.12.14.16`` +| ``tegile_nas_login = admin`` +| ``tegile_nas_password = password`` +| ``tegile_default_project = financeshares`` + +Restart of :term:`manila-share` service is needed for the configuration changes +to take effect. + +Restrictions +------------ + +The Tegile driver has the following restrictions: + +- IP access type is supported only for NFS. + +- Only FLAT network is supported. + +The :mod:`manila.share.drivers.tegile.tegile` Module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: manila.share.drivers.tegile.tegile + :noindex: + :members: + :undoc-members: + :show-inheritance: + :exclude-members: TegileAPIExecutor, debugger diff --git a/manila/exception.py b/manila/exception.py index 22531ddc33..c6f50fd9bf 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -768,3 +768,9 @@ class ReplicationException(ManilaException): class ShareReplicaNotFound(NotFound): message = _("Share Replica %(replica_id)s could not be found.") + + +# Tegile Storage drivers +class TegileAPIException(ShareBackendException): + message = _("Unexpected response from Tegile IntelliFlash API: " + "%(response)s") diff --git a/manila/opts.py b/manila/opts.py index f8290a5798..5b76019ade 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -68,6 +68,7 @@ import manila.share.drivers.lxd import manila.share.drivers.netapp.options import manila.share.drivers.quobyte.quobyte import manila.share.drivers.service_instance +import manila.share.drivers.tegile.tegile import manila.share.drivers.windows.service_instance import manila.share.drivers.windows.winrm_helper import manila.share.drivers.zfsonlinux.driver @@ -141,6 +142,7 @@ _global_opt_lists = [ manila.share.drivers.service_instance.common_opts, manila.share.drivers.service_instance.no_share_servers_handling_mode_opts, manila.share.drivers.service_instance.share_servers_handling_mode_opts, + manila.share.drivers.tegile.tegile.tegile_opts, manila.share.drivers.windows.service_instance.windows_share_server_opts, manila.share.drivers.windows.winrm_helper.winrm_opts, manila.share.drivers.zfsonlinux.driver.zfsonlinux_opts, diff --git a/manila/share/drivers/tegile/__init__.py b/manila/share/drivers/tegile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/tegile/tegile.py b/manila/share/drivers/tegile/tegile.py new file mode 100644 index 0000000000..0cdb2a7247 --- /dev/null +++ b/manila/share/drivers/tegile/tegile.py @@ -0,0 +1,513 @@ +# Copyright (c) 2016 by Tegile Systems, Inc. +# All Rights Reserved. +# +# 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. +""" +Share driver for Tegile storage. +""" + +import json +import requests +import six + +from oslo_config import cfg +from oslo_log import log + +from manila import utils +from manila.i18n import _, _LI, _LW +from manila import exception +from manila.share import driver +from manila.share import utils as share_utils + +tegile_opts = [ + cfg.StrOpt('tegile_nas_server', + help='Tegile NAS server hostname or IP address.'), + cfg.StrOpt('tegile_nas_login', + help='User name for the Tegile NAS server.'), + cfg.StrOpt('tegile_nas_password', + help='Password for the Tegile NAS server.'), + cfg.StrOpt('tegile_default_project', + help='Create shares in this project')] + + +CONF = cfg.CONF +CONF.register_opts(tegile_opts) + +LOG = log.getLogger(__name__) +DEFAULT_API_SERVICE = 'openstack' +TEGILE_API_PATH = 'zebi/api' +TEGILE_LOCAL_CONTAINER_NAME = 'Local' +TEGILE_SNAPSHOT_PREFIX = 'Manual-S-' +VENDOR = 'Tegile Systems Inc.' +DEFAULT_BACKEND_NAME = 'Tegile' +VERSION = '1.0.0' +DEBUG_LOGGING = False # For debugging purposes + + +def debugger(func): + """Returns a wrapper that wraps func. + + The wrapper will log the entry and exit points of the function. + """ + + def wrapper(*args, **kwds): + if DEBUG_LOGGING: + LOG.debug('Entering %(classname)s.%(funcname)s', + { + 'classname': args[0].__class__.__name__, + 'funcname': func.__name__, + }) + LOG.debug('Arguments: %(args)s, %(kwds)s', + { + 'args': args[1:], + 'kwds': kwds, + }) + f_result = func(*args, **kwds) + if DEBUG_LOGGING: + LOG.debug('Exiting %(classname)s.%(funcname)s', + { + 'classname': args[0].__class__.__name__, + 'funcname': func.__name__, + }) + LOG.debug('Results: %(result)s', + {'result': f_result}) + return f_result + + return wrapper + + +class TegileAPIExecutor(object): + def __init__(self, classname, hostname, username, password): + self._classname = classname + self._hostname = hostname + self._username = username + self._password = password + + def __call__(self, *args, **kwargs): + return self._send_api_request(*args, **kwargs) + + @debugger + @utils.retry(exception=(requests.ConnectionError, requests.Timeout), + interval=30, + retries=3, + backoff_rate=1) + def _send_api_request(self, method, params=None, + request_type='post', + api_service=DEFAULT_API_SERVICE, + fine_logging=DEBUG_LOGGING): + if params is not None: + params = json.dumps(params) + + url = 'https://%s/%s/%s/%s' % (self._hostname, + TEGILE_API_PATH, + api_service, + method) + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' + 'url: %(url)s', { + 'classname': self._classname, + 'method': method, + 'url': url, + }) + if request_type == 'post': + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) ' + 'method: %(method)s, payload: %(payload)s', + { + 'classname': self._classname, + 'method': method, + 'payload': params, + }) + req = requests.post(url, + data=params, + auth=(self._username, self._password), + verify=False) + else: + req = requests.get(url, + auth=(self._username, self._password), + verify=False) + + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' + 'return code: %(retcode)s', + { + 'classname': self._classname, + 'method': method, + 'retcode': req, + }) + try: + response = req.json() + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) ' + 'method: %(method)s, response: %(response)s', + { + 'classname': self._classname, + 'method': method, + 'response': response, + }) + except ValueError: + # Some APIs don't return output and that's fine + response = '' + req.close() + + if req.status_code != 200: + raise exception.TegileAPIException(response=req.text) + + return response + + +class TegileShareDriver(driver.ShareDriver): + """Tegile NAS driver. Allows for NFS and CIFS NAS storage usage.""" + def __init__(self, *args, **kwargs): + super(TegileShareDriver, self).__init__(False, *args, **kwargs) + + self.configuration.append_config_values(tegile_opts) + self._default_project = (self.configuration.safe_get( + "tegile_default_project") or 'openstack') + self._backend_name = (self.configuration.safe_get('share_backend_name') + or CONF.share_backend_name + or DEFAULT_BACKEND_NAME) + self._hostname = self.configuration.safe_get('tegile_nas_server') + username = self.configuration.safe_get('tegile_nas_login') + password = self.configuration.safe_get('tegile_nas_password') + self._api = TegileAPIExecutor(self.__class__.__name__, + self._hostname, + username, + password) + + @debugger + def create_share(self, context, share, share_server=None): + """Is called to create share.""" + share_name = share['name'] + share_proto = share['share_proto'] + + pool_name = share_utils.extract_host(share['host'], level='pool') + + params = (pool_name, self._default_project, share_name, share_proto) + + # Share name coming from the backend is the most reliable. Sometimes + # a few options in Tegile array could cause sharename to be different + # from the one passed to it. Eg. 'projectname-sharename' instead + # of 'sharename' if inherited share properties are selected. + ip, real_share_name = self._api('createShare', params).split() + + LOG.info(_LI("Created share %(sharename)s, share id %(shid)s."), + {'sharename': share_name, 'shid': share['id']}) + + return self._get_location_path(real_share_name, share_proto, ip) + + @debugger + def extend_share(self, share, new_size, share_server=None): + """Is called to extend share. + + There is no resize for Tegile shares. + We just adjust the quotas. The API is still called 'resizeShare'. + """ + + self._adjust_size(share, new_size, share_server) + + @debugger + def shrink_share(self, shrink_share, shrink_size, share_server=None): + """Uses resize_share to shrink a share. + + There is no shrink for Tegile shares. + We just adjust the quotas. The API is still called 'resizeShare'. + """ + self._adjust_size(shrink_share, shrink_size, share_server) + + @debugger + def _adjust_size(self, share, new_size, share_server=None): + pool, project, share_name = self._get_pool_project_share_name(share) + params = ('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + six.text_type(new_size), + 'GB') + self._api('resizeShare', params) + + @debugger + def delete_share(self, context, share, share_server=None): + """Is called to remove share.""" + pool, project, share_name = self._get_pool_project_share_name(share) + params = ('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + True, + False) + + self._api('deleteShare', params) + + @debugger + def create_snapshot(self, context, snapshot, share_server=None): + """Is called to create snapshot.""" + snap_name = snapshot['name'] + + pool, project, share_name = self._get_pool_project_share_name( + snapshot['share']) + + share = { + 'poolName': '%s' % pool, + 'projectName': '%s' % project, + 'name': share_name, + 'availableSize': 0, + 'totalSize': 0, + 'datasetPath': '%s/%s/%s' % + (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project), + 'mountpoint': share_name, + 'local': 'true', + } + + params = (share, snap_name, False) + + LOG.info(_LI('Creating snapshot for share_name=%(shr)s' + ' snap_name=%(name)s'), + {'shr': share_name, 'name': snap_name}) + + self._api('createShareSnapshot', params) + + @debugger + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None): + """Create a share from a snapshot - clone a snapshot.""" + pool, project, share_name = self._get_pool_project_share_name(share) + + params = ('%s/%s/%s/%s@%s%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + snapshot['share_name'], + TEGILE_SNAPSHOT_PREFIX, + snapshot['name'], + ), + share_name, + True, + ) + + ip, real_share_name = self._api('cloneShareSnapshot', + params).split() + + share_proto = share['share_proto'] + return self._get_location_path(real_share_name, share_proto, ip) + + @debugger + def delete_snapshot(self, context, snapshot, share_server=None): + """Is called to remove snapshot.""" + pool, project, share_name = self._get_pool_project_share_name( + snapshot['share']) + params = ('%s/%s/%s/%s@%s%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name, + TEGILE_SNAPSHOT_PREFIX, + snapshot['name']), + False) + + self._api('deleteShareSnapshot', params) + + @debugger + def ensure_share(self, context, share, share_server=None): + """Invoked to sure that share is exported.""" + + # Fetching share name from server, because some configuration + # options can cause sharename different from the OpenStack share name + pool, project, share_name = self._get_pool_project_share_name(share) + params = [ + '%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + ] + ip, real_share_name = self._api('getShareIPAndMountPoint', + params).split() + + share_proto = share['share_proto'] + location = self._get_location_path(real_share_name, share_proto, ip) + return [location] + + @debugger + def _allow_access(self, context, share, access, share_server=None): + """Allow access to the share.""" + share_proto = share['share_proto'] + access_type = access['access_type'] + access_level = access['access_level'] + access_to = access['access_to'] + + self._check_share_access(share_proto, access_type) + + pool, project, share_name = self._get_pool_project_share_name(share) + params = ('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + share_proto, + access_type, + access_to, + access_level) + + self._api('shareAllowAccess', params) + + @debugger + def _deny_access(self, context, share, access, share_server=None): + """Deny access to the share.""" + share_proto = share['share_proto'] + access_type = access['access_type'] + access_level = access['access_level'] + access_to = access['access_to'] + + self._check_share_access(share_proto, access_type) + + pool, project, share_name = self._get_pool_project_share_name(share) + params = ('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + share_proto, + access_type, + access_to, + access_level) + + self._api('shareDenyAccess', params) + + def _check_share_access(self, share_proto, access_type): + if share_proto == 'CIFS' and access_type != 'user': + reason = _LW('Only USER access type is allowed for ' + 'CIFS shares.') + LOG.warning(reason) + raise exception.InvalidShareAccess(reason=reason) + elif share_proto == 'NFS' and access_type not in ('ip', 'user'): + reason = _LW('Only IP or USER access types are allowed for ' + 'NFS shares.') + LOG.warning(reason) + raise exception.InvalidShareAccess(reason=reason) + elif share_proto not in ('NFS', 'CIFS'): + reason = _LW('Unsupported protocol \"%s\" specified for ' + 'access rule.') % share_proto + raise exception.InvalidShareAccess(reason=reason) + + @debugger + def update_access(self, context, share, access_rules, add_rules=None, + delete_rules=None, share_server=None): + if not (add_rules or delete_rules): + # Recovery mode + pool, project, share_name = ( + self._get_pool_project_share_name(share)) + share_proto = share['share_proto'] + params = ('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + share_name), + share_proto) + + # Clears all current ACLs + # Remove ip and user ACLs if share_proto is NFS + # Remove user ACLs if share_proto is CIFS + self._api('clearAccessRules', params) + + # Looping thru all rules. + # Will have one API call per rule. + for access in access_rules: + self._allow_access(context, share, access, share_server) + else: + # Adding/Deleting specific rules + for access in delete_rules: + self._deny_access(context, share, access, share_server) + for access in add_rules: + self._allow_access(context, share, access, share_server) + + @debugger + def _update_share_stats(self, **kwargs): + """Retrieve stats info.""" + + try: + data = self._api(method='getArrayStats', + request_type='get', + fine_logging=False) + # fixing values coming back here as String to float + for pool in data.get('pools', []): + pool['total_capacity_gb'] = float( + pool.get('total_capacity_gb', 0)) + pool['free_capacity_gb'] = float( + pool.get('free_capacity_gb', 0)) + pool['allocated_capacity_gb'] = float( + pool.get('allocated_capacity_gb', 0)) + + pool['qos'] = pool.pop('QoS_support', False) + pool['reserved_percentage'] = ( + self.configuration.reserved_share_percentage) + pool['dedupe'] = True + pool['compression'] = True + pool['thin_provisioning'] = True + pool['max_over_subscription_ratio'] = ( + self.configuration.max_over_subscription_ratio) + + data['share_backend_name'] = self._backend_name + data['vendor_name'] = VENDOR + data['driver_version'] = VERSION + data['storage_protocol'] = 'NFS_CIFS' + data['snapshot_support'] = True + data['qos'] = False + + super(TegileShareDriver, self)._update_share_stats(data) + except Exception as e: + msg = _('Unexpected error while trying to get the ' + 'usage stats from array.') + LOG.exception(msg) + raise e + + @debugger + def get_pool(self, share): + """Returns pool name where share resides. + + :param share: The share hosted by the driver. + :return: Name of the pool where given share is hosted. + """ + pool = share_utils.extract_host(share['host'], level='pool') + return pool + + @debugger + def get_network_allocations_number(self): + """Get number of network interfaces to be created.""" + return 0 + + @debugger + def _get_location_path(self, share_name, share_proto, ip=None): + if ip is None: + ip = self._hostname + if share_proto == 'NFS': + location = '%s:%s' % (ip, share_name) + elif share_proto == 'CIFS': + location = r'\\%s\%s' % (ip, share_name) + else: + message = _('Invalid NAS protocol supplied: %s.') % share_proto + raise exception.InvalidInput(message) + + export_location = { + 'path': location, + 'is_admin_only': False, + 'metadata': { + 'preferred': True, + }, + } + return export_location + + @debugger + def _get_pool_project_share_name(self, share): + pool = share_utils.extract_host(share['host'], level='pool') + project = self._default_project + + share_name = share['name'] + + return pool, project, share_name diff --git a/manila/tests/share/drivers/tegile/__init__.py b/manila/tests/share/drivers/tegile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/tegile/test_tegile.py b/manila/tests/share/drivers/tegile/test_tegile.py new file mode 100644 index 0000000000..3467d541bc --- /dev/null +++ b/manila/tests/share/drivers/tegile/test_tegile.py @@ -0,0 +1,834 @@ +# Copyright (c) 2016 by Tegile Systems, Inc. +# All Rights Reserved. +# +# 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. +""" +Share driver Test for Tegile storage. +""" + +import ddt +import mock +from oslo_config import cfg +import requests +import six + +from manila.common import constants as const +from manila import context +from manila import exception +from manila.exception import TegileAPIException +from manila.share.configuration import Configuration +from manila.share.drivers.tegile import tegile +from manila import test + + +CONF = cfg.CONF + +test_config = Configuration(None) +test_config.tegile_nas_server = 'some-ip' +test_config.tegile_nas_login = 'some-user' +test_config.tegile_nas_password = 'some-password' +test_config.reserved_share_percentage = 10 +test_config.max_over_subscription_ratio = 30.0 + +test_share = { + 'host': 'node#fake_pool', + 'name': 'testshare', + 'id': 'a24c2ee8-525a-4406-8ccd-8d38688f8e9e', + 'share_proto': 'NFS', + 'size': 10, +} + +test_share_cifs = { + 'host': 'node#fake_pool', + 'name': 'testshare', + 'id': 'a24c2ee8-525a-4406-8ccd-8d38688f8e9e', + 'share_proto': 'CIFS', + 'size': 10, +} + +test_share_fail = { + 'host': 'node#fake_pool', + 'name': 'testshare', + 'id': 'a24c2ee8-525a-4406-8ccd-8d38688f8e9e', + 'share_proto': 'OTHER', + 'size': 10, +} + +test_snapshot = { + 'name': 'testSnap', + 'id': '07ae9978-5445-405e-8881-28f2adfee732', + 'share': test_share, + 'share_name': 'snapshotted', + 'display_name': 'disp', + 'display_description': 'disp-desc', +} + +array_stats = { + 'total_capacity_gb': 4569.199686084874, + 'free_capacity_gb': 4565.381390112452, + 'pools': [ + { + 'total_capacity_gb': 913.5, + 'QoS_support': False, + 'free_capacity_gb': 911.812650680542, + 'reserved_percentage': 0, + 'pool_name': 'pyramid', + }, + { + 'total_capacity_gb': 2742.1996604874, + 'QoS_support': False, + 'free_capacity_gb': 2740.148867149747, + 'reserved_percentage': 0, + 'pool_name': 'cobalt', + }, + { + 'total_capacity_gb': 913.5, + 'QoS_support': False, + 'free_capacity_gb': 913.4198722839355, + 'reserved_percentage': 0, + 'pool_name': 'test', + }, + ], +} + + +fake_tegile_backend_fail = mock.Mock( + side_effect=TegileAPIException(response="Fake Exception")) + + +class FakeResponse(object): + def __init__(self, status, json_output): + self.status_code = status + self.text = 'Random text' + self._json = json_output + + def json(self): + return self._json + + def close(self): + pass + + +@ddt.ddt +class TegileShareDriverTestCase(test.TestCase): + def __init__(self, *args, **kwds): + super(TegileShareDriverTestCase, self).__init__(*args, **kwds) + self._ctxt = context.get_admin_context() + self.configuration = test_config + + def setUp(self): + CONF.set_default('driver_handles_share_servers', False) + self._driver = tegile.TegileShareDriver( + configuration=self.configuration) + self._driver._default_project = 'fake_project' + super(TegileShareDriverTestCase, self).setUp() + + def test_create_share(self): + api_return_value = (test_config.tegile_nas_server + + " " + test_share['name']) + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + return_value=api_return_value)) + + result = self._driver.create_share(self._ctxt, test_share) + + expected = { + 'is_admin_only': False, + 'metadata': { + 'preferred': True, + }, + 'path': 'some-ip:testshare', + } + self.assertEqual(expected, result) + + create_params = ( + 'fake_pool', + 'fake_project', + test_share['name'], + test_share['share_proto'], + ) + mock_api.assert_called_once_with('createShare', create_params) + + def test_create_share_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.create_share, + self._ctxt, + test_share) + + create_params = ( + 'fake_pool', + 'fake_project', + test_share['name'], + test_share['share_proto'], + ) + mock_api.assert_called_once_with('createShare', create_params) + + def test_delete_share(self): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + self._driver.delete_share(self._ctxt, test_share) + + delete_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + delete_params = (delete_path, True, False) + mock_api.assert_called_once_with('deleteShare', delete_params) + mock_params.assert_called_once_with(test_share) + + def test_delete_share_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.delete_share, + self._ctxt, + test_share) + + delete_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + delete_params = (delete_path, True, False) + mock_api.assert_called_once_with('deleteShare', delete_params) + + def test_create_snapshot(self): + mock_api = self.mock_object(self._driver, '_api') + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + + self._driver.create_snapshot(self._ctxt, test_snapshot) + + share = { + 'poolName': 'fake_pool', + 'projectName': 'fake_project', + 'name': test_share['name'], + 'availableSize': 0, + 'totalSize': 0, + 'datasetPath': '%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + ), + 'mountpoint': test_share['name'], + 'local': 'true', + } + create_params = (share, test_snapshot['name'], False) + mock_api.assert_called_once_with('createShareSnapshot', create_params) + mock_params.assert_called_once_with(test_share) + + def test_create_snapshot_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.create_snapshot, + self._ctxt, + test_snapshot) + + share = { + 'poolName': 'fake_pool', + 'projectName': 'fake_project', + 'name': test_share['name'], + 'availableSize': 0, + 'totalSize': 0, + 'datasetPath': '%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + ), + 'mountpoint': test_share['name'], + 'local': 'true', + } + create_params = (share, test_snapshot['name'], False) + mock_api.assert_called_once_with('createShareSnapshot', create_params) + + def test_delete_snapshot(self): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + self._driver.delete_snapshot(self._ctxt, test_snapshot) + + delete_snap_path = ('%s/%s/%s/%s@%s%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + 'Manual-S-', + test_snapshot['name'], + )) + + delete_params = (delete_snap_path, False) + mock_api.assert_called_once_with('deleteShareSnapshot', delete_params) + mock_params.assert_called_once_with(test_share) + + def test_delete_snapshot_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.delete_snapshot, + self._ctxt, + test_snapshot) + + delete_snap_path = ('%s/%s/%s/%s@%s%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + 'Manual-S-', + test_snapshot['name'], + )) + delete_params = (delete_snap_path, False) + mock_api.assert_called_once_with('deleteShareSnapshot', delete_params) + + def test_create_share_from_snapshot(self): + api_return_value = (test_config.tegile_nas_server + + " " + test_share['name']) + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + return_value=api_return_value)) + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + + result = self._driver.create_share_from_snapshot(self._ctxt, + test_share, + test_snapshot) + + expected = { + 'is_admin_only': False, + 'metadata': { + 'preferred': True, + }, + 'path': 'some-ip:testshare', + } + self.assertEqual(expected, result) + + create_params = ( + '%s/%s/%s/%s@%s%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_snapshot['share_name'], + 'Manual-S-', + test_snapshot['name'], + ), + test_share['name'], + True, + ) + mock_api.assert_called_once_with('cloneShareSnapshot', create_params) + mock_params.assert_called_once_with(test_share) + + def test_create_share_from_snapshot_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.create_share_from_snapshot, + self._ctxt, + test_share, + test_snapshot) + + create_params = ( + '%s/%s/%s/%s@%s%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_snapshot['share_name'], + 'Manual-S-', + test_snapshot['name'], + ), + test_share['name'], + True, + ) + mock_api.assert_called_once_with('cloneShareSnapshot', create_params) + + def test_ensure_share(self): + api_return_value = (test_config.tegile_nas_server + + " " + test_share['name']) + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + return_value=api_return_value)) + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + + result = self._driver.ensure_share(self._ctxt, test_share) + + expected = [ + { + 'is_admin_only': False, + 'metadata': { + 'preferred': + True, + }, + 'path': 'some-ip:testshare', + }, + ] + self.assertEqual(expected, result) + + ensure_params = [ + '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name'])] + mock_api.assert_called_once_with('getShareIPAndMountPoint', + ensure_params) + mock_params.assert_called_once_with(test_share) + + def test_ensure_share_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + self.assertRaises(TegileAPIException, + self._driver.ensure_share, + self._ctxt, + test_share) + + ensure_params = [ + '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name'])] + mock_api.assert_called_once_with('getShareIPAndMountPoint', + ensure_params) + + def test_get_share_stats(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + return_value=array_stats)) + + result_dict = self._driver.get_share_stats(True) + + expected_dict = { + 'driver_handles_share_servers': False, + 'driver_version': '1.0.0', + 'free_capacity_gb': 4565.381390112452, + 'pools': [ + { + 'allocated_capacity_gb': 0.0, + 'compression': True, + 'dedupe': True, + 'free_capacity_gb': 911.812650680542, + 'pool_name': 'pyramid', + 'qos': False, + 'reserved_percentage': 10, + 'thin_provisioning': True, + 'max_over_subscription_ratio': 30.0, + 'total_capacity_gb': 913.5}, + { + 'allocated_capacity_gb': 0.0, + 'compression': True, + 'dedupe': True, + 'free_capacity_gb': 2740.148867149747, + 'pool_name': 'cobalt', + 'qos': False, + 'reserved_percentage': 10, + 'thin_provisioning': True, + 'max_over_subscription_ratio': 30.0, + 'total_capacity_gb': 2742.1996604874 + }, + { + 'allocated_capacity_gb': 0.0, + 'compression': True, + 'dedupe': True, + 'free_capacity_gb': 913.4198722839355, + 'pool_name': 'test', + 'qos': False, + 'reserved_percentage': 10, + 'thin_provisioning': True, + 'max_over_subscription_ratio': 30.0, + 'total_capacity_gb': 913.5}, ], + 'qos': False, + 'reserved_percentage': 0, + 'replication_domain': None, + 'share_backend_name': 'Tegile', + 'snapshot_support': True, + 'storage_protocol': 'NFS_CIFS', + 'total_capacity_gb': 4569.199686084874, + 'vendor_name': 'Tegile Systems Inc.', + } + self.assertSubDictMatch(expected_dict, result_dict) + + mock_api.assert_called_once_with(fine_logging=False, + method='getArrayStats', + request_type='get') + + def test_get_share_stats_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.get_share_stats, + True) + + mock_api.assert_called_once_with(fine_logging=False, + method='getArrayStats', + request_type='get') + + def test_get_pool(self): + result = self._driver.get_pool(test_share) + + expected = 'fake_pool' + self.assertEqual(expected, result) + + def test_extend_share(self): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + self._driver.extend_share(test_share, 12) + + extend_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + extend_params = (extend_path, six.text_type(12), 'GB') + mock_api.assert_called_once_with('resizeShare', extend_params) + mock_params.assert_called_once_with(test_share) + + def test_extend_share_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.extend_share, + test_share, 30) + + extend_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + extend_params = (extend_path, six.text_type(30), 'GB') + mock_api.assert_called_once_with('resizeShare', extend_params) + + def test_shrink_share(self): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + self._driver.shrink_share(test_share, 15) + + shrink_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + shrink_params = (shrink_path, six.text_type(15), 'GB') + mock_api.assert_called_once_with('resizeShare', shrink_params) + mock_params.assert_called_once_with(test_share) + + def test_shrink_share_fail(self): + mock_api = self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + self.assertRaises(TegileAPIException, + self._driver.shrink_share, + test_share, 30) + + shrink_path = '%s/%s/%s/%s' % ( + 'fake_pool', 'Local', 'fake_project', test_share['name']) + shrink_params = (shrink_path, six.text_type(30), 'GB') + mock_api.assert_called_once_with('resizeShare', shrink_params) + + @ddt.data('ip', 'user') + def test_allow_access(self, access_type): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + access = { + 'access_type': access_type, + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': 'some-ip', + } + + self._driver._allow_access(self._ctxt, test_share, access) + + allow_params = ( + '%s/%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + ), + test_share['share_proto'], + access_type, + access['access_to'], + access['access_level'], + ) + mock_api.assert_called_once_with('shareAllowAccess', allow_params) + mock_params.assert_called_once_with(test_share) + + @ddt.data({'access_type': 'other', 'to': 'some-ip', 'share': test_share, + 'exception_type': exception.InvalidShareAccess}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share, + 'exception_type': exception.TegileAPIException}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share_cifs, + 'exception_type': exception.InvalidShareAccess}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share_fail, + 'exception_type': exception.InvalidShareAccess}) + @ddt.unpack + def test_allow_access_fail(self, access_type, to, share, exception_type): + self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + access = { + 'access_type': access_type, + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': to, + } + + self.assertRaises(exception_type, + self._driver._allow_access, + self._ctxt, + share, + access) + + @ddt.data('ip', 'user') + def test_deny_access(self, access_type): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + access = { + 'access_type': access_type, + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': 'some-ip', + } + + self._driver._deny_access(self._ctxt, test_share, access) + + deny_params = ( + '%s/%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + ), + test_share['share_proto'], + access_type, + access['access_to'], + access['access_level'], + ) + mock_api.assert_called_once_with('shareDenyAccess', deny_params) + mock_params.assert_called_once_with(test_share) + + @ddt.data({'access_type': 'other', 'to': 'some-ip', 'share': test_share, + 'exception_type': exception.InvalidShareAccess}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share, + 'exception_type': exception.TegileAPIException}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share_cifs, + 'exception_type': exception.InvalidShareAccess}, + {'access_type': 'ip', 'to': 'some-ip', 'share': test_share_fail, + 'exception_type': exception.InvalidShareAccess}) + @ddt.unpack + def test_deny_access_fail(self, access_type, to, share, exception_type): + self.mock_object(self._driver, '_api', + mock.Mock( + side_effect=TegileAPIException( + response="Fake Exception"))) + + access = { + 'access_type': access_type, + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': to, + } + + self.assertRaises(exception_type, + self._driver._deny_access, + self._ctxt, + share, + access) + + @ddt.data({'access_rules': [{'access_type': 'ip', + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': 'some-ip', + }, ], 'add_rules': None, + 'delete_rules': None, 'call_name': 'shareAllowAccess'}, + {'access_rules': [], 'add_rules': + [{'access_type': 'ip', + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': 'some-ip'}, ], 'delete_rules': [], + 'call_name': 'shareAllowAccess'}, + {'access_rules': [], 'add_rules': [], 'delete_rules': + [{'access_type': 'ip', + 'access_level': const.ACCESS_LEVEL_RW, + 'access_to': 'some-ip', }, ], + 'call_name': 'shareDenyAccess'}) + @ddt.unpack + def test_update_access(self, access_rules, add_rules, + delete_rules, call_name): + fake_share_info = ('fake_pool', 'fake_project', test_share['name']) + mock_params = self.mock_object(self._driver, + '_get_pool_project_share_name', + mock.Mock(return_value=fake_share_info)) + mock_api = self.mock_object(self._driver, '_api') + + self._driver.update_access(self._ctxt, + test_share, + access_rules=access_rules, + add_rules=add_rules, + delete_rules=delete_rules) + + allow_params = ( + '%s/%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + ), + test_share['share_proto'], + 'ip', + 'some-ip', + const.ACCESS_LEVEL_RW, + ) + if not (add_rules or delete_rules): + clear_params = ( + '%s/%s/%s/%s' % ( + 'fake_pool', + 'Local', + 'fake_project', + test_share['name'], + ), + test_share['share_proto'], + ) + mock_api.assert_has_calls([mock.call('clearAccessRules', + clear_params), + mock.call(call_name, + allow_params)]) + mock_params.assert_called_with(test_share) + else: + mock_api.assert_called_once_with(call_name, allow_params) + mock_params.assert_called_once_with(test_share) + + @ddt.data({'path': r'\\some-ip\shareName', 'share_proto': 'CIFS', + 'host': 'some-ip'}, + {'path': 'some-ip:shareName', 'share_proto': 'NFS', + 'host': 'some-ip'}, + {'path': 'some-ip:shareName', 'share_proto': 'NFS', + 'host': None}) + @ddt.unpack + def test_get_location_path(self, path, share_proto, host): + self._driver._hostname = 'some-ip' + + result = self._driver._get_location_path('shareName', + share_proto, + host) + expected = { + 'is_admin_only': False, + 'metadata': { + 'preferred': True, + }, + 'path': path, + } + self.assertEqual(expected, result) + + def test_get_location_path_fail(self): + self.assertRaises(exception.InvalidInput, + self._driver._get_location_path, + 'shareName', + 'SOME', + 'some-ip') + + def test_get_network_allocations_number(self): + result = self._driver.get_network_allocations_number() + + expected = 0 + self.assertEqual(expected, result) + + +class TegileAPIExecutorTestCase(test.TestCase): + def setUp(self): + self._api = tegile.TegileAPIExecutor("TestCase", + test_config.tegile_nas_server, + test_config.tegile_nas_login, + test_config.tegile_nas_password) + super(TegileAPIExecutorTestCase, self).setUp() + + def test_send_api_post(self): + json_output = {'value': 'abc'} + + self.mock_object(requests, 'post', + mock.Mock(return_value=FakeResponse(200, + json_output))) + result = self._api(method="Test", request_type='post', params='[]', + fine_logging=True) + + self.assertEqual(json_output, result) + + def test_send_api_get(self): + json_output = {'value': 'abc'} + + self.mock_object(requests, 'get', + mock.Mock(return_value=FakeResponse(200, + json_output))) + + result = self._api(method="Test", + request_type='get', + fine_logging=False) + + self.assertEqual(json_output, result) + + def test_send_api_get_fail(self): + self.mock_object(requests, 'get', + mock.Mock(return_value=FakeResponse(404, []))) + + self.assertRaises(TegileAPIException, + self._api, + method="Test", + request_type='get', + fine_logging=False) + + def test_send_api_value_error_fail(self): + json_output = {'value': 'abc'} + + self.mock_object(requests, 'post', + mock.Mock(return_value=FakeResponse(200, + json_output))) + self.mock_object(FakeResponse, 'json', + mock.Mock(side_effect=ValueError)) + + result = self._api(method="Test", + request_type='post', + fine_logging=False) + + expected = '' + self.assertEqual(expected, result) diff --git a/releasenotes/notes/add-tegile-driver-1859114513edb13e.yaml b/releasenotes/notes/add-tegile-driver-1859114513edb13e.yaml new file mode 100644 index 0000000000..5111e55a24 --- /dev/null +++ b/releasenotes/notes/add-tegile-driver-1859114513edb13e.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added driver for Tegile IntelliFlash arrays. +