Nexenta: adding share drivers for NexentaStor

Share drivers to support NexentaStor4 and NexentaStor5 via
NFS protocol. Supports all minimal required features.

Implements: blueprint nexenta-manila-driver

DocImpact

Change-Id: Ib8b8525397626162968458ec837f9239ff5dbb2b
This commit is contained in:
Alexey Khodos 2016-06-18 07:03:31 +03:00
parent 98da15991a
commit 1fd7e88191
22 changed files with 2380 additions and 0 deletions

View File

@ -75,6 +75,10 @@ Mapping of share drivers and share features support
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Tegile | M | \- | M | M | M | M | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NexentaStor4 | N | \- | N | \- | N | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NexentaStor5 | N | \- | N | N | N | N | \- |
+----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
Mapping of share drivers and share access rules support
-------------------------------------------------------
@ -126,6 +130,10 @@ Mapping of share drivers and share access rules support
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| Tegile | NFS (M) |NFS (M),CIFS (M)| \- | \- | NFS (M) |NFS (M),CIFS (M)| \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| NexentaStor4 | NFS (N) | \- | \- | \- | NFS (N) | \- | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
| NexentaStor5 | NFS (N) | \- | \- | \- | NFS (N) | \- | \- | \- |
+----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+
Mapping of share drivers and security services support
------------------------------------------------------
@ -175,6 +183,10 @@ Mapping of share drivers and security services support
+----------------------------------------+------------------+-----------------+------------------+
| Tegile | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| NexentaStor4 | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
| NexentaStor5 | \- | \- | \- |
+----------------------------------------+------------------+-----------------+------------------+
Mapping of share drivers and common capabilities
------------------------------------------------
@ -224,6 +236,10 @@ Mapping of share drivers and common capabilities
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| Tegile | \- | M | M | M | M | \- | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| NexentaStor4 | \- | N | N | N | N | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
| NexentaStor5 | \- | N | N | N | N | N | \- |
+----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+
.. note::

View File

@ -820,3 +820,7 @@ class HSPTimeoutException(ShareBackendException):
class HSPItemNotFoundException(ShareBackendException):
message = _("HSP Item Not Found Exception: %(msg)s")
class NexentaException(ShareBackendException):
message = _("Exception due to Nexenta failure. %(reason)s")

View File

@ -69,6 +69,7 @@ import manila.share.drivers.huawei.huawei_nas
import manila.share.drivers.ibm.gpfs
import manila.share.drivers.lvm
import manila.share.drivers.netapp.options
import manila.share.drivers.nexenta.options
import manila.share.drivers.quobyte.quobyte
import manila.share.drivers.service_instance
import manila.share.drivers.tegile.tegile
@ -143,6 +144,9 @@ _global_opt_lists = [
manila.share.drivers.netapp.options.netapp_basicauth_opts,
manila.share.drivers.netapp.options.netapp_provisioning_opts,
manila.share.drivers.netapp.options.netapp_replication_opts,
manila.share.drivers.nexenta.options.nexenta_connection_opts,
manila.share.drivers.nexenta.options.nexenta_dataset_opts,
manila.share.drivers.nexenta.options.nexenta_nfs_opts,
manila.share.drivers.quobyte.quobyte.quobyte_manila_share_opts,
manila.share.drivers.service_instance.common_opts,
manila.share.drivers.service_instance.no_share_servers_handling_mode_opts,

View File

View File

@ -0,0 +1,92 @@
# Copyright 2016 Nexenta 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.
"""
:mod:`nexenta.jsonrpc` -- Nexenta-specific JSON RPC client
=====================================================================
.. automodule:: nexenta.jsonrpc
"""
import base64
import json
import requests
from oslo_log import log
from oslo_serialization import jsonutils
from manila import exception
from manila import utils
LOG = log.getLogger(__name__)
class NexentaJSONProxy(object):
retry_exc_tuple = (requests.exceptions.ConnectionError,)
def __init__(self, scheme, host, port, path, user, password, auto=False,
obj=None, method=None):
self.scheme = scheme.lower()
self.host = host
self.port = port
self.path = path
self.user = user
self.password = password
self.auto = auto
self.obj = obj
self.method = method
def __getattr__(self, name):
if not self.obj:
obj, method = name, None
elif not self.method:
obj, method = self.obj, name
else:
obj, method = '%s.%s' % (self.obj, self.method), name
return NexentaJSONProxy(self.scheme, self.host, self.port, self.path,
self.user, self.password, self.auto, obj,
method)
@property
def url(self):
return '%s://%s:%s%s' % (self.scheme, self.host, self.port, self.path)
def __hash__(self):
return self.url.__hash__()
def __repr__(self):
return 'NMS proxy: %s' % self.url
@utils.retry(retry_exc_tuple, retries=6)
def __call__(self, *args):
data = jsonutils.dumps({
'object': self.obj,
'method': self.method,
'params': args,
})
auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))
headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic %s' % auth,
}
LOG.debug('Sending JSON data: %s', data)
r = requests.post(self.url, data=data, headers=headers)
response = json.loads(r.content) if r.content else None
LOG.debug('Got response: %s', response)
if response.get('error') is not None:
message = response['error'].get('message', '')
raise exception.NexentaException(reason=message)
return response.get('result')

View File

@ -0,0 +1,134 @@
# Copyright 2016 Nexenta 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.
from oslo_log import log
from manila import exception
from manila.i18n import _, _LI
from manila.share import driver
from manila.share.drivers.nexenta.ns4 import nexenta_nfs_helper
from manila.share.drivers.nexenta import options
VERSION = '1.0'
LOG = log.getLogger(__name__)
class NexentaNasDriver(driver.ShareDriver):
"""Nexenta Share Driver.
Executes commands relating to Shares.
API version history:
1.0 - Initial version.
"""
def __init__(self, *args, **kwargs):
"""Do initialization."""
LOG.debug('Initializing Nexenta driver.')
super(NexentaNasDriver, self).__init__(False, *args, **kwargs)
self.configuration = kwargs.get('configuration')
if self.configuration:
self.configuration.append_config_values(
options.nexenta_connection_opts)
self.configuration.append_config_values(
options.nexenta_nfs_opts)
self.configuration.append_config_values(
options.nexenta_dataset_opts)
self.helper = nexenta_nfs_helper.NFSHelper(self.configuration)
else:
raise exception.BadConfigurationException(
reason=_('Nexenta configuration missing.'))
@property
def share_backend_name(self):
if not hasattr(self, '_share_backend_name'):
self._share_backend_name = None
if self.configuration:
self._share_backend_name = self.configuration.safe_get(
'share_backend_name')
if not self._share_backend_name:
self._share_backend_name = 'NexentaStor4'
return self._share_backend_name
def do_setup(self, context):
"""Any initialization the Nexenta NAS driver does while starting."""
LOG.debug('Setting up the NexentaStor4 plugin.')
return self.helper.do_setup()
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
self.helper.check_for_setup_error()
def create_share(self, context, share, share_server=None):
"""Create a share."""
LOG.debug('Creating share %s.', share['name'])
return self.helper.create_filesystem(share)
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Is called to create share from snapshot."""
LOG.debug('Creating share from snapshot %s.', snapshot['name'])
return self.helper.create_share_from_snapshot(share, snapshot)
def delete_share(self, context, share, share_server=None):
"""Delete a share."""
LOG.debug('Deleting share %s.', share['name'])
self.helper.delete_share(share['name'])
def extend_share(self, share, new_size, share_server=None):
"""Extends a share."""
LOG.debug('Extending share %(name)s to %(size)sG.', {
'name': share['name'], 'size': new_size})
self.helper.set_quota(share['name'], new_size)
def create_snapshot(self, context, snapshot, share_server=None):
"""Create a snapshot."""
LOG.debug('Creating a snapshot of share %s.', snapshot['share_name'])
snap_id = self.helper.create_snapshot(
snapshot['share_name'], snapshot['name'])
LOG.info(_LI('Created snapshot %s.'), snap_id)
def delete_snapshot(self, context, snapshot, share_server=None):
"""Delete a snapshot."""
LOG.debug('Deleting snapshot %(shr_name)s@%(snap_name)s.', {
'shr_name': snapshot['share_name'],
'snap_name': snapshot['name']})
self.helper.delete_snapshot(snapshot['share_name'], snapshot['name'])
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update access rules for given share.
:param context: The `context.RequestContext` object for the request
:param share: Share that will have its access rules updated.
:param access_rules: All access rules for given share. This list
is enough to update the access rules for given share.
:param add_rules: Empty List or List of access rules which should be
added. access_rules already contains these rules. Not used by this
driver.
:param delete_rules: Empty List or List of access rules which should be
removed. access_rules doesn't contain these rules. Not used by
this driver.
:param share_server: Data structure with share server information.
Not used by this driver.
"""
self.helper.update_access(share['name'], access_rules)
def _update_share_stats(self, data=None):
super(NexentaNasDriver, self)._update_share_stats()
data = self.helper.update_share_stats()
data['driver_version'] = VERSION
data['share_backend_name'] = self.share_backend_name
self._stats.update(data)

