Merge "Add share driver for Tegile IntelliFlash Arrays"

This commit is contained in:
Jenkins 2016-03-03 17:14:01 +00:00 committed by Gerrit Code Review
commit f11558ff16
10 changed files with 1495 additions and 0 deletions

View File

@ -112,6 +112,7 @@ Share backends
hdfs_native_driver
hds_hnas_driver
hpe_3par_driver
tegile_driver
Indices and tables
------------------

View File

@ -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 | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+

View File

@ -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

View File

@ -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")

View File

@ -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,

View File

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,4 @@
---
features:
- Added driver for Tegile IntelliFlash arrays.