manila/manila/share/drivers/nexenta/ns5/nexenta_nas.py

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)