View File

@ -0,0 +1,227 @@
# Copyright 2016 Nexenta 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.
from oslo_log import log
from oslo_utils import excutils
from manila.common import constants as common
from manila import exception
from manila.i18n import _, _LI
from manila.share.drivers.nexenta.ns4 import jsonrpc
from manila.share.drivers.nexenta import utils
LOG = log.getLogger(__name__)
NOT_EXIST = 'does not exist'
DEP_CLONES = 'has dependent clones'
class NFSHelper(object):
def __init__(self, configuration):
self.configuration = configuration
self.nfs_mount_point_base = (
self.configuration.nexenta_mount_point_base)
self.dataset_compression = (
self.configuration.nexenta_dataset_compression)
self.dataset_dedupe = self.configuration.nexenta_dataset_dedupe
self.nms = None
self.nms_protocol = self.configuration.nexenta_rest_protocol
self.nms_host = self.configuration.nexenta_host
self.volume = self.configuration.nexenta_volume
self.share = self.configuration.nexenta_nfs_share
self.nms_port = self.configuration.nexenta_rest_port
self.nms_user = self.configuration.nexenta_user
self.nfs = self.configuration.nexenta_nfs
self.nms_password = self.configuration.nexenta_password
self.storage_protocol = 'NFS'
def do_setup(self):
if self.nms_protocol == 'auto':
protocol, auto = 'http', True
else:
protocol, auto = self.nms_protocol, False
path = '/rest/nms/'
self.nms = jsonrpc.NexentaJSONProxy(
protocol, self.nms_host, self.nms_port, path, self.nms_user,
self.nms_password, auto=auto)
def check_for_setup_error(self):
if not self.nms.volume.object_exists(self.volume):
raise exception.NexentaException(reason=_(
"Volume %s does not exist in NexentaStor appliance.") %
self.volume)
folder = '%s/%s' % (self.volume, self.share)
create_folder_props = {
'recordsize': '4K',
'quota': 'none',
'compression': self.dataset_compression,
}
if not self.nms.folder.object_exists(folder):
self.nms.folder.create_with_props(
self.volume, self.share, create_folder_props)
def create_filesystem(self, share):
"""Create file system."""
create_folder_props = {
'recordsize': '4K',
'quota': '%sG' % share['size'],
'compression': self.dataset_compression,
}
if not self.configuration.nexenta_thin_provisioning:
create_folder_props['reservation'] = '%sG' % share['size']
parent_path = '%s/%s' % (self.volume, self.share)
self.nms.folder.create_with_props(
parent_path, share['name'], create_folder_props)
path = self._get_share_path(share['name'])
return [self._get_location_path(path, share['share_proto'])]
def set_quota(self, share_name, new_size):
if self.configuration.nexenta_thin_provisioning:
quota = '%sG' % new_size
self.nms.folder.set_child_prop(
self._get_share_path(share_name), 'quota', quota)
def _get_location_path(self, path, protocol):
location = None
if protocol == 'NFS':
location = {'path': '%s:/volumes/%s' % (self.nms_host, path)}
else:
raise exception.InvalidShare(
reason=(_('Only NFS protocol is currently supported.')))
return location
def delete_share(self, share_name):
"""Delete share."""
folder = self._get_share_path(share_name)
try:
self.nms.folder.destroy(folder.strip(), '-r')
except exception.NexentaException as e:
with excutils.save_and_reraise_exception() as exc:
if NOT_EXIST in e.args[0]:
LOG.info(_LI('Folder %s does not exist, it was '
'already deleted.'), folder)
exc.reraise = False
def _get_share_path(self, share_name):
return '%s/%s/%s' % (self.volume, self.share, share_name)
def _get_snapshot_name(self, snapshot_name):
return 'snapshot-%s' % snapshot_name
def create_snapshot(self, share_name, snapshot_name):
"""Create a snapshot."""
folder = self._get_share_path(share_name)
self.nms.folder.create_snapshot(folder, snapshot_name, '-r')
model_update = {'provider_location': '%s@%s' % (folder, snapshot_name)}
return model_update
def delete_snapshot(self, share_name, snapshot_name):
"""Deletes snapshot."""
try:
self.nms.snapshot.destroy('%s@%s' % (
self._get_share_path(share_name), snapshot_name), '')
except exception.NexentaException as e:
with excutils.save_and_reraise_exception() as exc:
if NOT_EXIST in e.args[0]:
LOG.info(_LI('Snapshot %(folder)s@%(snapshot)s does not '
'exist, it was already deleted.'),
{
'folder': share_name,
'snapshot': snapshot_name,
})
exc.reraise = False
elif DEP_CLONES in e.args[0]:
LOG.info(_LI(
'Snapshot %(folder)s@%(snapshot)s has dependent '
'clones, it will be deleted later.'), {
'folder': share_name,
'snapshot': snapshot_name
})
exc.reraise = False
def create_share_from_snapshot(self, share, snapshot):
snapshot_name = '%s/%s/%s@%s' % (
self.volume, self.share, snapshot['share_name'], snapshot['name'])
self.nms.folder.clone(
snapshot_name,
'%s/%s/%s' % (self.volume, self.share, share['name']))
path = self._get_share_path(share['name'])
return [self._get_location_path(path, share['share_proto'])]
def update_access(self, share_name, access_rules):
"""Update access to the share."""
rw_list = []
ro_list = []
for rule in access_rules:
if rule['access_type'].lower() != 'ip':
msg = _('Only IP access type is supported.')
raise exception.InvalidShareAccess(reason=msg)
else:
if rule['access_level'] == common.ACCESS_LEVEL_RW:
rw_list.append(rule['access_to'])
else:
ro_list.append(rule['access_to'])
share_opts = {
'auth_type': 'none',
'read_write': ':'.join(rw_list),
'read_only': ':'.join(ro_list),
'recursive': 'true',
'anonymous_rw': 'true',
'anonymous': 'true',
'extra_options': 'anon=0',
}
self.nms.netstorsvc.share_folder(
'svc:/network/nfs/server:default',
self._get_share_path(share_name), share_opts)
def _get_capacity_info(self):
"""Calculate available space on the NFS share."""
folder_props = self.nms.folder.get_child_props(
'%s/%s' % (self.volume, self.share), 'used|available')
free = utils.str2gib_size(folder_props['available'])
allocated = utils.str2gib_size(folder_props['used'])
return free + allocated, free, allocated
def update_share_stats(self):
"""Update driver capabilities.
No way of tracking provisioned capacity on this appliance,
not returning any to let the scheduler estimate it.
"""
total, free, allocated = self._get_capacity_info()
compression = not self.dataset_compression == 'off'
dedupe = not self.dataset_dedupe == 'off'
return {
'vendor_name': 'Nexenta',
'storage_protocol': self.storage_protocol,
'nfs_mount_point_base': self.nfs_mount_point_base,
'pools': [{
'pool_name': self.volume,
'total_capacity_gb': total,
'free_capacity_gb': free,
'reserved_percentage':
self.configuration.reserved_share_percentage,
'compression': compression,
'dedupe': dedupe,
'max_over_subscription_ratio': (
self.configuration.safe_get(
'max_over_subscription_ratio')),
'thin_provisioning':
self.configuration.nexenta_thin_provisioning,
}],
}

View File

