423 lines
17 KiB
Python
423 lines
17 KiB
Python
# 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 _
|
|
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(
|
|
"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(
|
|
('Failed to add permissions for %s'), share['name'])
|
|
try:
|
|
self.delete_share(None, share)
|
|
except exception.NexentaException:
|
|
LOG.warning("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(
|
|
'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)
|