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:
parent
98da15991a
commit
1fd7e88191
@ -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::
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
0
manila/share/drivers/nexenta/__init__.py
Normal file
0
manila/share/drivers/nexenta/__init__.py
Normal file
0
manila/share/drivers/nexenta/ns4/__init__.py
Normal file
0
manila/share/drivers/nexenta/ns4/__init__.py
Normal file
92
manila/share/drivers/nexenta/ns4/jsonrpc.py
Normal file
92
manila/share/drivers/nexenta/ns4/jsonrpc.py
Normal 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')
|
134
manila/share/drivers/nexenta/ns4/nexenta_nas.py
Normal file
134
manila/share/drivers/nexenta/ns4/nexenta_nas.py
Normal 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)
|
227
manila/share/drivers/nexenta/ns4/nexenta_nfs_helper.py
Normal file
227
manila/share/drivers/nexenta/ns4/nexenta_nfs_helper.py
Normal 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,
|
||||
}],
|
||||
}
|
0
manila/share/drivers/nexenta/ns5/__init__.py
Normal file
0
manila/share/drivers/nexenta/ns5/__init__.py
Normal file
145
manila/share/drivers/nexenta/ns5/jsonrpc.py
Normal file
145
manila/share/drivers/nexenta/ns5/jsonrpc.py
Normal 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})
|
422
manila/share/drivers/nexenta/ns5/nexenta_nas.py
Normal file
422
manila/share/drivers/nexenta/ns5/nexenta_nas.py
Normal 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)
|
79
manila/share/drivers/nexenta/options.py
Normal file
79
manila/share/drivers/nexenta/options.py
Normal 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.')),
|
||||
]
|
54
manila/share/drivers/nexenta/utils.py
Normal file
54
manila/share/drivers/nexenta/utils.py
Normal 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
|
0
manila/tests/share/drivers/nexenta/__init__.py
Normal file
0
manila/tests/share/drivers/nexenta/__init__.py
Normal file
0
manila/tests/share/drivers/nexenta/ns4/__init__.py
Normal file
0
manila/tests/share/drivers/nexenta/ns4/__init__.py
Normal file
38
manila/tests/share/drivers/nexenta/ns4/test_jsonrpc.py
Normal file
38
manila/tests/share/drivers/nexenta/ns4/test_jsonrpc.py
Normal 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)
|
606
manila/tests/share/drivers/nexenta/ns4/test_nexenta_nas.py
Normal file
606
manila/tests/share/drivers/nexenta/ns4/test_nexenta_nas.py
Normal 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)
|
0
manila/tests/share/drivers/nexenta/ns5/__init__.py
Normal file
0
manila/tests/share/drivers/nexenta/ns5/__init__.py
Normal file
129
manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py
Normal file
129
manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py
Normal 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)
|
378
manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py
Normal file
378
manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py
Normal 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())
|
49
manila/tests/share/drivers/nexenta/test_utils.py
Normal file
49
manila/tests/share/drivers/nexenta/test_utils.py
Normal 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')
|
@ -0,0 +1,3 @@
|
||||
features:
|
||||
- Added share backend drivers for NexentaStor4 and NexentaStor5 appliances.
|
||||
|
Loading…
Reference in New Issue
Block a user