@ -0,0 +1,145 @@
# Copyright 2016 Nexenta 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.
"""
:mod:`nexenta.jsonrpc` -- Nexenta-specific JSON RPC client
=====================================================================
.. automodule:: nexenta.jsonrpc
"""
import base64
import json
import requests
from requests.packages.urllib3 import exceptions
import time
from oslo_log import log
from oslo_serialization import jsonutils
from manila import exception
from manila.i18n import _
LOG = log.getLogger(__name__)
requests.packages.urllib3.disable_warnings(exceptions.InsecureRequestWarning)
requests.packages.urllib3.disable_warnings(
exceptions.InsecurePlatformWarning)
session = requests.Session()
class NexentaJSONProxy(object):
def __init__(self, scheme, host, port, user,
password, method='get'):
self.scheme = scheme
self.host = host
self.port = port
self.user = user
self.password = password
self.method = method
@property
def url(self):
return '%s://%s:%s/' % (self.scheme, self.host, self.port)
def __getattr__(self, method='get'):
if method:
return NexentaJSONProxy(
self.scheme, self.host, self.port,
self.user, self.password, method)
def __hash__(self):
return self.url.__hash__()
def __repr__(self):
return 'NEF proxy: %s' % self.url
def __call__(self, path, data=None):
auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))
url = self.url + path
if data:
data = jsonutils.dumps(data)
LOG.debug('Sending JSON to url: %s, data: %s, method: %s',
path, data, self.method)
session.headers.update({'Content-Type': 'application/json'})
response = getattr(session, self.method)(
url, data=data, verify=False)
if response.status_code in (401, 403):
LOG.debug('Login requested by NexentaStor')
if self.scheme == 'http':
session.headers.update({'Authorization': 'Basic %s' % auth})
else:
session.headers.update(
{'Authorization': 'Bearer %s' % self.https_auth()})
LOG.debug('Re-sending JSON to url: %s, data: %s, method: %s',
path, data, self.method)
response = getattr(session, self.method)(
url, data=data, verify=False)
self.check_error(response)
content = json.loads(response.content) if response.content else None
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
'code': response.status_code,
'reason': response.reason,
'content': content})
response.close()
if response.status_code == 202 and content:
url = self.url + content['links'][0]['href']
keep_going = True
while keep_going:
time.sleep(1)
response = session.get(url, verify=False)
self.check_error(response)
LOG.debug("Got response: %(code)s %(reason)s", {
'code': response.status_code,
'reason': response.reason})
content = json.loads(
response.content) if response.content else None
keep_going = response.status_code == 202
response.close()
return content
def https_auth(self):
url = self.url + 'auth/login'
data = jsonutils.dumps(
{'username': self.user, 'password': self.password})
response = session.post(
url, data=data, verify=False)
content = json.loads(response.content) if response.content else None
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
'code': response.status_code,
'reason': response.reason,
'content': content})
response.close()
return content['token']
def check_error(self, response):
code = response.status_code
if code not in (200, 201, 202):
reason = response.reason
content = json.loads(
response.content) if response.content else None
response.close()
if content and 'code' in content:
message = content.get(
'message', 'Message is not specified by Nexenta REST')
raise exception.NexentaException(
reason=message, code=content['code'])
raise exception.NexentaException(
reason=_(
'Got bad response: %(code)s %(reason)s %(content)s') % {
'code': code, 'reason': reason, 'content': content})

View File

@ -0,0 +1,422 @@
# Copyright 2016 Nexenta 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.
from oslo_log import log
from oslo_utils import units
from manila.common import constants as common
from manila import exception
from manila.i18n import _, _LW, _LE
from manila.share import driver
from manila.share.drivers.nexenta.ns5 import jsonrpc
from manila.share.drivers.nexenta import options
from manila.share.drivers.nexenta import utils
PATH_DELIMITER = '%2F'
VERSION = '1.0'
LOG = log.getLogger(__name__)
class NexentaNasDriver(driver.ShareDriver):
"""Nexenta Share Driver.
Executes commands relating to Shares.
API version history:
1.0 - Initial version.
"""
driver_prefix = 'nexenta'
def __init__(self, *args, **kwargs):
"""Do initialization."""
LOG.debug('Initializing Nexenta driver.')
super(NexentaNasDriver, self).__init__(False, *args, **kwargs)
self.configuration = kwargs.get('configuration')
if self.configuration:
self.configuration.append_config_values(
options.nexenta_connection_opts)
self.configuration.append_config_values(
options.nexenta_nfs_opts)
self.configuration.append_config_values(
options.nexenta_dataset_opts)
else:
raise exception.BadConfigurationException(
reason=_('Nexenta configuration missing.'))
self.nef = None
self.nef_protocol = self.configuration.nexenta_rest_protocol
self.nef_host = self.configuration.nexenta_host
self.nef_port = self.configuration.nexenta_rest_port
self.nef_user = self.configuration.nexenta_user
self.nef_password = self.configuration.nexenta_password
self.pool_name = self.configuration.nexenta_pool
self.fs_prefix = self.configuration.nexenta_nfs_share
self.storage_protocol = 'NFS'
self.nfs_mount_point_base = self.configuration.nexenta_mount_point_base
self.dataset_compression = (
self.configuration.nexenta_dataset_compression)
self.provisioned_capacity = 0
@property
def share_backend_name(self):
if not hasattr(self, '_share_backend_name'):
self._share_backend_name = None
if self.configuration:
self._share_backend_name = self.configuration.safe_get(
'share_backend_name')
if not self._share_backend_name:
self._share_backend_name = 'NexentaStor5'
return self._share_backend_name
def do_setup(self, context):
"""Any initialization the nexenta nas driver does while starting."""
if self.nef_protocol == 'auto':
protocol = 'https'
else:
protocol = self.nef_protocol
self.nef = jsonrpc.NexentaJSONProxy(
protocol, self.nef_host, self.nef_port, self.nef_user,
self.nef_password)
def check_for_setup_error(self):
"""Verify that the volume for our folder exists.
:raise: :py:exc:`LookupError`
"""
url = 'storage/pools/{}'.format(self.pool_name)
if not self.nef.get(url):
raise LookupError(
_("Pool {} does not exist in Nexenta Store appliance").format(
self.pool_name))
url = 'storage/pools/{}/filesystems/{}'.format(self.pool_name,
self.fs_prefix)
if not self.nef.get(url):
raise LookupError(
_("filesystem {} does not exist in Nexenta Store "
"appliance").format(self.fs_prefix))
path = '/'.join((self.pool_name, self.fs_prefix))
shared = False
response = self.nef.get('nas/nfs')
for share in response['data']:
if share.get('filesystem') == path:
shared = True
break
if not shared:
raise LookupError(_(
"Dataset {} is not shared in Nexenta Store appliance").format(
path))
self._get_provisioned_capacity()
def _get_provisioned_capacity(self):
path = '%(pool)s/%(fs)s' % {
'pool': self.pool_name, 'fs': self.fs_prefix}
url = 'storage/filesystems?parent=%s' % path
fs_list = self.nef.get(url)['data']
for fs in fs_list:
if fs['path'] != path:
self.provisioned_capacity += fs['quotaSize'] / units.Gi
def create_share(self, context, share, share_server=None):
"""Create a share."""
LOG.debug('Creating share: %s.', share['name'])
data = {
'recordSize': 4 * units.Ki,
'compressionMode': self.dataset_compression,
'name': '/'.join((self.fs_prefix, share['name'])),
'quotaSize': share['size'] * units.Gi,
}
if not self.configuration.nexenta_thin_provisioning:
data['reservationSize'] = share['size'] * units.Gi
url = 'storage/pools/{}/filesystems'.format(self.pool_name)
self.nef.post(url, data)
location = {
'path': '{}:/{}/{}/{}'.format(self.nef_host, self.pool_name,
self.fs_prefix, share['name'])
}
try:
self._add_permission(share['name'])
except exception.NexentaException:
try:
self.delete_share(None, share)
except exception.NexentaException as exc:
LOG.warning(_LW(
"Cannot destroy created filesystem: %(vol)s/%(folder)s, "
"exception: %(exc)s"),
{'vol': self.pool_name, 'folder': '/'.join(
(self.fs_prefix, share['name'])), 'exc': exc})
raise
self.provisioned_capacity += share['size']
return [location]
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Is called to create share from snapshot."""
LOG.debug('Creating share from snapshot %s.', snapshot['name'])
url = ('storage/pools/%(pool)s/'
'filesystems/%(fs)s/snapshots/%(snap)s/clone') % {
'pool': self.pool_name,
'fs': PATH_DELIMITER.join(
(self.fs_prefix, snapshot['share_name'])),
'snap': snapshot['name']}
location = {
'path': '{}:/{}/{}/{}'.format(self.nef_host, self.pool_name,
self.fs_prefix, share['name'])
}
path = '/'.join((self.pool_name, self.fs_prefix, share['name']))
data = {
'targetPath': path,
'quotaSize': share['size'] * units.Gi,
'recordSize': 4 * units.Ki,
'compressionMode': self.dataset_compression,
}
if not self.configuration.nexenta_thin_provisioning:
data['reservationSize'] = share['size'] * units.Gi
self.nef.post(url, data)
try:
self._add_permission(share['name'])
except exception.NexentaException:
LOG.exception(
_LE('Failed to add permissions for %s'), share['name'])
try:
self.delete_share(None, share)
except exception.NexentaException:
LOG.warning(_LW("Cannot destroy cloned filesystem: "
"%(vol)s/%(filesystem)s"),
{'vol': self.pool_name,
'filesystem': '/'.join(
(self.fs_prefix, share['name']))})
raise
self.provisioned_capacity += share['size']
return [location]
def delete_share(self, context, share, share_server=None):
"""Delete a share."""
LOG.debug('Deleting share: %s.', share['name'])
url = 'storage/pools/%(pool)s/filesystems/%(fs)s' % {
'pool': self.pool_name,
'fs': PATH_DELIMITER.join([self.fs_prefix, share['name']]),
}
self.nef.delete(url)
self.provisioned_capacity -= share['size']
def extend_share(self, share, new_size, share_server=None):
"""Extends a share."""
LOG.debug(
'Extending share: %(name)s to %(size)sG.', (
{'name': share['name'], 'size': new_size}))
self._set_quota(share['name'], new_size)
self.provisioned_capacity += (new_size - share['size'])
def shrink_share(self, share, new_size, share_server=None):
"""Shrinks size of existing share."""
LOG.debug(
'Shrinking share: %(name)s to %(size)sG.', {
'name': share['name'], 'size': new_size})
url = 'storage/pools/{}/filesystems/{}%2F{}'.format(self.pool_name,
self.fs_prefix,
share['name'])
used = self.nef.get(url)['bytesUsed'] / units.Gi
if used > new_size:
raise exception.ShareShrinkingPossibleDataLoss(
share_id=share['id'])
self._set_quota(share['name'], new_size)
self.provisioned_capacity += (share['size'] - new_size)
def create_snapshot(self, context, snapshot, share_server=None):
"""Create a snapshot."""
LOG.debug('Creating a snapshot of share: %s.', snapshot['share_name'])
url = 'storage/pools/%(pool)s/filesystems/%(fs)s/snapshots' % {
'pool': self.pool_name,
'fs': PATH_DELIMITER.join(
(self.fs_prefix, snapshot['share_name'])),
}
data = {'name': snapshot['name']}
self.nef.post(url, data)
def delete_snapshot(self, context, snapshot, share_server=None):
"""Delete a snapshot."""
LOG.debug('Deleting a snapshot: %(shr_name)s@%(snap_name)s.', {
'shr_name': snapshot['share_name'],
'snap_name': snapshot['name']})
url = ('storage/pools/%(pool)s/filesystems/%(fs)s/snapshots/'
'%(snap)s') % {'pool': self.pool_name,
'fs': PATH_DELIMITER.join(
(self.fs_prefix, snapshot['share_name'])),
'snap': snapshot['name']}
try:
self.nef.delete(url)
except exception.NexentaException as e:
if e.kwargs['code'] == 'ENOENT':
LOG.warning(
_LW('snapshot %(name)s not found, response: %(msg)s'), {
'name': snapshot['name'], 'msg': e.msg})
else:
raise
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update access rules for given share.
Using access_rules list for both adding and deleting rules.
:param context: The `context.RequestContext` object for the request
:param share: Share that will have its access rules updated.
:param access_rules: All access rules for given share. This list
is enough to update the access rules for given share.
:param add_rules: Empty List or List of access rules which should be
added. access_rules already contains these rules. Not used by this
driver.
:param delete_rules: Empty List or List of access rules which should be
removed. access_rules doesn't contain these rules. Not used by
this driver.
:param share_server: Data structure with share server information.
Not used by this driver.
"""
LOG.debug('Updating access to share %s.', share)
rw_list = []
ro_list = []
security_contexts = []
for rule in access_rules:
if rule['access_type'].lower() != 'ip':
msg = _('Only IP access type is supported.')
raise exception.InvalidShareAccess(reason=msg)
else:
if rule['access_level'] == common.ACCESS_LEVEL_RW:
rw_list.append(rule['access_to'])
else:
ro_list.append(rule['access_to'])
def append_sc(addr_list, sc_type):
for addr in addr_list:
address_mask = addr.strip().split('/', 1)
address = address_mask[0]
ls = [{"allow": True, "etype": "network", "entity": address}]
if len(address_mask) == 2:
try:
mask = int(address_mask[1])
if mask != 32:
ls[0]['mask'] = mask
except Exception:
raise exception.InvalidInput(
reason=_(
'<{}> is not a valid access parameter').format(
addr))
new_sc = {"securityModes": ["sys"]}
new_sc[sc_type] = ls
security_contexts.append(new_sc)
append_sc(rw_list, 'readWriteList')
append_sc(ro_list, 'readOnlyList')
data = {"securityContexts": security_contexts}
url = 'nas/nfs/' + PATH_DELIMITER.join(
(self.pool_name, self.fs_prefix, share['name']))
self.nef.put(url, data)
def _set_quota(self, share_name, new_size):
quota = new_size * units.Gi
data = {'quotaSize': quota}
if not self.configuration.nexenta_thin_provisioning:
data['reservationSize'] = quota
url = 'storage/pools/{}/filesystems/{}%2F{}'.format(self.pool_name,
self.fs_prefix,
share_name)
self.nef.put(url, data)
def _update_share_stats(self, data=None):
super(NexentaNasDriver, self)._update_share_stats()
total, free, allocated = self._get_capacity_info()
data = {
'vendor_name': 'Nexenta',
'storage_protocol': self.storage_protocol,
'share_backend_name': self.share_backend_name,
'nfs_mount_point_base': self.nfs_mount_point_base,
'driver_version': VERSION,
'pools': [{
'pool_name': self.pool_name,
'total_capacity_gb': total,
'free_capacity_gb': free,
'reserved_percentage': (
self.configuration.reserved_share_percentage),
'max_over_subscription_ratio': (
self.configuration.safe_get(
'max_over_subscription_ratio')),
'thin_provisioning':
self.configuration.nexenta_thin_provisioning,
'provisioned_capacity_gb': self.provisioned_capacity,
}],
}
self._stats.update(data)
def _get_capacity_info(self):
"""Calculate available space on the NFS share."""
url = 'storage/pools/{}/filesystems/{}'.format(self.pool_name,
self.fs_prefix)
data = self.nef.get(url)
total = utils.bytes_to_gb(data['bytesAvailable'])
allocated = utils.bytes_to_gb(data['bytesUsed'])
free = total - allocated
return total, free, allocated
def _add_permission(self, share_name):
"""Share NFS filesystem on NexentaStor Appliance.
:param share_name: relative filesystem name to be shared
"""
LOG.debug(
'Creating RW ACE for filesystem everyone on Nexenta Store '
'for <%s> filesystem.', share_name)
url = 'storage/pools/{}/filesystems/{}/acl'.format(
self.pool_name, PATH_DELIMITER.join((self.fs_prefix, share_name)))
data = {
"type": "allow",
"principal": "everyone@",
"permissions": [
"list_directory",
"read_data",
"add_file",
"write_data",
"add_subdirectory",
"append_data",
"read_xattr",
"write_xattr",
"execute",
"delete_child",
"read_attributes",
"write_attributes",
"delete",
"read_acl",
"write_acl",
"write_owner",
"synchronize",
],
"flags": [
"file_inherit",
"dir_inherit",
],
}
self.nef.post(url, data)
LOG.debug(
'RW ACE for filesystem <%s> on Nexenta Store has been '
'successfully created.', share_name)

