928 lines
40 KiB
Python
928 lines
40 KiB
Python
# Copyright (c) 2016 QNAP Systems, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
Share driver for QNAP Storage.
|
|
This driver supports QNAP Storage for NFS.
|
|
"""
|
|
import datetime
|
|
import math
|
|
import re
|
|
import time
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import timeutils
|
|
from oslo_utils import units
|
|
|
|
from manila.common import constants
|
|
from manila import exception
|
|
from manila.i18n import _
|
|
from manila import share
|
|
from manila.share import driver
|
|
from manila.share.drivers.qnap import api
|
|
from manila.share import share_types
|
|
from manila import utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
qnap_manila_opts = [
|
|
cfg.StrOpt('qnap_management_url',
|
|
required=True,
|
|
help='The URL to manage QNAP Storage.'),
|
|
cfg.HostAddressOpt('qnap_share_ip',
|
|
required=True,
|
|
help='NAS share IP for mounting shares.'),
|
|
cfg.StrOpt('qnap_nas_login',
|
|
required=True,
|
|
help='Username for QNAP storage.'),
|
|
cfg.StrOpt('qnap_nas_password',
|
|
required=True,
|
|
secret=True,
|
|
help='Password for QNAP storage.'),
|
|
cfg.StrOpt('qnap_poolname',
|
|
required=True,
|
|
help='Pool within which QNAP shares must be created.'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(qnap_manila_opts)
|
|
|
|
|
|
class QnapShareDriver(driver.ShareDriver):
|
|
"""OpenStack driver to enable QNAP Storage.
|
|
|
|
Version history:
|
|
1.0.0 - Initial driver (Only NFS)
|
|
1.0.1 - Add support for QES fw 1.1.4.
|
|
1.0.2 - Fix bug #1736370, QNAP Manila driver: Access rule setting is
|
|
override by the another access rule.
|
|
1.0.3 - Add supports for Thin Provisioning, SSD Cache, Deduplication
|
|
and Compression.
|
|
1.0.4 - Add support for QES fw 2.0.0.
|
|
1.0.5 - Fix bug #1773761, when user tries to manage share, the size
|
|
of managed share should not be changed.
|
|
1.0.6 - Add support for QES fw 2.1.0.
|
|
1.0.7 - Add support for QES fw on TDS series NAS model.
|
|
1.0.8 - Fix bug, driver should not manage snapshot which does not
|
|
exist in NAS.
|
|
Fix bug, driver should create share from snapshot with
|
|
specified size.
|
|
"""
|
|
|
|
DRIVER_VERSION = '1.0.8'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize QnapShareDriver."""
|
|
super(QnapShareDriver, self).__init__(False, *args, **kwargs)
|
|
self.private_storage = kwargs.get('private_storage')
|
|
self.api_executor = None
|
|
self.group_stats = {}
|
|
self.configuration.append_config_values(qnap_manila_opts)
|
|
self.share_api = share.API()
|
|
|
|
def do_setup(self, context):
|
|
"""Setup the QNAP Manila share driver."""
|
|
self.ctxt = context
|
|
LOG.debug('context: %s', context)
|
|
|
|
# Setup API Executor
|
|
try:
|
|
self.api_executor = self._create_api_executor()
|
|
except Exception:
|
|
LOG.exception('Failed to create HTTP client. Check IP '
|
|
'address, port, username, password and make '
|
|
'sure the array version is compatible.')
|
|
raise
|
|
|
|
def check_for_setup_error(self):
|
|
"""Check the status of setup."""
|
|
if self.api_executor is None:
|
|
msg = _("Failed to instantiate API client to communicate with "
|
|
"QNAP storage systems.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
def _create_api_executor(self):
|
|
"""Create API executor by NAS model."""
|
|
"""LOG.debug('CONF.qnap_nas_login=%(conf)s',
|
|
{'conf': CONF.qnap_nas_login})
|
|
LOG.debug('self.configuration.qnap_nas_login=%(conf)s',
|
|
{'conf': self.configuration.qnap_nas_login})"""
|
|
self.api_executor = api.QnapAPIExecutor(
|
|
username=self.configuration.qnap_nas_login,
|
|
password=self.configuration.qnap_nas_password,
|
|
management_url=self.configuration.qnap_management_url)
|
|
|
|
display_model_name, internal_model_name, fw_version = (
|
|
self.api_executor.get_basic_info(
|
|
self.configuration.qnap_management_url))
|
|
|
|
pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)")
|
|
matches = pattern.match(display_model_name)
|
|
|
|
if not matches:
|
|
return None
|
|
model_type = matches.group(1)
|
|
|
|
ts_model_types = (
|
|
"TS", "SS", "IS", "TVS", "TBS"
|
|
)
|
|
tes_model_types = (
|
|
"TES", "TDS"
|
|
)
|
|
es_model_types = (
|
|
"ES",
|
|
)
|
|
|
|
if model_type in ts_model_types:
|
|
if (fw_version.startswith("4.2") or fw_version.startswith("4.3")):
|
|
LOG.debug('Create TS API Executor')
|
|
# modify the pool name to pool index
|
|
self.configuration.qnap_poolname = (
|
|
self._get_ts_model_pool_id(
|
|
self.configuration.qnap_poolname))
|
|
|
|
return api.QnapAPIExecutorTS(
|
|
username=self.configuration.qnap_nas_login,
|
|
password=self.configuration.qnap_nas_password,
|
|
management_url=self.configuration.qnap_management_url)
|
|
elif model_type in tes_model_types:
|
|
if 'TS' in internal_model_name:
|
|
if (fw_version.startswith("4.2") or
|
|
fw_version.startswith("4.3")):
|
|
LOG.debug('Create TS API Executor')
|
|
# modify the pool name to pool index
|
|
self.configuration.qnap_poolname = (
|
|
self._get_ts_model_pool_id(
|
|
self.configuration.qnap_poolname))
|
|
return api.QnapAPIExecutorTS(
|
|
username=self.configuration.qnap_nas_login,
|
|
password=self.configuration.qnap_nas_password,
|
|
management_url=self.configuration.qnap_management_url)
|
|
elif "1.1.2" <= fw_version <= "2.1.9999":
|
|
LOG.debug('Create ES API Executor')
|
|
return api.QnapAPIExecutor(
|
|
username=self.configuration.qnap_nas_login,
|
|
password=self.configuration.qnap_nas_password,
|
|
management_url=self.configuration.qnap_management_url)
|
|
elif model_type in es_model_types:
|
|
if "1.1.2" <= fw_version <= "2.1.9999":
|
|
LOG.debug('Create ES API Executor')
|
|
return api.QnapAPIExecutor(
|
|
username=self.configuration.qnap_nas_login,
|
|
password=self.configuration.qnap_nas_password,
|
|
management_url=self.configuration.qnap_management_url)
|
|
|
|
msg = _('QNAP Storage model is not supported by this driver.')
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
def _get_ts_model_pool_id(self, pool_name):
|
|
"""Modify the pool name to pool index."""
|
|
pattern = re.compile(r"^(\d+)+|^Storage Pool (\d+)+")
|
|
matches = pattern.match(pool_name)
|
|
if matches.group(1):
|
|
return matches.group(1)
|
|
else:
|
|
return matches.group(2)
|
|
|
|
@utils.synchronized('qnap-gen_name')
|
|
def _gen_random_name(self, type):
|
|
if type == 'share':
|
|
infix = "shr-"
|
|
elif type == 'snapshot':
|
|
infix = "snp-"
|
|
elif type == 'host':
|
|
infix = "hst-"
|
|
else:
|
|
infix = ""
|
|
return ("manila-%(ifx)s%(time)s" %
|
|
{'ifx': infix,
|
|
'time': timeutils.utcnow().strftime('%Y%m%d%H%M%S%f')})
|
|
|
|
def _gen_host_name(self, vol_name_timestamp, access_level):
|
|
# host_name will be manila-{vol_name_timestamp}-ro or
|
|
# manila-{vol_name_timestamp}-rw
|
|
return 'manila-{}-{}'.format(vol_name_timestamp, access_level)
|
|
|
|
def _get_timestamp_from_vol_name(self, vol_name):
|
|
vol_name_split = vol_name.split('-')
|
|
dt = datetime.datetime.strptime(vol_name_split[2], '%Y%m%d%H%M%S%f')
|
|
return int(time.mktime(dt.timetuple()))
|
|
|
|
def _get_location_path(self, share_name, share_proto, ip, vol_id):
|
|
if share_proto == 'NFS':
|
|
vol = self.api_executor.get_specific_volinfo(vol_id)
|
|
vol_mount_path = vol.find('vol_mount_path').text
|
|
|
|
location = '%s:%s' % (ip, vol_mount_path)
|
|
else:
|
|
msg = _('Invalid NAS protocol: %s') % share_proto
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
export_location = {
|
|
'path': location,
|
|
'is_admin_only': False,
|
|
}
|
|
return export_location
|
|
|
|
def _update_share_stats(self):
|
|
"""Get latest share stats."""
|
|
backend_name = (self.configuration.safe_get(
|
|
'share_backend_name') or
|
|
self.__class__.__name__)
|
|
LOG.debug('backend_name=%(backend_name)s',
|
|
{'backend_name': backend_name})
|
|
|
|
selected_pool = self.api_executor.get_specific_poolinfo(
|
|
self.configuration.qnap_poolname)
|
|
total_capacity_gb = (int(selected_pool.find('capacity_bytes').text) /
|
|
units.Gi)
|
|
LOG.debug('total_capacity_gb: %s GB', total_capacity_gb)
|
|
free_capacity_gb = (int(selected_pool.find('freesize_bytes').text) /
|
|
units.Gi)
|
|
LOG.debug('free_capacity_gb: %s GB', free_capacity_gb)
|
|
alloc_capacity_gb = (int(selected_pool.find('allocated_bytes').text) /
|
|
units.Gi)
|
|
LOG.debug('allocated_capacity_gb: %s GB', alloc_capacity_gb)
|
|
|
|
reserved_percentage = self.configuration.safe_get(
|
|
'reserved_share_percentage')
|
|
|
|
# single pool now, need support multiple pools in the future
|
|
single_pool = {
|
|
"pool_name": self.configuration.qnap_poolname,
|
|
"total_capacity_gb": total_capacity_gb,
|
|
"free_capacity_gb": free_capacity_gb,
|
|
"allocated_capacity_gb": alloc_capacity_gb,
|
|
"reserved_percentage": reserved_percentage,
|
|
"qos": False,
|
|
"dedupe": [True, False],
|
|
"compression": [True, False],
|
|
"thin_provisioning": [True, False],
|
|
"qnap_ssd_cache": [True, False]
|
|
}
|
|
|
|
data = {
|
|
"share_backend_name": backend_name,
|
|
"vendor_name": "QNAP",
|
|
"driver_version": self.DRIVER_VERSION,
|
|
"storage_protocol": "NFS",
|
|
"snapshot_support": True,
|
|
"create_share_from_snapshot_support": True,
|
|
"driver_handles_share_servers": self.configuration.safe_get(
|
|
'driver_handles_share_servers'),
|
|
'pools': [single_pool],
|
|
}
|
|
super(QnapShareDriver, self)._update_share_stats(data)
|
|
|
|
@utils.retry(exception=exception.ShareBackendException,
|
|
interval=3,
|
|
retries=5)
|
|
@utils.synchronized('qnap-create_share')
|
|
def create_share(self, context, share, share_server=None):
|
|
"""Create a new share."""
|
|
LOG.debug('share: %s', share.__dict__)
|
|
extra_specs = share_types.get_extra_specs_from_share(share)
|
|
LOG.debug('extra_specs: %s', extra_specs)
|
|
qnap_thin_provision = share_types.parse_boolean_extra_spec(
|
|
'thin_provisioning', extra_specs.get("thin_provisioning") or
|
|
extra_specs.get('capabilities:thin_provisioning') or 'true')
|
|
qnap_compression = share_types.parse_boolean_extra_spec(
|
|
'compression', extra_specs.get("compression") or
|
|
extra_specs.get('capabilities:compression') or 'true')
|
|
qnap_deduplication = share_types.parse_boolean_extra_spec(
|
|
'dedupe', extra_specs.get("dedupe") or
|
|
extra_specs.get('capabilities:dedupe') or 'false')
|
|
qnap_ssd_cache = share_types.parse_boolean_extra_spec(
|
|
'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
|
|
extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
|
|
LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
|
|
'qnap_compression: %(qnap_compression)s '
|
|
'qnap_deduplication: %(qnap_deduplication)s '
|
|
'qnap_ssd_cache: %(qnap_ssd_cache)s',
|
|
{'qnap_thin_provision': qnap_thin_provision,
|
|
'qnap_compression': qnap_compression,
|
|
'qnap_deduplication': qnap_deduplication,
|
|
'qnap_ssd_cache': qnap_ssd_cache})
|
|
|
|
share_proto = share['share_proto']
|
|
|
|
# User could create two shares with the same name on horizon.
|
|
# Therefore, we should not use displayname to create shares on NAS.
|
|
create_share_name = self._gen_random_name("share")
|
|
# If share name exists, need to change to another name.
|
|
created_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_label=create_share_name)
|
|
LOG.debug('created_share: %s', created_share)
|
|
if created_share is not None:
|
|
msg = (_("The share name %s is used by other share on NAS.") %
|
|
create_share_name)
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
if (qnap_deduplication and not qnap_thin_provision):
|
|
msg = _("Dedupe cannot be enabled without thin_provisioning.")
|
|
LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
|
|
raise exception.InvalidExtraSpec(reason=msg)
|
|
self.api_executor.create_share(
|
|
share,
|
|
self.configuration.qnap_poolname,
|
|
create_share_name,
|
|
share_proto,
|
|
qnap_thin_provision=qnap_thin_provision,
|
|
qnap_compression=qnap_compression,
|
|
qnap_deduplication=qnap_deduplication,
|
|
qnap_ssd_cache=qnap_ssd_cache)
|
|
created_share = self._get_share_info(create_share_name)
|
|
volID = created_share.find('vol_no').text
|
|
# Use private_storage to record volume ID and Name created in the NAS.
|
|
LOG.debug('volID: %(volID)s '
|
|
'volName: %(create_share_name)s',
|
|
{'volID': volID,
|
|
'create_share_name': create_share_name})
|
|
_metadata = {'volID': volID,
|
|
'volName': create_share_name,
|
|
'thin_provision': qnap_thin_provision,
|
|
'compression': qnap_compression,
|
|
'deduplication': qnap_deduplication,
|
|
'ssd_cache': qnap_ssd_cache}
|
|
self.private_storage.update(share['id'], _metadata)
|
|
|
|
return self._get_location_path(create_share_name,
|
|
share['share_proto'],
|
|
self.configuration.qnap_share_ip,
|
|
volID)
|
|
|
|
@utils.retry(exception=exception.ShareBackendException,
|
|
interval=5, retries=5, backoff_rate=1)
|
|
def _get_share_info(self, share_name):
|
|
share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_label=share_name)
|
|
if share is None:
|
|
msg = _("Fail to get share info of %s on NAS.") % share_name
|
|
LOG.error(msg)
|
|
raise exception.ShareBackendException(msg=msg)
|
|
else:
|
|
return share
|
|
|
|
@utils.synchronized('qnap-delete_share')
|
|
def delete_share(self, context, share, share_server=None):
|
|
"""Delete the specified share."""
|
|
# Use private_storage to retrieve volume ID created in the NAS.
|
|
volID = self.private_storage.get(share['id'], 'volID')
|
|
if not volID:
|
|
LOG.warning('volID for Share %s does not exist', share['id'])
|
|
return
|
|
LOG.debug('volID: %s', volID)
|
|
|
|
del_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_no=volID)
|
|
if del_share is None:
|
|
LOG.warning('Share %s does not exist', share['id'])
|
|
return
|
|
|
|
vol_no = del_share.find('vol_no').text
|
|
|
|
self.api_executor.delete_share(vol_no)
|
|
self.private_storage.delete(share['id'])
|
|
|
|
@utils.synchronized('qnap-extend_share')
|
|
def extend_share(self, share, new_size, share_server=None):
|
|
"""Extend an existing share."""
|
|
LOG.debug('Entering extend_share share_name=%(share_name)s '
|
|
'share_id=%(share_id)s '
|
|
'new_size=%(size)s',
|
|
{'share_name': share['display_name'],
|
|
'share_id': share['id'],
|
|
'size': new_size})
|
|
|
|
# Use private_storage to retrieve volume Name created in the NAS.
|
|
volName = self.private_storage.get(share['id'], 'volName')
|
|
if not volName:
|
|
LOG.debug('Share %s does not exist', share['id'])
|
|
raise exception.ShareResourceNotFound(share_id=share['id'])
|
|
LOG.debug('volName: %s', volName)
|
|
thin_provision = self.private_storage.get(
|
|
share['id'], 'thin_provision')
|
|
compression = self.private_storage.get(share['id'], 'compression')
|
|
deduplication = self.private_storage.get(share['id'], 'deduplication')
|
|
ssd_cache = self.private_storage.get(share['id'], 'ssd_cache')
|
|
LOG.debug('thin_provision: %(thin_provision)s '
|
|
'compression: %(compression)s '
|
|
'deduplication: %(deduplication)s '
|
|
'ssd_cache: %(ssd_cache)s',
|
|
{'thin_provision': thin_provision,
|
|
'compression': compression,
|
|
'deduplication': deduplication,
|
|
'ssd_cache': ssd_cache})
|
|
share_dict = {
|
|
'sharename': volName,
|
|
'old_sharename': volName,
|
|
'new_size': new_size,
|
|
'thin_provision': thin_provision == 'True',
|
|
'compression': compression == 'True',
|
|
'deduplication': deduplication == 'True',
|
|
'ssd_cache': ssd_cache == 'True',
|
|
'share_proto': share['share_proto']
|
|
}
|
|
self.api_executor.edit_share(share_dict)
|
|
|
|
@utils.retry(exception=exception.ShareBackendException,
|
|
interval=3,
|
|
retries=5)
|
|
@utils.synchronized('qnap-create_snapshot')
|
|
def create_snapshot(self, context, snapshot, share_server=None):
|
|
"""Create a snapshot."""
|
|
LOG.debug('snapshot[share][share_id]: %s',
|
|
snapshot['share']['share_id'])
|
|
LOG.debug('snapshot id: %s', snapshot['id'])
|
|
|
|
# Use private_storage to retrieve volume ID created in the NAS.
|
|
volID = self.private_storage.get(snapshot['share']['id'], 'volID')
|
|
if not volID:
|
|
LOG.warning(
|
|
'volID for Share %s does not exist',
|
|
snapshot['share']['id'])
|
|
raise exception.ShareResourceNotFound(
|
|
share_id=snapshot['share']['id'])
|
|
LOG.debug('volID: %s', volID)
|
|
|
|
# User could create two snapshot with the same name on horizon.
|
|
# Therefore, we should not use displayname to create snapshot on NAS.
|
|
|
|
# if snapshot exist, need to change another
|
|
create_snapshot_name = self._gen_random_name("snapshot")
|
|
LOG.debug('create_snapshot_name: %s', create_snapshot_name)
|
|
check_snapshot = self.api_executor.get_snapshot_info(
|
|
volID=volID, snapshot_name=create_snapshot_name)
|
|
if check_snapshot is not None:
|
|
msg = _("Failed to create an unused snapshot name.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
LOG.debug('create_snapshot_name: %s', create_snapshot_name)
|
|
self.api_executor.create_snapshot_api(volID, create_snapshot_name)
|
|
|
|
snapshot_id = ""
|
|
created_snapshot = self.api_executor.get_snapshot_info(
|
|
volID=volID, snapshot_name=create_snapshot_name)
|
|
if created_snapshot is not None:
|
|
snapshot_id = created_snapshot.find('snapshot_id').text
|
|
else:
|
|
msg = _("Failed to get snapshot information.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
LOG.debug('created_snapshot: %s', created_snapshot)
|
|
LOG.debug('snapshot_id: %s', snapshot_id)
|
|
|
|
# Use private_storage to record data instead of metadata.
|
|
_metadata = {'snapshot_id': snapshot_id}
|
|
self.private_storage.update(snapshot['id'], _metadata)
|
|
|
|
# Test to get value from private_storage.
|
|
snapshot_id = self.private_storage.get(snapshot['id'], 'snapshot_id')
|
|
LOG.debug('snapshot_id: %s', snapshot_id)
|
|
|
|
return {'provider_location': snapshot_id}
|
|
|
|
@utils.synchronized('qnap-delete_snapshot')
|
|
def delete_snapshot(self, context, snapshot, share_server=None):
|
|
"""Delete a snapshot."""
|
|
LOG.debug('Entering delete_snapshot. The deleted snapshot=%(snap)s',
|
|
{'snap': snapshot['id']})
|
|
|
|
snapshot_id = (snapshot.get('provider_location') or
|
|
self.private_storage.get(snapshot['id'], 'snapshot_id'))
|
|
if not snapshot_id:
|
|
LOG.warning('Snapshot %s does not exist', snapshot['id'])
|
|
return
|
|
LOG.debug('snapshot_id: %s', snapshot_id)
|
|
|
|
self.api_executor.delete_snapshot_api(snapshot_id)
|
|
self.private_storage.delete(snapshot['id'])
|
|
|
|
@utils.retry(exception=exception.ShareBackendException,
|
|
interval=3,
|
|
retries=5)
|
|
@utils.synchronized('qnap-create_share_from_snapshot')
|
|
def create_share_from_snapshot(self, context, share, snapshot,
|
|
share_server=None):
|
|
"""Create a share from a snapshot."""
|
|
LOG.debug('Entering create_share_from_snapshot. The source '
|
|
'snapshot=%(snap)s. The created share=%(share)s',
|
|
{'snap': snapshot['id'], 'share': share['id']})
|
|
|
|
snapshot_id = (snapshot.get('provider_location') or
|
|
self.private_storage.get(snapshot['id'], 'snapshot_id'))
|
|
if not snapshot_id:
|
|
LOG.warning('Snapshot %s does not exist', snapshot['id'])
|
|
raise exception.SnapshotResourceNotFound(name=snapshot['id'])
|
|
LOG.debug('snapshot_id: %s', snapshot_id)
|
|
|
|
create_share_name = self._gen_random_name("share")
|
|
# if sharename exist, need to change another
|
|
created_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_label=create_share_name)
|
|
|
|
if created_share is not None:
|
|
msg = _("Failed to create an unused share name.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
self.api_executor.clone_snapshot(snapshot_id,
|
|
create_share_name, share['size'])
|
|
|
|
create_volID = ""
|
|
created_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_label=create_share_name)
|
|
if created_share is not None:
|
|
create_volID = created_share.find('vol_no').text
|
|
LOG.debug('create_volID: %s', create_volID)
|
|
else:
|
|
msg = _("Failed to clone a snapshot in time.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
thin_provision = self.private_storage.get(
|
|
snapshot['share_instance_id'], 'thin_provision')
|
|
compression = self.private_storage.get(
|
|
snapshot['share_instance_id'], 'compression')
|
|
deduplication = self.private_storage.get(
|
|
snapshot['share_instance_id'], 'deduplication')
|
|
ssd_cache = self.private_storage.get(
|
|
snapshot['share_instance_id'], 'ssd_cache')
|
|
LOG.debug('thin_provision: %(thin_provision)s '
|
|
'compression: %(compression)s '
|
|
'deduplication: %(deduplication)s '
|
|
'ssd_cache: %(ssd_cache)s',
|
|
{'thin_provision': thin_provision,
|
|
'compression': compression,
|
|
'deduplication': deduplication,
|
|
'ssd_cache': ssd_cache})
|
|
|
|
# Use private_storage to record volume ID and Name created in the NAS.
|
|
_metadata = {
|
|
'volID': create_volID,
|
|
'volName': create_share_name,
|
|
'thin_provision': thin_provision,
|
|
'compression': compression,
|
|
'deduplication': deduplication,
|
|
'ssd_cache': ssd_cache
|
|
}
|
|
self.private_storage.update(share['id'], _metadata)
|
|
|
|
# Test to get value from private_storage.
|
|
volName = self.private_storage.get(share['id'], 'volName')
|
|
LOG.debug('volName: %s', volName)
|
|
|
|
return self._get_location_path(create_share_name,
|
|
share['share_proto'],
|
|
self.configuration.qnap_share_ip,
|
|
create_volID)
|
|
|
|
def _get_vol_host(self, host_list, vol_name_timestamp):
|
|
vol_host_list = []
|
|
if host_list is None:
|
|
return vol_host_list
|
|
for host in host_list:
|
|
# Check host alias name with prefix "manila-{vol_name_timestamp}"
|
|
# to find the host of this manila share.
|
|
LOG.debug('_get_vol_host name:%s', host.find('name').text)
|
|
# Because driver supports only IPv4 now, check "netaddrs"
|
|
# have "ipv4" tag to get address.
|
|
if re.match("^manila-{}".format(vol_name_timestamp),
|
|
host.find('name').text):
|
|
host_dict = {
|
|
'index': host.find('index').text,
|
|
'hostid': host.find('hostid').text,
|
|
'name': host.find('name').text,
|
|
'ipv4': [],
|
|
}
|
|
for ipv4 in host.findall('netaddrs/ipv4'):
|
|
host_dict['ipv4'].append(ipv4.text)
|
|
vol_host_list.append(host_dict)
|
|
LOG.debug('_get_vol_host vol_host_list:%s', vol_host_list)
|
|
return vol_host_list
|
|
|
|
@utils.synchronized('qnap-update_access')
|
|
def update_access(self, context, share, access_rules, add_rules,
|
|
delete_rules, share_server=None):
|
|
if not (add_rules or delete_rules):
|
|
volName = self.private_storage.get(share['id'], 'volName')
|
|
LOG.debug('volName: %s', volName)
|
|
|
|
if volName is None:
|
|
LOG.debug('Share %s does not exist', share['id'])
|
|
raise exception.ShareResourceNotFound(share_id=share['id'])
|
|
|
|
# Clear all current ACLs
|
|
self.api_executor.set_nfs_access(volName, 2, "all")
|
|
|
|
vol_name_timestamp = self._get_timestamp_from_vol_name(volName)
|
|
host_list = self.api_executor.get_host_list()
|
|
LOG.debug('host_list:%s', host_list)
|
|
vol_host_list = self._get_vol_host(host_list, vol_name_timestamp)
|
|
# If host already exist, delete the host
|
|
if len(vol_host_list) > 0:
|
|
for vol_host in vol_host_list:
|
|
self.api_executor.delete_host(vol_host['name'])
|
|
|
|
# Add each one through all rules.
|
|
for access in access_rules:
|
|
self._allow_access(context, share, access, share_server)
|
|
else:
|
|
# Adding/Deleting specific rules
|
|
for access in delete_rules:
|
|
self._deny_access(context, share, access, share_server)
|
|
for access in add_rules:
|
|
self._allow_access(context, share, access, share_server)
|
|
|
|
def _allow_access(self, context, share, access, share_server=None):
|
|
"""Allow access to the share."""
|
|
share_proto = share['share_proto']
|
|
access_type = access['access_type']
|
|
access_level = access['access_level']
|
|
access_to = access['access_to']
|
|
LOG.debug('share_proto: %(share_proto)s '
|
|
'access_type: %(access_type)s'
|
|
'access_level: %(access_level)s'
|
|
'access_to: %(access_to)s',
|
|
{'share_proto': share_proto,
|
|
'access_type': access_type,
|
|
'access_level': access_level,
|
|
'access_to': access_to})
|
|
|
|
self._check_share_access(share_proto, access_type)
|
|
|
|
vol_name = self.private_storage.get(share['id'], 'volName')
|
|
vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
|
|
host_name = self._gen_host_name(vol_name_timestamp, access_level)
|
|
|
|
host_list = self.api_executor.get_host_list()
|
|
LOG.debug('vol_name: %(vol_name)s '
|
|
'access_level: %(access_level)s '
|
|
'host_name: %(host_name)s '
|
|
'host_list: %(host_list)s ',
|
|
{'vol_name': vol_name,
|
|
'access_level': access_level,
|
|
'host_name': host_name,
|
|
'host_list': host_list})
|
|
filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
|
|
if len(filter_host_list) == 0:
|
|
# if host does not exist, create a host for the share
|
|
self.api_executor.add_host(host_name, access_to)
|
|
elif (len(filter_host_list) == 1 and
|
|
filter_host_list[0]['name'] == host_name):
|
|
# if the host exist, and this host is for the same access right,
|
|
# add ip to the host.
|
|
ipv4_list = filter_host_list[0]['ipv4']
|
|
if access_to not in ipv4_list:
|
|
ipv4_list.append(access_to)
|
|
LOG.debug('vol_host["ipv4"]: %s', filter_host_list[0]['ipv4'])
|
|
LOG.debug('ipv4_list: %s', ipv4_list)
|
|
self.api_executor.edit_host(host_name, ipv4_list)
|
|
else:
|
|
# Until now, share of QNAP NAS can only apply one access level for
|
|
# all ips. "rw" for some ips and "ro" for else is not allowed.
|
|
support_level = (constants.ACCESS_LEVEL_RW if
|
|
access_level == constants.ACCESS_LEVEL_RO
|
|
else constants.ACCESS_LEVEL_RO)
|
|
reason = _('Share only supports one access '
|
|
'level: %s') % support_level
|
|
LOG.error(reason)
|
|
raise exception.InvalidShareAccess(reason=reason)
|
|
access = 1 if access_level == constants.ACCESS_LEVEL_RO else 0
|
|
self.api_executor.set_nfs_access(vol_name, access, host_name)
|
|
|
|
def _deny_access(self, context, share, access, share_server=None):
|
|
"""Deny access to the share."""
|
|
share_proto = share['share_proto']
|
|
access_type = access['access_type']
|
|
access_level = access['access_level']
|
|
access_to = access['access_to']
|
|
LOG.debug('share_proto: %(share_proto)s '
|
|
'access_type: %(access_type)s'
|
|
'access_level: %(access_level)s'
|
|
'access_to: %(access_to)s',
|
|
{'share_proto': share_proto,
|
|
'access_type': access_type,
|
|
'access_level': access_level,
|
|
'access_to': access_to})
|
|
|
|
try:
|
|
self._check_share_access(share_proto, access_type)
|
|
except exception.InvalidShareAccess:
|
|
LOG.warning('The denied rule is invalid and does not exist.')
|
|
return
|
|
|
|
vol_name = self.private_storage.get(share['id'], 'volName')
|
|
vol_name_timestamp = self._get_timestamp_from_vol_name(vol_name)
|
|
host_name = self._gen_host_name(vol_name_timestamp, access_level)
|
|
host_list = self.api_executor.get_host_list()
|
|
LOG.debug('vol_name: %(vol_name)s '
|
|
'access_level: %(access_level)s '
|
|
'host_name: %(host_name)s '
|
|
'host_list: %(host_list)s ',
|
|
{'vol_name': vol_name,
|
|
'access_level': access_level,
|
|
'host_name': host_name,
|
|
'host_list': host_list})
|
|
filter_host_list = self._get_vol_host(host_list, vol_name_timestamp)
|
|
# if share already have host, remove ip from host
|
|
for vol_host in filter_host_list:
|
|
if vol_host['name'] == host_name:
|
|
ipv4_list = vol_host['ipv4']
|
|
if access_to in ipv4_list:
|
|
ipv4_list.remove(access_to)
|
|
LOG.debug('vol_host["ipv4"]: %s', vol_host['ipv4'])
|
|
LOG.debug('ipv4_list: %s', ipv4_list)
|
|
if len(ipv4_list) == 0: # if list empty, remove the host
|
|
self.api_executor.set_nfs_access(
|
|
vol_name, 2, host_name)
|
|
self.api_executor.delete_host(host_name)
|
|
else:
|
|
self.api_executor.edit_host(host_name, ipv4_list)
|
|
break
|
|
|
|
def _check_share_access(self, share_proto, access_type):
|
|
if share_proto == 'NFS' and access_type != 'ip':
|
|
reason = _('Only "ip" access type is allowed for '
|
|
'NFS shares.')
|
|
LOG.warning(reason)
|
|
raise exception.InvalidShareAccess(reason=reason)
|
|
elif share_proto != 'NFS':
|
|
reason = _('Invalid NAS protocol: %s') % share_proto
|
|
raise exception.InvalidShareAccess(reason=reason)
|
|
|
|
def manage_existing(self, share, driver_options):
|
|
"""Manages a share that exists on backend."""
|
|
if share['share_proto'].lower() == 'nfs':
|
|
# 10.0.0.1:/share/example
|
|
LOG.info("Share %(shr_path)s will be managed with ID "
|
|
"%(shr_id)s.",
|
|
{'shr_path': share['export_locations'][0]['path'],
|
|
'shr_id': share['id']})
|
|
|
|
old_path_info = share['export_locations'][0]['path'].split(
|
|
':/share/')
|
|
|
|
if len(old_path_info) == 2:
|
|
ip = old_path_info[0]
|
|
share_name = old_path_info[1]
|
|
else:
|
|
msg = _("Incorrect path. It should have the following format: "
|
|
"IP:/share/share_name.")
|
|
raise exception.ShareBackendException(msg=msg)
|
|
else:
|
|
msg = _('Invalid NAS protocol: %s') % share['share_proto']
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
if ip != self.configuration.qnap_share_ip:
|
|
msg = _("The NAS IP %(ip)s is not configured.") % {'ip': ip}
|
|
raise exception.ShareBackendException(msg=msg)
|
|
|
|
existing_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_label=share_name)
|
|
if existing_share is None:
|
|
msg = _("The share %s trying to be managed was not found on "
|
|
"backend.") % share['id']
|
|
raise exception.ManageInvalidShare(reason=msg)
|
|
|
|
extra_specs = share_types.get_extra_specs_from_share(share)
|
|
qnap_thin_provision = share_types.parse_boolean_extra_spec(
|
|
'thin_provisioning', extra_specs.get("thin_provisioning") or
|
|
extra_specs.get('capabilities:thin_provisioning') or 'true')
|
|
qnap_compression = share_types.parse_boolean_extra_spec(
|
|
'compression', extra_specs.get("compression") or
|
|
extra_specs.get('capabilities:compression') or 'true')
|
|
qnap_deduplication = share_types.parse_boolean_extra_spec(
|
|
'dedupe', extra_specs.get("dedupe") or
|
|
extra_specs.get('capabilities:dedupe') or 'false')
|
|
qnap_ssd_cache = share_types.parse_boolean_extra_spec(
|
|
'qnap_ssd_cache', extra_specs.get("qnap_ssd_cache") or
|
|
extra_specs.get("capabilities:qnap_ssd_cache") or 'false')
|
|
LOG.debug('qnap_thin_provision: %(qnap_thin_provision)s '
|
|
'qnap_compression: %(qnap_compression)s '
|
|
'qnap_deduplication: %(qnap_deduplication)s '
|
|
'qnap_ssd_cache: %(qnap_ssd_cache)s',
|
|
{'qnap_thin_provision': qnap_thin_provision,
|
|
'qnap_compression': qnap_compression,
|
|
'qnap_deduplication': qnap_deduplication,
|
|
'qnap_ssd_cache': qnap_ssd_cache})
|
|
if (qnap_deduplication and not qnap_thin_provision):
|
|
msg = _("Dedupe cannot be enabled without thin_provisioning.")
|
|
LOG.debug('Dedupe cannot be enabled without thin_provisioning.')
|
|
raise exception.InvalidExtraSpec(reason=msg)
|
|
|
|
vol_no = existing_share.find('vol_no').text
|
|
vol = self.api_executor.get_specific_volinfo(vol_no)
|
|
vol_size_gb = math.ceil(float(vol.find('size').text) / units.Gi)
|
|
|
|
share_dict = {
|
|
'sharename': share_name,
|
|
'old_sharename': share_name,
|
|
'thin_provision': qnap_thin_provision,
|
|
'compression': qnap_compression,
|
|
'deduplication': qnap_deduplication,
|
|
'ssd_cache': qnap_ssd_cache,
|
|
'share_proto': share['share_proto']
|
|
}
|
|
self.api_executor.edit_share(share_dict)
|
|
|
|
_metadata = {}
|
|
_metadata['volID'] = vol_no
|
|
_metadata['volName'] = share_name
|
|
_metadata['thin_provision'] = qnap_thin_provision
|
|
_metadata['compression'] = qnap_compression
|
|
_metadata['deduplication'] = qnap_deduplication
|
|
_metadata['ssd_cache'] = qnap_ssd_cache
|
|
self.private_storage.update(share['id'], _metadata)
|
|
|
|
LOG.info("Share %(shr_path)s was successfully managed with ID "
|
|
"%(shr_id)s.",
|
|
{'shr_path': share['export_locations'][0]['path'],
|
|
'shr_id': share['id']})
|
|
|
|
export_locations = self._get_location_path(
|
|
share_name,
|
|
share['share_proto'],
|
|
self.configuration.qnap_share_ip,
|
|
vol_no)
|
|
|
|
return {'size': vol_size_gb, 'export_locations': export_locations}
|
|
|
|
def unmanage(self, share):
|
|
"""Remove the specified share from Manila management."""
|
|
self.private_storage.delete(share['id'])
|
|
|
|
def manage_existing_snapshot(self, snapshot, driver_options):
|
|
"""Manage existing share snapshot with manila."""
|
|
volID = self.private_storage.get(snapshot['share']['id'], 'volID')
|
|
LOG.debug('volID: %s', volID)
|
|
|
|
existing_share = self.api_executor.get_share_info(
|
|
self.configuration.qnap_poolname,
|
|
vol_no=volID)
|
|
|
|
if existing_share is None:
|
|
msg = _("The share id %s was not found on backend.") % volID
|
|
LOG.error(msg)
|
|
raise exception.ShareNotFound(reason=msg)
|
|
|
|
snapshot_id = snapshot.get('provider_location')
|
|
snapshot_id_info = snapshot_id.split('@')
|
|
|
|
if len(snapshot_id_info) == 2:
|
|
share_name = snapshot_id_info[0]
|
|
snapshot_name = snapshot_id_info[1]
|
|
else:
|
|
msg = _("Incorrect provider_location format. It should have the "
|
|
"following format: share_name@snapshot_name.")
|
|
LOG.error(msg)
|
|
raise exception.InvalidParameterValue(reason=msg)
|
|
|
|
if share_name != existing_share.find('vol_label').text:
|
|
msg = (_("The assigned share %(share_name)s was not matched "
|
|
"%(vol_label)s on backend.") %
|
|
{'share_name': share_name,
|
|
'vol_label': existing_share.find('vol_label').text})
|
|
LOG.error(msg)
|
|
raise exception.ShareNotFound(reason=msg)
|
|
|
|
check_snapshot = self.api_executor.get_snapshot_info(
|
|
volID=volID, snapshot_name=snapshot_name)
|
|
if check_snapshot is None:
|
|
msg = (_("The snapshot %(snapshot_name)s was not "
|
|
"found on backend.") %
|
|
{'snapshot_name': snapshot_name})
|
|
LOG.error(msg)
|
|
raise exception.InvalidParameterValue(err=msg)
|
|
|
|
_metadata = {
|
|
'snapshot_id': snapshot_id,
|
|
}
|
|
self.private_storage.update(snapshot['id'], _metadata)
|
|
parent_size = check_snapshot.find('parent_size')
|
|
snap_size_gb = None
|
|
if parent_size is not None:
|
|
snap_size_gb = math.ceil(float(parent_size.text) / units.Gi)
|
|
return {'size': snap_size_gb}
|
|
|
|
def unmanage_snapshot(self, snapshot):
|
|
"""Remove the specified snapshot from Manila management."""
|
|
self.private_storage.delete(snapshot['id'])
|