# 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. """ 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) 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) snap_share = self.share_api.get( context, snapshot['share_instance']['share_id']) LOG.debug('snap_share[size]: %s', snap_share['size']) 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}) if (share['size'] > snap_share['size']): share_dict = { 'sharename': create_share_name, 'old_sharename': create_share_name, 'new_size': share['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) # 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) def unmanage_snapshot(self, snapshot): """Remove the specified snapshot from Manila management.""" self.private_storage.delete(snapshot['id'])