View File

@ -0,0 +1,79 @@
# Copyright 2016 Nexenta 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.
"""
:mod:`nexenta.options` -- Contains configuration options for Nexenta drivers.
=============================================================================
.. automodule:: nexenta.options
"""
from oslo_config import cfg
nexenta_connection_opts = [
cfg.StrOpt('nexenta_host',
help='IP address of Nexenta storage appliance.'),
cfg.IntOpt('nexenta_rest_port',
default=8457,
help='Port to connect to Nexenta REST API server.'),
cfg.IntOpt('nexenta_retry_count',
default=6,
help='Number of retries for unsuccessful API calls.'),
cfg.StrOpt('nexenta_rest_protocol',
default='auto',
choices=['http', 'https', 'auto'],
help='Use http or https for REST connection (default auto).'),
cfg.StrOpt('nexenta_user',
default='admin',
help='User name to connect to Nexenta SA.'),
cfg.StrOpt('nexenta_password',
help='Password to connect to Nexenta SA.',
secret=True),
cfg.StrOpt('nexenta_volume',
default='volume1',
help='Volume name on NexentaStor.'),
cfg.StrOpt('nexenta_pool',
default='pool1',
help='Pool name on NexentaStor.'),
cfg.BoolOpt('nexenta_nfs',
default=True,
help='On if share over NFS is enabled.'),
]
nexenta_nfs_opts = [
cfg.StrOpt('nexenta_mount_point_base',
default='$state_path/mnt',
help='Base directory that contains NFS share mount points.'),
]
nexenta_dataset_opts = [
cfg.StrOpt('nexenta_nfs_share',
default='nfs_share',
help='Parent folder on NexentaStor.'),
cfg.StrOpt('nexenta_dataset_compression',
default='on',
choices=['on', 'off', 'gzip', 'gzip-1', 'gzip-2', 'gzip-3',
'gzip-4', 'gzip-5', 'gzip-6', 'gzip-7', 'gzip-8',
'gzip-9', 'lzjb', 'zle', 'lz4'],
help='Compression value for new ZFS folders.'),
cfg.StrOpt('nexenta_dataset_dedupe',
default='off',
choices=['on', 'off', 'sha256', 'verify', 'sha256, verify'],
help='Deduplication value for new ZFS folders.'),
cfg.BoolOpt('nexenta_thin_provisioning',
default=True,
help=('If True shares will not be space guaranteed and '
'overprovisioning will be enabled.')),
]

View File

@ -0,0 +1,54 @@
# Copyright 2016 Nexenta 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.
import re
import six
from oslo_utils import units
def str2size(s, scale=1024):
"""Convert size-string.
String format: <value>[:space:]<B | K | M | ...> to bytes.
:param s: size-string
:param scale: base size
"""
if not s:
return 0
if isinstance(s, six.integer_types):
return s
match = re.match(r'^([\.\d]+)\s*([BbKkMmGgTtPpEeZzYy]?)', s)
if match is None:
raise ValueError('Invalid value: %s' % s)
groups = match.groups()
value = float(groups[0])
suffix = len(groups) > 1 and groups[1].upper() or 'B'
types = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
for i, t in enumerate(types):
if suffix == t:
return float(value * pow(scale, i))
def str2gib_size(s):
"""Covert size-string to size in gigabytes."""
size_in_bytes = str2size(s)
return size_in_bytes // units.Gi
def bytes_to_gb(size):
return float(size) / units.Gi

View File

@ -0,0 +1,38 @@
# Copyright 2016 Nexenta 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.
from mock import patch
from oslo_serialization import jsonutils
import requests
from manila import exception
from manila.share.drivers.nexenta.ns4 import jsonrpc
from manila import test
class TestNexentaJSONProxy(test.TestCase):
@patch('requests.post')
def test_call(self, post):
nms_post = jsonrpc.NexentaJSONProxy(
'http', '1.1.1.1', '8080', 'user', 'pass',
'obj', auto=False, method='get')
data = {'error': {'message': 'some_error'}}
post.return_value = requests.Response()
post.return_value.__setstate__({
'status_code': 500, '_content': jsonutils.dumps(data)})
self.assertRaises(exception.NexentaException, nms_post)

View File

@ -0,0 +1,606 @@
# Copyright 2016 Nexenta 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.
import base64
import json
import mock
from mock import patch
from mock import PropertyMock
from oslo_serialization import jsonutils
from oslo_utils import units
from manila import context
from manila import exception
from manila.share import configuration as conf
from manila.share.drivers.nexenta.ns4 import nexenta_nas
from manila import test
PATH_TO_RPC = 'requests.post'
CODE = PropertyMock(return_value=200)
class FakeResponse(object):
def __init__(self, response={}):
self.content = json.dumps(response)
super(FakeResponse, self).__init__()
def close(self):
pass
class RequestParams(object):
def __init__(self, scheme, host, port, path, user, password):
self.scheme = scheme.lower()
self.host = host
self.port = port
self.path = path
self.user = user
self.password = password
@property
def url(self):
return '%s://%s:%s%s' % (self.scheme, self.host, self.port, self.path)
@property
def headers(self):
auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))
headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic %s' % auth,
}
return headers
def build_post_args(self, obj, method, *args):
data = jsonutils.dumps({
'object': obj,
'method': method,
'params': args,
})
return data
class TestNexentaNasDriver(test.TestCase):
def _get_share_path(self, share_name):
return '%s/%s/%s' % (self.volume, self.share, share_name)
def setUp(self):
def _safe_get(opt):
return getattr(self.cfg, opt)
self.cfg = mock.Mock(spec=conf.Configuration)
self.cfg.nexenta_host = '1.1.1.1'
super(TestNexentaNasDriver, self).setUp()
self.ctx = context.get_admin_context()
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
self.cfg.nexenta_rest_port = 1000
self.cfg.reserved_share_percentage = 0
self.cfg.max_over_subscription_ratio = 0
self.cfg.nexenta_rest_protocol = 'auto'
self.cfg.nexenta_volume = 'volume'
self.cfg.nexenta_nfs_share = 'nfs_share'
self.cfg.nexenta_user = 'user'
self.cfg.nexenta_password = 'password'
self.cfg.nexenta_thin_provisioning = False
self.cfg.enabled_share_protocols = 'NFS'
self.cfg.nexenta_mount_point_base = '$state_path/mnt'
self.cfg.share_backend_name = 'NexentaStor'
self.cfg.nexenta_dataset_compression = 'on'
self.cfg.nexenta_smb = 'on'
self.cfg.nexenta_nfs = 'on'
self.cfg.nexenta_dataset_dedupe = 'on'
self.cfg.network_config_group = 'DEFAULT'
self.cfg.admin_network_config_group = (
'fake_admin_network_config_group')
self.cfg.driver_handles_share_servers = False
self.request_params = RequestParams(
'http', self.cfg.nexenta_host, self.cfg.nexenta_rest_port,
'/rest/nms/', self.cfg.nexenta_user, self.cfg.nexenta_password)
self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg)
self.drv.do_setup(self.ctx)
self.volume = self.cfg.nexenta_volume
self.share = self.cfg.nexenta_nfs_share
@patch(PATH_TO_RPC)
def test_check_for_setup_error__volume_doesnt_exist(self, post):
post.return_value = FakeResponse()
self.assertRaises(
exception.NexentaException, self.drv.check_for_setup_error)
@patch(PATH_TO_RPC)
def test_check_for_setup_error__folder_doesnt_exist(self, post):
folder = '%s/%s' % (self.volume, self.share)
create_folder_props = {
'recordsize': '4K',
'quota': '1G',
'compression': self.cfg.nexenta_dataset_compression,
'sharesmb': self.cfg.nexenta_smb,
'sharenfs': self.cfg.nexenta_nfs,
}
share_opts = {
'read_write': '*',
'read_only': '',
'root': 'nobody',
'extra_options': 'anon=0',
'recursive': 'true',
'anonymous_rw': 'true',
}
def my_side_effect(*args, **kwargs):
if kwargs['data'] == self.request_params.build_post_args(
'volume', 'object_exists', self.volume):
return FakeResponse({'result': 'OK'})
elif kwargs['data'] == self.request_params.build_post_args(
'folder', 'object_exists', folder):
return FakeResponse()
elif kwargs['data'] == self.request_params.build_post_args(
'folder', 'create_with_props', self.volume, self.share,
create_folder_props):
return FakeResponse()
elif kwargs['data'] == self.request_params.build_post_args(
'netstorsvc', 'share_folder',
'svc:/network/nfs/server:default', folder, share_opts):
return FakeResponse()
else:
raise exception.ManilaException('Unexpected request')
post.side_effect = my_side_effect
self.assertRaises(
exception.ManilaException, self.drv.check_for_setup_error)
post.assert_any_call(
self.request_params.url, data=self.request_params.build_post_args(
'volume', 'object_exists', self.volume),
headers=self.request_params.headers)
post.assert_any_call(
self.request_params.url, data=self.request_params.build_post_args(
'folder', 'object_exists', folder),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_create_share(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
self.cfg.nexenta_thin_provisioning = False
path = '%s/%s/%s' % (self.volume, self.share, share['name'])
location = {'path': '%s:/volumes/%s' % (self.cfg.nexenta_host, path)}
post.return_value = FakeResponse()
self.assertEqual([location],
self.drv.create_share(self.ctx, share))
@patch(PATH_TO_RPC)
def test_create_share__wrong_proto(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': 'A_VERY_WRONG_PROTO'
}
post.return_value = FakeResponse()
self.assertRaises(exception.InvalidShare, self.drv.create_share,
self.ctx, share)
@patch(PATH_TO_RPC)
def test_create_share__thin_provisioning(self, post):
share = {'name': 'share', 'size': 1,
'share_proto': self.cfg.enabled_share_protocols}
create_folder_props = {
'recordsize': '4K',
'quota': '1G',
'compression': self.cfg.nexenta_dataset_compression,
}
parent_path = '%s/%s' % (self.volume, self.share)
post.return_value = FakeResponse()
self.cfg.nexenta_thin_provisioning = True
self.drv.create_share(self.ctx, share)
post.assert_called_with(
self.request_params.url,
data=self.request_params.build_post_args(
'folder',
'create_with_props',
parent_path,
share['name'],
create_folder_props),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_create_share__thick_provisioning(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
quota = '%sG' % share['size']
create_folder_props = {
'recordsize': '4K',
'quota': quota,
'compression': self.cfg.nexenta_dataset_compression,
'reservation': quota,
}
parent_path = '%s/%s' % (self.volume, self.share)
post.return_value = FakeResponse()
self.cfg.nexenta_thin_provisioning = False
self.drv.create_share(self.ctx, share)
post.assert_called_with(
self.request_params.url,
data=self.request_params.build_post_args(
'folder',
'create_with_props',
parent_path,
share['name'],
create_folder_props),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_create_share_from_snapshot(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
snapshot = {'name': 'sn1', 'share_name': share['name']}
post.return_value = FakeResponse()
path = '%s/%s/%s' % (self.volume, self.share, share['name'])
location = {'path': '%s:/volumes/%s' % (self.cfg.nexenta_host, path)}
snapshot_name = '%s/%s/%s@%s' % (
self.volume, self.share, snapshot['share_name'], snapshot['name'])
self.assertEqual([location], self.drv.create_share_from_snapshot(
self.ctx, share, snapshot))
post.assert_any_call(
self.request_params.url,
data=self.request_params.build_post_args(
'folder',
'clone',
snapshot_name,
'%s/%s/%s' % (self.volume, self.share, share['name'])),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_delete_share(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
post.return_value = FakeResponse()
folder = '%s/%s/%s' % (self.volume, self.share, share['name'])
self.drv.delete_share(self.ctx, share)
post.assert_any_call(
self.request_params.url,
data=self.request_params.build_post_args(
'folder',
'destroy',
folder.strip(),
'-r'),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_delete_share__exists_error(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
post.return_value = FakeResponse()
post.side_effect = exception.NexentaException('does not exist')
self.drv.delete_share(self.ctx, share)
@patch(PATH_TO_RPC)
def test_delete_share__some_error(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
post.return_value = FakeResponse()
post.side_effect = exception.ManilaException('Some error')
self.assertRaises(
exception.ManilaException, self.drv.delete_share, self.ctx, share)
@patch(PATH_TO_RPC)
def test_extend_share__thin_provisoning(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
new_size = 5
quota = '%sG' % new_size
post.return_value = FakeResponse()
self.cfg.nexenta_thin_provisioning = True
self.drv.extend_share(share, new_size)
post.assert_called_with(
self.request_params.url,
data=self.request_params.build_post_args(
'folder',
'set_child_prop',
'%s/%s/%s' % (self.volume, self.share, share['name']),
'quota', quota),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_extend_share__thick_provisoning(self, post):
share = {
'name': 'share',
'size': 1,
'share_proto': self.cfg.enabled_share_protocols
}
new_size = 5
post.return_value = FakeResponse()
self.cfg.nexenta_thin_provisioning = False
self.drv.extend_share(share, new_size)
post.assert_not_called()
@patch(PATH_TO_RPC)
def test_create_snapshot(self, post):
snapshot = {'share_name': 'share', 'name': 'share@first'}
post.return_value = FakeResponse()
folder = '%s/%s/%s' % (self.volume, self.share, snapshot['share_name'])
self.drv.create_snapshot(self.ctx, snapshot)
post.assert_called_with(
self.request_params.url, data=self.request_params.build_post_args(
'folder', 'create_snapshot', folder, snapshot['name'], '-r'),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_delete_snapshot(self, post):
snapshot = {'share_name': 'share', 'name': 'share@first'}
post.return_value = FakeResponse()
self.drv.delete_snapshot(self.ctx, snapshot)
post.assert_called_with(
self.request_params.url, data=self.request_params.build_post_args(
'snapshot', 'destroy', '%s@%s' % (
self._get_share_path(snapshot['share_name']),
snapshot['name']),
''),
headers=self.request_params.headers)
@patch(PATH_TO_RPC)
def test_delete_snapshot__nexenta_error_1(self, post):
snapshot = {'share_name': 'share', 'name': 'share@first'}
post.return_value = FakeResponse()
post.side_effect = exception.NexentaException('does not exist')
self.drv.delete_snapshot(self.ctx, snapshot)
@patch(PATH_TO_RPC)
def test_delete_snapshot__nexenta_error_2(self, post):
snapshot = {'share_name': 'share', 'name': 'share@first'}
post.return_value = FakeResponse()
post.side_effect = exception.NexentaException('has dependent clones')
self.drv.delete_snapshot(self.ctx, snapshot)
@patch(PATH_TO_RPC)
def test_delete_snapshot__some_error(self, post):
snapshot = {'share_name': 'share', 'name': 'share@first'}
post.return_value = FakeResponse()
post.side_effect = exception.ManilaException('Some error')
self.assertRaises(exception.ManilaException, self.drv.delete_snapshot,
self.ctx, snapshot)
@patch(PATH_TO_RPC)
def test_update_access__unsupported_access_type(self, post):
share = {
'name': 'share',
'share_proto': self.cfg.enabled_share_protocols
}
access = {
'access_type': 'group',
'access_to': 'ordinary_users',
'access_level': 'rw'
}
self.assertRaises(exception.InvalidShareAccess,
self.drv.update_access,
self.ctx,
share,
[access],
None,
None)
@patch(PATH_TO_RPC)
def test_update_access__cidr(self, post):
share = {
'name': 'share',
'share_proto': self.cfg.enabled_share_protocols
}
access1 = {
'access_type': 'ip',
'access_to': '1.1.1.1/24',
'access_level': 'rw'
}
access2 = {
'access_type': 'ip',
'access_to': '1.2.3.4',
'access_level': 'rw'
}
access_rules = [access1, access2]
share_opts = {
'auth_type': 'none',
'read_write': '%s:%s' % (
access1['access_to'], access2['access_to']),
'read_only': '',
'recursive': 'true',
'anonymous_rw': 'true',
'anonymous': 'true',
'extra_options': 'anon=0',
}
def my_side_effect(*args, **kwargs):
if kwargs['data'] == self.request_params.build_post_args(
'netstorsvc', 'share_folder',
'svc:/network/nfs/server:default',
self._get_share_path(share['name']), share_opts):
return FakeResponse()
else:
raise exception.ManilaException('Unexpected request')
post.return_value = FakeResponse()
post.side_effect = my_side_effect
self.drv.update_access(self.ctx, share, access_rules, None, None)
post.assert_called_with(
self.request_params.url, data=self.request_params.build_post_args(
'netstorsvc', 'share_folder',
'svc:/network/nfs/server:default',
self._get_share_path(share['name']), share_opts),
headers=self.request_params.headers)
self.assertRaises(exception.ManilaException, self.drv.update_access,
self.ctx, share,
[access1, {'access_type': 'ip',
'access_to': '2.2.2.2',
'access_level': 'rw'}],
None, None)
@patch(PATH_TO_RPC)
def test_update_access__add_one_ip_to_empty_access_list(self, post):
share = {'name': 'share',
'share_proto': self.cfg.enabled_share_protocols}
access = {
'access_type': 'ip',
'access_to': '1.1.1.1',
'access_level': 'rw'
}
rw_list = None
share_opts = {
'auth_type': 'none',
'read_write': access['access_to'],
'read_only': '',
'recursive': 'true',
'anonymous_rw': 'true',
'anonymous': 'true',
'extra_options': 'anon=0',
}
def my_side_effect(*args, **kwargs):
if kwargs['data'] == self.request_params.build_post_args(
'netstorsvc', 'get_shareopts',
'svc:/network/nfs/server:default',
self._get_share_path(share['name'])):
return FakeResponse({'result': {'read_write': rw_list}})
elif kwargs['data'] == self.request_params.build_post_args(
'netstorsvc', 'share_folder',
'svc:/network/nfs/server:default',
self._get_share_path(share['name']), share_opts):
return FakeResponse()
else:
raise exception.ManilaException('Unexpected request')
post.return_value = FakeResponse()
self.drv.update_access(self.ctx, share, [access], None, None)
post.assert_called_with(
self.request_params.url, data=self.request_params.build_post_args(
'netstorsvc', 'share_folder',
'svc:/network/nfs/server:default',
self._get_share_path(share['name']), share_opts),
headers=self.request_params.headers)
post.side_effect = my_side_effect
self.assertRaises(exception.ManilaException, self.drv.update_access,
self.ctx, share,
[{'access_type': 'ip',
'access_to': '1111',
'access_level': 'rw'}],
None, None)
@patch(PATH_TO_RPC)
def test_deny_access__unsupported_access_type(self, post):
share = {'name': 'share',
'share_proto': self.cfg.enabled_share_protocols}
access = {
'access_type': 'group',
'access_to': 'ordinary_users',
'access_level': 'rw'
}
self.assertRaises(exception.InvalidShareAccess, self.drv.update_access,
self.ctx, share, [access], None, None)
def test_share_backend_name(self):
self.assertEqual('NexentaStor', self.drv.share_backend_name)
@patch(PATH_TO_RPC)
def test_get_capacity_info(self, post):
post.return_value = FakeResponse({'result': {
'available': 9 * units.Gi, 'used': 1 * units.Gi}})
self.assertEqual(
(10, 9, 1), self.drv.helper._get_capacity_info())
@patch('manila.share.drivers.nexenta.ns4.nexenta_nfs_helper.NFSHelper.'
'_get_capacity_info')
@patch('manila.share.driver.ShareDriver._update_share_stats')
def test_update_share_stats(self, super_stats, info):
info.return_value = (100, 90, 10)
stats = {
'vendor_name': 'Nexenta',
'storage_protocol': 'NFS',
'nfs_mount_point_base': self.cfg.nexenta_mount_point_base,
'driver_version': '1.0',
'share_backend_name': self.cfg.share_backend_name,
'pools': [{
'total_capacity_gb': 100,
'free_capacity_gb': 90,
'pool_name': 'volume',
'reserved_percentage': (
self.cfg.reserved_share_percentage),
'compression': True,
'dedupe': True,
'thin_provisioning': self.cfg.nexenta_thin_provisioning,
'max_over_subscription_ratio': (
self.cfg.safe_get(
'max_over_subscription_ratio')),
}],
}
self.drv._update_share_stats()
self.assertEqual(stats, self.drv._stats)

View File

@ -0,0 +1,129 @@
# Copyright 2016 Nexenta 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.
from mock import patch
from oslo_serialization import jsonutils
import requests
from manila import exception
from manila.share.drivers.nexenta.ns5 import jsonrpc
from manila import test
PATH_TO_RPC = 'manila.share.drivers.nexenta.ns5.jsonrpc.NexentaJSONProxy'
class TestNexentaJSONProxy(test.TestCase):
def __init__(self, method):
super(self.__class__, self).__init__(method)
def setUp(self):
super(self.__class__, self).setUp()
self.nef_get = jsonrpc.NexentaJSONProxy(
'http', '1.1.1.1', '8080', 'user', 'pass', 'get')
self.nef_post = jsonrpc.NexentaJSONProxy(
'https', '1.1.1.1', '8080', 'user', 'pass', 'post')
@patch('requests.Response.close')
@patch('requests.Session.get')
def test_call_get_data(self, get, close):
data = {'key': 'value'}
get.return_value = requests.Response()
get.return_value.__setstate__(
{'status_code': 200, '_content': jsonutils.dumps(data)})
self.assertEqual({'key': 'value'}, self.nef_get('url'))
@patch('requests.Response.close')
@patch('requests.Session.get')
def test_call_get_created(self, get, close):
get.return_value = requests.Response()
get.return_value.__setstate__({
'status_code': 201, '_content': ''})
self.assertIsNone(self.nef_get('url'))
@patch('requests.Response.close')
@patch('requests.Session.post')
def test_call_post_success(self, post, close):
data = {'key': 'value'}
post.return_value = requests.Response()
post.return_value.__setstate__({
'status_code': 200, '_content': ''})
self.assertIsNone(self.nef_post('url', data))
@patch('time.sleep')
@patch('requests.Response.close')
@patch('requests.Session.get')
@patch('requests.Session.post')
def test_call_post_202(self, post, get, close, sleep):
data = {'key': 'value'}
data2 = {'links': [{'href': 'redirect_url'}]}
get.return_value = requests.Response()
post.return_value = requests.Response()
post.return_value.__setstate__({
'status_code': 202, '_content': jsonutils.dumps(data2)})
get.return_value.__setstate__({
'status_code': 200, '_content': jsonutils.dumps(data)})
self.assertEqual({'key': 'value'}, self.nef_post('url'))
@patch('requests.Response.close')
@patch('requests.Session.get')
def test_call_get_not_exist(self, get, close):
get.return_value = requests.Response()
get.return_value.__setstate__({
'status_code': 400,
'_content': jsonutils.dumps({'code': 'ENOENT'})})
self.assertRaises(
exception.NexentaException, lambda: self.nef_get('url'))
@patch('requests.Response.close')
@patch('requests.Session.get')
def test_call_get_unauthorized(self, get, close):
get.return_value = requests.Response()
get.return_value.__setstate__({
'status_code': 401,
'_content': jsonutils.dumps({'code': 'unauthorized'})})
self.assertRaises(
exception.NexentaException, lambda: self.nef_get('url'))
@patch('%s.https_auth' % PATH_TO_RPC)
@patch('requests.Response.close')
@patch('requests.Session.post')
def test_call_post_bad_token(self, post, close, auth):
post.return_value = requests.Response()
auth.return_value = {'token': 'tok'}
post.return_value.__setstate__({
'status_code': 401,
'_content': jsonutils.dumps({'code': 'unauthorized'})})
self.assertRaises(
exception.NexentaException, lambda: self.nef_post('url'))
@patch('requests.Response.close')
@patch('requests.Session.post')
def test_auth(self, post, close):
httpsdata = {'token': 'tok'}
post.return_value = requests.Response()
post.return_value.__setstate__({
'status_code': 200, '_content': jsonutils.dumps(httpsdata)})
nef_get = jsonrpc.NexentaJSONProxy(
'http', '1.1.1.1', '8080', 'user', 'pass', method='get')
https_auth = nef_get.https_auth()
self.assertEqual('tok', https_auth)

View File

@ -0,0 +1,378 @@
# Copyright 2016 Nexenta 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.
import ddt
import mock
from mock import patch
from oslo_utils import units
from manila import context
from manila import exception
from manila.share import configuration as conf
from manila.share.drivers.nexenta.ns5 import nexenta_nas
from manila import test
PATH_TO_RPC = 'manila.share.drivers.nexenta.ns5.jsonrpc.NexentaJSONProxy'
DRV_PATH = 'manila.share.drivers.nexenta.ns5.nexenta_nas.NexentaNasDriver'
@ddt.ddt
class TestNexentaNasDriver(test.TestCase):
def setUp(self):
def _safe_get(opt):
return getattr(self.cfg, opt)
self.cfg = conf.Configuration(None)
self.cfg.nexenta_host = '1.1.1.1'
super(self.__class__, self).setUp()
self.ctx = context.get_admin_context()
self.mock_object(
self.cfg, 'safe_get', mock.Mock(side_effect=_safe_get))
self.cfg.nexenta_rest_port = 8080
self.cfg.nexenta_rest_protocol = 'auto'
self.cfg.nexenta_pool = 'pool1'
self.cfg.reserved_share_percentage = 0
self.cfg.nexenta_nfs_share = 'nfs_share'
self.cfg.nexenta_user = 'user'
self.cfg.share_backend_name = 'NexentaStor5'
self.cfg.nexenta_password = 'password'
self.cfg.nexenta_thin_provisioning = False
self.cfg.nexenta_mount_point_base = 'mnt'
self.cfg.enabled_share_protocols = 'NFS'
self.cfg.nexenta_mount_point_base = '$state_path/mnt'
self.cfg.nexenta_dataset_compression = 'on'
self.cfg.network_config_group = 'DEFAULT'
self.cfg.admin_network_config_group = (
'fake_admin_network_config_group')
self.cfg.driver_handles_share_servers = False
self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg)
self.drv.do_setup(self.ctx)
self.mock_rpc = self.mock_class(PATH_TO_RPC)
self.pool_name = self.cfg.nexenta_pool
self.fs_prefix = self.cfg.nexenta_nfs_share
def test_backend_name(self):
self.assertEqual('NexentaStor5', self.drv.share_backend_name)
@patch('%s._get_provisioned_capacity' % DRV_PATH)
def test_check_for_setup_error(self, mock_provisioned):
self.drv.nef.get.return_value = None
self.assertRaises(LookupError, self.drv.check_for_setup_error)
@patch('%s._get_provisioned_capacity' % DRV_PATH)
def test_check_for_setup_error__none(self, mock_provisioned):
self.drv.nef.get.return_value = {
'data': [{'filesystem': 'pool1/nfs_share', 'quotaSize': 1}]
}
self.assertIsNone(self.drv.check_for_setup_error())
@patch('%s._get_provisioned_capacity' % DRV_PATH)
def test_check_for_setup_error__with_data(self, mock_provisioned):
self.drv.nef.get.return_value = {
'data': [{'filesystem': 'asd', 'quotaSize': 1}]}
self.assertRaises(LookupError, self.drv.check_for_setup_error)
def test__get_provisioned_capacity(self):
self.drv.nef.get.return_value = {
'data': [
{'path': 'pool1/nfs_share/123', 'quotaSize': 1 * units.Gi}]
}
self.drv._get_provisioned_capacity()
self.assertEqual(1, self.drv.provisioned_capacity)
def test_create_share(self):
share = {'name': 'share', 'size': 1}
self.assertEqual(
[{
'path': '{}:/{}/{}/{}'.format(
self.cfg.nexenta_host, self.pool_name,
self.fs_prefix, share['name'])
}],
self.drv.create_share(self.ctx, share))
@patch('%s.delete_share' % DRV_PATH)
@patch('%s._add_permission' % DRV_PATH)
def test_create_share__error_on_add_permission(
self, add_permission_mock, delete_share):
share = {'name': 'share', 'size': 1}
add_permission_mock.side_effect = exception.NexentaException(
'An error occurred while adding permission')
delete_share.side_effect = exception.NexentaException(
'An error occurred while deleting')
self.assertRaises(
exception.NexentaException, self.drv.create_share, self.ctx, share)
def test_create_share_from_snapshot(self):
share = {'name': 'share', 'size': 1}
snapshot = {'name': 'share@first', 'share_name': 'share'}
self.assertEqual(
[{
'path': '{}:/{}/{}/{}'.format(
self.cfg.nexenta_host, self.pool_name,
self.fs_prefix, share['name'])
}],
self.drv.create_share_from_snapshot(self.ctx, share, snapshot)
)
@patch('%s.delete_share' % DRV_PATH)
@patch('%s._add_permission' % DRV_PATH)
def test_create_share_from_snapshot__add_permission_error(
self, add_permission_mock, delete_share):
share = {'name': 'share', 'size': 1}
snapshot = {'share_name': 'share', 'name': 'share@first'}
delete_share.side_effect = exception.NexentaException(
'An error occurred while deleting')
add_permission_mock.side_effect = exception.NexentaException(
'Some exception')
self.assertRaises(
exception.NexentaException, self.drv.create_share_from_snapshot,
self.ctx, share, snapshot)
@patch('%s._add_permission' % DRV_PATH)
def test_create_share_from_snapshot__add_permission_error_error(
self, add_permission_mock):
share = {'name': 'share', 'size': 1}
snapshot = {'share_name': 'share', 'name': 'share@first'}
add_permission_mock.side_effect = exception.NexentaException(
'Some exception')
self.drv.nef.delete.side_effect = exception.NexentaException(
'Some exception 2')
self.assertRaises(
exception.NexentaException, self.drv.create_share_from_snapshot,
self.ctx, share, snapshot)
def test_delete_share(self):
share = {'name': 'share', 'size': 1}
self.assertIsNone(self.drv.delete_share(self.ctx, share))
def test_extend_share(self):
share = {'name': 'share', 'size': 1}
new_size = 2
quota = new_size * units.Gi
data = {
'reservationSize': quota,
'quotaSize': quota,
}
url = 'storage/pools/{}/filesystems/{}%2F{}'.format(
self.pool_name, self.fs_prefix, share['name'])
self.drv.extend_share(share, new_size)
self.drv.nef.post.assert_called_with(url, data)
def test_shrink_share(self):
share = {'name': 'share', 'size': 2}
new_size = 1
quota = new_size * units.Gi
data = {
'reservationSize': quota,
'quotaSize': quota
}
url = 'storage/pools/{}/filesystems/{}%2F{}'.format(
self.pool_name, self.fs_prefix, share['name'])
self.drv.nef.get.return_value = {'bytesUsed': 512}
self.drv.shrink_share(share, new_size)
self.drv.nef.post.assert_called_with(url, data)
def test_create_snapshot(self):
snapshot = {'share_name': 'share', 'name': 'share@first'}
url = 'storage/pools/%(pool)s/filesystems/%(fs)s/snapshots' % {
'pool': self.pool_name,
'fs': nexenta_nas.PATH_DELIMITER.join(
[self.fs_prefix, snapshot['share_name']])
}
data = {'name': snapshot['name']}
self.drv.create_snapshot(self.ctx, snapshot)
self.drv.nef.post.assert_called_with(url, data)
def test_delete_snapshot(self):
self.mock_rpc.side_effect = exception.NexentaException(
'err', code='ENOENT')
snapshot = {'share_name': 'share', 'name': 'share@first'}
self.assertIsNone(self.drv.delete_snapshot(self.ctx, snapshot))
self.mock_rpc.side_effect = exception.NexentaException(
'err', code='somecode')
self.assertRaises(
exception.NexentaException, self.drv.delete_snapshot,
self.ctx, snapshot)
def build_access_security_context(self, level, ip, mask=None):
ls = [{"allow": True, "etype": "network", "entity": ip}]
if mask is not None:
ls[0]['mask'] = mask
new_sc = {
"securityModes": ["sys"],
}
if level == 'rw':
new_sc['readWriteList'] = ls
elif level == 'ro':
new_sc['readOnlyList'] = ls
else:
raise exception.ManilaException('Wrong access level')
return new_sc
def test_update_access__unsupported_access_type(self):
share = {'name': 'share', 'size': 1}
access = {
'access_type': 'group',
'access_to': 'ordinary_users',
'access_level': 'rw'
}
self.assertRaises(exception.InvalidShareAccess, self.drv.update_access,
self.ctx, share, [access], None, None)
def test_update_access__cidr(self):
share = {'name': 'share', 'size': 1}
access = {
'access_type': 'ip',
'access_to': '1.1.1.1/24',
'access_level': 'rw'
}
url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join(
(self.pool_name, self.fs_prefix, share['name']))
self.drv.nef.get.return_value = {}
self.drv.update_access(self.ctx, share, [access], None, None)
self.drv.nef.put.assert_called_with(
url, {'securityContexts': [
self.build_access_security_context('rw', '1.1.1.1', 24)]})
def test_update_access__ip(self):
share = {'name': 'share', 'size': 1}
access = {
'access_type': 'ip',
'access_to': '1.1.1.1',
'access_level': 'rw'
}
url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join(
(self.pool_name, self.fs_prefix, share['name']))
self.drv.nef.get.return_value = {}
self.drv.update_access(self.ctx, share, [access], None, None)
self.drv.nef.put.assert_called_with(
url, {'securityContexts': [
self.build_access_security_context('rw', '1.1.1.1')]})
@ddt.data('rw', 'ro')
def test_update_access__cidr_wrong_mask(self, access_level):
share = {'name': 'share', 'size': 1}
access = {
'access_type': 'ip',
'access_to': '1.1.1.1/aa',
'access_level': access_level,
}
self.assertRaises(exception.InvalidInput, self.drv.update_access,
self.ctx, share, [access], None, None)
def test_update_access__one_ip_ro_add_rule_to_existing(self):
share = {'name': 'share', 'size': 1}
access = [
{
'access_type': 'ip',
'access_to': '5.5.5.5',
'access_level': 'ro'
},
{
'access_type': 'ip',
'access_to': '1.1.1.1/24',
'access_level': 'rw'
}
]
url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join(
(self.pool_name, self.fs_prefix, share['name']))
sc = self.build_access_security_context('rw', '1.1.1.1', 24)
self.drv.nef.get.return_value = {'securityContexts': [sc]}
self.drv.update_access(self.ctx, share, access, None, None)
self.drv.nef.put.assert_called_with(
url, {'securityContexts': [
sc, self.build_access_security_context('ro', '5.5.5.5')]})
def test_update_access__one_ip_ro_add_rule_to_existing_wrong_mask(
self):
share = {'name': 'share', 'size': 1}
access = [
{
'access_type': 'ip',
'access_to': '5.5.5.5/aa',
'access_level': 'ro'
},
{
'access_type': 'ip',
'access_to': '1.1.1.1/24',
'access_level': 'rw'
}
]
sc = self.build_access_security_context('rw', '1.1.1.1', 24)
self.drv.nef.get.return_value = {'securityContexts': [sc]}
self.assertRaises(exception.InvalidInput, self.drv.update_access,
self.ctx, share, access, None, None)
@patch('%s._get_capacity_info' % DRV_PATH)
@patch('manila.share.driver.ShareDriver._update_share_stats')
def test_update_share_stats(self, super_stats, info):
info.return_value = (100, 90, 10)
stats = {
'vendor_name': 'Nexenta',
'storage_protocol': 'NFS',
'nfs_mount_point_base': self.cfg.nexenta_mount_point_base,
'driver_version': '1.0',
'share_backend_name': self.cfg.share_backend_name,
'pools': [{
'pool_name': 'pool1',
'total_capacity_gb': 100,
'free_capacity_gb': 90,
'provisioned_capacity_gb': 0,
'max_over_subscription_ratio': 20.0,
'reserved_percentage': (
self.cfg.reserved_share_percentage),
'thin_provisioning': self.cfg.nexenta_thin_provisioning,
}],
}
self.drv._update_share_stats()
self.assertEqual(stats, self.drv._stats)
def test_get_capacity_info(self):
self.drv.nef.get.return_value = {
'bytesAvailable': 10 * units.Gi, 'bytesUsed': 1 * units.Gi}
self.assertEqual((10, 9, 1), self.drv._get_capacity_info())

View File

@ -0,0 +1,49 @@
# Copyright 2016 Nexenta 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.
import ddt
from oslo_utils import units
from manila.share.drivers.nexenta import utils
from manila import test
@ddt.ddt
class TestNexentaUtils(test.TestCase):
@ddt.data(
# Test empty value
(None, 0),
('', 0),
('0', 0),
('12', 12),
# Test int values
(10, 10),
# Test bytes string
('1b', 1),
('1B', 1),
('1023b', 1023),
('0B', 0),
# Test other units
('1M', units.Mi),
('1.0M', units.Mi),
)
@ddt.unpack
def test_str2size(self, value, result):
self.assertEqual(result, utils.str2size(value))
def test_str2size_input_error(self):
# Invalid format value
self.assertRaises(ValueError, utils.str2size, 'A')

View File

@ -0,0 +1,3 @@
features:
- Added share backend drivers for NexentaStor4 and NexentaStor5 appliances.