# 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. """Volume driver for QNAP Storage. This driver supports QNAP Storage for iSCSI. """ import base64 from collections import OrderedDict import functools import re import threading import time import urllib.parse import eventlet from lxml import etree as ET from oslo_concurrency import lockutils from oslo_config import cfg from oslo_log import log as logging from oslo_utils import strutils from oslo_utils import timeutils from oslo_utils import units import requests from cinder.common import constants from cinder import exception from cinder.i18n import _ from cinder import interface from cinder import utils from cinder.volume import configuration from cinder.volume.drivers.san import san LOG = logging.getLogger(__name__) qnap_opts = [ cfg.URIOpt('qnap_management_url', help='The URL to management QNAP Storage. ' 'Driver does not support IPv6 address in URL.'), cfg.StrOpt('qnap_poolname', help='The pool name in the QNAP Storage'), cfg.StrOpt('qnap_storage_protocol', default=constants.ISCSI, help='Communication protocol to access QNAP storage') ] CONF = cfg.CONF CONF.register_opts(qnap_opts, group=configuration.SHARED_CONF_GROUP) @interface.volumedriver class QnapISCSIDriver(san.SanISCSIDriver): """QNAP iSCSI based cinder driver .. code-block:: none Version History: 1.0.0: Initial driver (Only iSCSI). 1.2.001: Add supports for Thin Provisioning, SSD Cache, Deduplication, Compression and CHAP. 1.2.002: Add support for QES fw 2.0.0. 1.2.003: Add support for QES fw 2.1.0. 1.2.004: Add support for QES fw on TDS series NAS model. 1.2.005: Add support for QTS fw 4.4.0. NOTE: Set driver_ssl_cert_verify as True under backend section to enable SSL verification. """ # ThirdPartySystems wiki page CI_WIKI_NAME = "QNAP_CI" # Remove if third party CI requirements are met SUPPORTED = False VERSION = '1.2.005' TIME_INTERVAL = 3 def __init__(self, *args, **kwargs): """Initialize QnapISCSIDriver.""" super(QnapISCSIDriver, self).__init__(*args, **kwargs) self.api_executor = None self.group_stats = {} self.configuration.append_config_values(qnap_opts) self.cache_time = 0 self.initiator = '' self.iscsi_port = '' self.target_index = '' self.target_iqn = '' self.target_iqns = [] self.nasInfoCache = {} @classmethod def get_driver_options(cls): additional_opts = cls._get_oslo_driver_opts( 'target_ip_address', 'san_login', 'san_password', 'use_chap_auth', 'chap_username', 'chap_password', 'driver_ssl_cert_verify', 'reserved_percentage') return qnap_opts + additional_opts def _check_config(self): """Ensure that the flags we care about are set.""" LOG.debug('in _check_config') required_config = ['qnap_management_url', 'san_login', 'san_password', 'qnap_poolname', 'qnap_storage_protocol'] for attr in required_config: if not getattr(self.configuration, attr, None): raise exception.InvalidInput( reason=_('%s is not set.') % attr) if not self.configuration.use_chap_auth: self.configuration.chap_username = '' self.configuration.chap_password = '' else: if not str.isalnum(self.configuration.chap_username): # invalid chap_username LOG.error('Username must be single-byte alphabet or number.') raise exception.InvalidInput( reason=_('Username must be single-byte ' 'alphabet or number.')) if not 12 <= len(self.configuration.chap_password) <= 16: # invalid chap_password LOG.error('Password must contain 12-16 characters.') raise exception.InvalidInput( reason=_('Password must contain 12-16 characters.')) def do_setup(self, context): """Setup the QNAP Cinder volume driver.""" self._check_config() self.ctxt = context LOG.debug('context: %s', context) # Setup API Executor try: self.api_executor = self.create_api_executor() except Exception: LOG.error('Failed to create HTTP client. ' 'Check ip, port, username, password' ' and make sure the array version is compatible') msg = _('Failed to create HTTP client.') raise exception.VolumeDriverException(message=msg) def check_for_setup_error(self): """Check the status of setup.""" pass def create_api_executor(self): """Create api executor by nas model.""" self.api_executor = QnapAPIExecutor( username=self.configuration.san_login, password=self.configuration.san_password, management_url=self.configuration.qnap_management_url, verify_ssl=self.configuration.driver_ssl_cert_verify) nas_model_name, internal_model_name, fw_version = ( self.api_executor.get_basic_info( self.configuration.qnap_management_url)) if (self.configuration.qnap_management_url not in self.nasInfoCache): self.nasInfoCache[self.configuration.qnap_management_url] = ( nas_model_name, internal_model_name, fw_version) pattern = re.compile(r"^([A-Z]+)-?[A-Z]{0,2}(\d+)\d{2}(U|[a-z]*)") matches = pattern.match(nas_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" ] LOG.debug('fw_version: %s', fw_version) if model_type in ts_model_types: if (fw_version >= "4.2") and (fw_version <= "4.4.9999"): 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 (QnapAPIExecutorTS( username=self.configuration.san_login, password=self.configuration.san_password, management_url=self.configuration.qnap_management_url, verify_ssl=self.configuration.driver_ssl_cert_verify)) elif model_type in tes_model_types: if 'TS' in internal_model_name: if (fw_version >= "4.2") and (fw_version <= "4.4.9999"): LOG.debug('Create TS API Executor') # modify the pool name to poole index self.configuration.qnap_poolname = ( self._get_ts_model_pool_id( self.configuration.qnap_poolname)) return (QnapAPIExecutorTS( username=self.configuration.san_login, password=self.configuration.san_password, management_url=self.configuration.qnap_management_url, verify_ssl=self.configuration.driver_ssl_cert_verify)) elif "1.1.2" <= fw_version <= "2.1.9999": LOG.debug('Create TES API Executor') return (QnapAPIExecutorTES( username=self.configuration.san_login, password=self.configuration.san_password, management_url=self.configuration.qnap_management_url, verify_ssl=self.configuration.driver_ssl_cert_verify)) elif model_type in es_model_types: if "1.1.2" <= fw_version <= "2.1.9999": LOG.debug('Create ES API Executor') return (QnapAPIExecutor( username=self.configuration.san_login, password=self.configuration.san_password, management_url=self.configuration.qnap_management_url, verify_ssl=self.configuration.driver_ssl_cert_verify)) msg = _('Model not support') raise exception.VolumeDriverException(message=msg) def _get_ts_model_pool_id(self, pool_name): """Modify the pool name to poole 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) def _gen_random_name(self): return "cinder-{0}".format(timeutils. utcnow(). strftime('%Y%m%d%H%M%S%f')) def _get_volume_metadata(self, volume): volume_metadata = {} if 'volume_metadata' in volume: for metadata in volume['volume_metadata']: volume_metadata[metadata['key']] = metadata['value'] return volume_metadata def _gen_lun_name(self): create_lun_name = '' while True: create_lun_name = self._gen_random_name() # If lun name with the name exists, need to change to # a different name created_lun = self.api_executor.get_lun_info( LUNName=create_lun_name) if created_lun is None: break return create_lun_name def _parse_boolean_extra_spec(self, extra_spec_value): """Parse boolean value from extra spec. Parse extra spec values of the form ' True' , ' False', 'True' and 'False'. """ if not isinstance(extra_spec_value, str): extra_spec_value = str(extra_spec_value) match = re.match(r'^\s*(?PTrue|False)$', extra_spec_value.strip(), re.IGNORECASE) if match: extra_spec_value = match.group('value') return strutils.bool_from_string(extra_spec_value, strict=True) def create_volume(self, volume): """Create a new volume.""" start_time = time.time() LOG.debug('in create_volume') LOG.debug('volume: %s', volume.__dict__) try: extra_specs = volume["volume_type"]["extra_specs"] LOG.debug('extra_spec: %s', extra_specs) qnap_thin_provision = self._parse_boolean_extra_spec( extra_specs.get('qnap_thin_provision', 'true')) qnap_compression = self._parse_boolean_extra_spec( extra_specs.get('qnap_compression', 'true')) qnap_deduplication = self._parse_boolean_extra_spec( extra_specs.get('qnap_deduplication', 'false')) qnap_ssd_cache = self._parse_boolean_extra_spec( extra_specs.get('qnap_ssd_cache', 'false')) except TypeError: LOG.debug('Unable to retrieve extra specs info. ' 'Use default extra spec.') qnap_thin_provision = True qnap_compression = True qnap_deduplication = False qnap_ssd_cache = 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): LOG.debug('Dedupe cannot be enabled without thin_provisioning.') raise exception.VolumeBackendAPIException( data=_('Dedupe cannot be enabled without thin_provisioning.')) # User could create two volume with the same name on horizon. # Therefore, We should not use display name to create lun on nas. create_lun_name = self._gen_lun_name() create_lun_index = self.api_executor.create_lun( volume, self.configuration.qnap_poolname, create_lun_name, qnap_thin_provision, qnap_ssd_cache, qnap_compression, qnap_deduplication) max_wait_sec = 600 try_times = 0 lun_naa = "" while True: created_lun = self.api_executor.get_lun_info( LUNIndex=create_lun_index) if (created_lun is not None and created_lun.find('LUNNAA').text is not None): lun_naa = created_lun.find('LUNNAA').text try_times += 3 if try_times > max_wait_sec or lun_naa: break eventlet.sleep(self.TIME_INTERVAL) LOG.debug('LUNNAA: %s', lun_naa) _metadata = self._get_volume_metadata(volume) _metadata['LUNIndex'] = create_lun_index _metadata['LUNNAA'] = lun_naa _metadata['LunName'] = create_lun_name elapsed_time = time.time() - start_time LOG.debug('create_volume elapsed_time: %s', elapsed_time) LOG.debug('create_volume volid: %(volid)s, metadata: %(meta)s', {'volid': volume['id'], 'meta': _metadata}) return {'metadata': _metadata} @lockutils.synchronized('delete_volume', 'cinder-', True) def delete_volume(self, volume): """Delete the specified volume.""" start_time = time.time() LOG.debug('volume: %s', volume.__dict__) lun_naa = self._get_lun_naa_from_volume_metadata(volume) if lun_naa == '': LOG.debug('Volume %s does not exist.', volume.id) return lun_index = '' for metadata in volume['volume_metadata']: if metadata['key'] == 'LUNIndex': lun_index = metadata['value'] break LOG.debug('LUNIndex: %s', lun_index) internal_model_name = (self.nasInfoCache [self.configuration.qnap_management_url][1]) LOG.debug('internal_model_name: %s', internal_model_name) fw_version = self.nasInfoCache[self.configuration .qnap_management_url][2] LOG.debug('fw_version: %s', fw_version) if 'TS' in internal_model_name.upper(): LOG.debug('in TS FW: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) del_lun = ET.fromstring(ret['data']).find('LUNInfo').find('row') elif 'ES' in internal_model_name.upper(): if fw_version >= "1.1.2" and fw_version <= "1.1.3": LOG.debug('in ES FW before 1.1.2/1.1.3: get_lun_info') del_lun = self.api_executor.get_lun_info( LUNIndex=lun_index) elif "1.1.4" <= fw_version <= "2.1.9999": LOG.debug('in ES FW after 1.1.4: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) del_lun = (ET.fromstring(ret['data']).find('LUNInfo') .find('row')) if del_lun is None: LOG.debug('Volume %s does not exist.', lun_naa) return # if lun is mapping at target, the delete action will fail if del_lun.find('LUNStatus').text == '2': target_index = (del_lun.find('LUNTargetList') .find('row').find('targetIndex').text) LOG.debug('target_index: %s', target_index) self.api_executor.disable_lun(lun_index, target_index) self.api_executor.unmap_lun(lun_index, target_index) retry_delete = False while True: retry_delete = self.api_executor.delete_lun(lun_index) if not retry_delete: break elapsed_time = time.time() - start_time LOG.debug('delete_volume elapsed_time: %s', elapsed_time) def _get_lun_naa_from_volume_metadata(self, volume): lun_naa = '' for metadata in volume['volume_metadata']: if metadata['key'] == 'LUNNAA': lun_naa = metadata['value'] break return lun_naa def _extend_lun(self, volume, lun_naa): LOG.debug('volume: %s', volume.__dict__) if lun_naa == '': lun_naa = self._get_lun_naa_from_volume_metadata(volume) LOG.debug('lun_naa: %s', lun_naa) selected_lun = self.api_executor.get_lun_info( LUNNAA=lun_naa) lun_index = selected_lun.find('LUNIndex').text LOG.debug('LUNIndex: %s', lun_index) lun_name = selected_lun.find('LUNName').text LOG.debug('LUNName: %s', lun_name) lun_thin_allocate = selected_lun.find('LUNThinAllocate').text LOG.debug('LUNThinAllocate: %s', lun_thin_allocate) lun_path = '' if selected_lun.find('LUNPath') is not None: lun_path = selected_lun.find('LUNPath').text LOG.debug('LUNPath: %s', lun_path) lun_status = selected_lun.find('LUNStatus').text LOG.debug('LUNStatus: %s', lun_status) lun = {'LUNName': lun_name, 'LUNCapacity': volume['size'], 'LUNIndex': lun_index, 'LUNThinAllocate': lun_thin_allocate, 'LUNPath': lun_path, 'LUNStatus': lun_status} self.api_executor.edit_lun(lun) def _create_snapshot_name(self, lun_index): create_snapshot_name = '' while True: # If snapshot with the name exists, need to change to # a different name create_snapshot_name = 'Q%d' % int(time.time()) snapshot = self.api_executor.get_snapshot_info( lun_index=lun_index, snapshot_name=create_snapshot_name) if snapshot is None: break return create_snapshot_name def create_cloned_volume(self, volume, src_vref): """Create a clone of the specified volume.""" LOG.debug('Entering create_cloned_volume...') LOG.debug('volume: %s', volume.__dict__) LOG.debug('src_vref: %s', src_vref.__dict__) LOG.debug('volume_metadata: %s', volume['volume_metadata']) src_lun_naa = self._get_lun_naa_from_volume_metadata(src_vref) # Below is to clone a volume from a snapshot in the snapshot manager src_lun = self.api_executor.get_lun_info( LUNNAA=src_lun_naa) lun_index = src_lun.find('LUNIndex').text LOG.debug('LUNIndex: %s', lun_index) # User could create two snapshot with the same name on horizon. # Therefore, we should not use displayname to create snapshot on nas. create_snapshot_name = self._create_snapshot_name(lun_index) self.api_executor.create_snapshot_api(lun_index, create_snapshot_name) created_snapshot = self.api_executor.get_snapshot_info( lun_index=lun_index, snapshot_name=create_snapshot_name) snapshot_id = created_snapshot.find('snapshot_id').text LOG.debug('snapshot_id: %s', snapshot_id) # User could create two volume with the same name on horizon. # Therefore, We should not use displayname to create lun on nas. while True: cloned_lun_name = self._gen_random_name() # If lunname with the name exists, need to change to # a different name cloned_lun = self.api_executor.get_lun_info( LUNName=cloned_lun_name) if cloned_lun is None: break self.api_executor.clone_snapshot(snapshot_id, cloned_lun_name) max_wait_sec = 600 try_times = 0 lun_naa = "" lun_index = "" while True: created_lun = self.api_executor.get_lun_info( LUNName=cloned_lun_name) if (created_lun is not None and created_lun.find('LUNNAA') is not None): lun_naa = created_lun.find('LUNNAA').text lun_index = created_lun.find('LUNIndex').text LOG.debug('LUNIndex: %s', lun_index) try_times += 3 if try_times > max_wait_sec or lun_naa: break eventlet.sleep(self.TIME_INTERVAL) LOG.debug('LUNNAA: %s', lun_naa) if (volume['size'] > src_vref['size']): self._extend_lun(volume, lun_naa) internal_model_name = (self.nasInfoCache [self.configuration.qnap_management_url][1]) if 'TS' in internal_model_name.upper(): LOG.debug('in TS FW: delete_snapshot_api') self.api_executor.delete_snapshot_api(snapshot_id) elif 'ES' in internal_model_name.upper(): LOG.debug('in ES FW: do nothing') _metadata = self._get_volume_metadata(volume) _metadata['LUNIndex'] = lun_index _metadata['LUNNAA'] = lun_naa _metadata['LunName'] = cloned_lun_name return {'metadata': _metadata} def create_snapshot(self, snapshot): """Create a snapshot.""" LOG.debug('snapshot: %s', snapshot.__dict__) LOG.debug('snapshot id: %s', snapshot['id']) # Below is to create snapshot in the snapshot manager LOG.debug('volume_metadata: %s', snapshot.volume['metadata']) volume_metadata = snapshot.volume['metadata'] LOG.debug('lun_naa: %s', volume_metadata['LUNNAA']) lun_naa = volume_metadata['LUNNAA'] src_lun = self.api_executor.get_lun_info(LUNNAA=lun_naa) lun_index = src_lun.find('LUNIndex').text LOG.debug('LUNIndex: %s', lun_index) # User could create two snapshot with the same name on horizon. # Therefore, We should not use displayname to create snapshot on nas. create_snapshot_name = self._create_snapshot_name(lun_index) LOG.debug('create_snapshot_name: %s', create_snapshot_name) self.api_executor.create_snapshot_api(lun_index, create_snapshot_name) max_wait_sec = 600 try_times = 0 snapshot_id = "" while True: created_snapshot = self.api_executor.get_snapshot_info( lun_index=lun_index, snapshot_name=create_snapshot_name) if (created_snapshot is not None and created_snapshot.find('snapshot_id').text is not None): snapshot_id = created_snapshot.find('snapshot_id').text try_times += 3 if try_times > max_wait_sec or snapshot_id: break eventlet.sleep(self.TIME_INTERVAL) LOG.debug('created_snapshot: %s', created_snapshot) LOG.debug('snapshot_id: %s', snapshot_id) _metadata = snapshot['metadata'] _metadata['snapshot_id'] = snapshot_id _metadata['SnapshotName'] = create_snapshot_name return {'metadata': _metadata} def delete_snapshot(self, snapshot): """Delete a snapshot.""" LOG.debug('snapshot: %s', snapshot.__dict__) # Below is to delete snapshot in the snapshot manager snap_metadata = snapshot['metadata'] if 'snapshot_id' not in snap_metadata: return LOG.debug('snapshot_id: %s', snap_metadata['snapshot_id']) snapshot_id = snap_metadata['snapshot_id'] self.api_executor.delete_snapshot_api(snapshot_id) def create_volume_from_snapshot(self, volume, snapshot): """Create a volume from a snapshot.""" LOG.debug('in create_volume_from_snapshot') LOG.debug('volume: %s', volume.__dict__) LOG.debug('snapshot: %s', snapshot.__dict__) # Below is to clone a volume from a snapshot in the snapshot manager snap_metadata = snapshot['metadata'] if 'snapshot_id' not in snap_metadata: LOG.debug('Metadata of the snapshot is invalid') msg = _('Metadata of the snapshot is invalid') raise exception.VolumeDriverException(message=msg) LOG.debug('snapshot_id: %s', snap_metadata['snapshot_id']) snapshot_id = snap_metadata['snapshot_id'] # User could create two volume with the same name on horizon. # Therefore, We should not use displayname to create lun on nas. create_lun_name = self._gen_lun_name() self.api_executor.clone_snapshot( snapshot_id, create_lun_name) max_wait_sec = 600 try_times = 0 lun_naa = "" lun_index = "" while True: created_lun = self.api_executor.get_lun_info( LUNName=create_lun_name) if (created_lun is not None and created_lun.find('LUNNAA') is not None): lun_naa = created_lun.find('LUNNAA').text lun_index = created_lun.find('LUNIndex').text LOG.debug('LUNNAA: %s', lun_naa) LOG.debug('LUNIndex: %s', lun_index) try_times += 3 if try_times > max_wait_sec or lun_naa: break eventlet.sleep(self.TIME_INTERVAL) if (volume['size'] > snapshot['volume_size']): self._extend_lun(volume, lun_naa) _metadata = self._get_volume_metadata(volume) _metadata['LUNIndex'] = lun_index _metadata['LUNNAA'] = lun_naa _metadata['LunName'] = create_lun_name return {'metadata': _metadata} def get_volume_stats(self, refresh=False): """Get volume stats. This is more of getting group stats.""" LOG.debug('in get_volume_stats refresh: %s', refresh) if refresh: backend_name = (self.configuration.safe_get( 'volume_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) capacity_bytes = int(selected_pool.find('capacity_bytes').text) LOG.debug('capacity_bytes: %s GB', capacity_bytes / units.Gi) freesize_bytes = int(selected_pool.find('freesize_bytes').text) LOG.debug('freesize_bytes: %s GB', freesize_bytes / units.Gi) provisioned_bytes = int(selected_pool.find('allocated_bytes').text) driver_protocol = self.configuration.qnap_storage_protocol LOG.debug( 'provisioned_bytes: %s GB', provisioned_bytes / units.Gi) self.group_stats = {'volume_backend_name': backend_name, 'vendor_name': 'QNAP', 'driver_version': self.VERSION, 'storage_protocol': driver_protocol} # single pool now, need support multiple pools in the future single_pool = dict( pool_name=self.configuration.qnap_poolname, total_capacity_gb=capacity_bytes / units.Gi, free_capacity_gb=freesize_bytes / units.Gi, provisioned_capacity_gb=provisioned_bytes / units.Gi, reserved_percentage=self.configuration.reserved_percentage, QoS_support=False, qnap_thin_provision=["True", "False"], qnap_compression=["True", "False"], qnap_deduplication=["True", "False"], qnap_ssd_cache=["True", "False"]) self.group_stats['pools'] = [single_pool] return self.group_stats def extend_volume(self, volume, new_size): """Extend an existing volume.""" LOG.debug('Entering extend_volume volume=%(vol)s ' 'new_size=%(size)s', {'vol': volume['display_name'], 'size': new_size}) volume['size'] = new_size self._extend_lun(volume, '') def _get_portal_info(self, volume, connector, lun_slot_id, lun_owner): """Get portal info.""" # Cache portal info for twenty seconds # If connectors were the same then use the portal info which was cached LOG.debug('get into _get_portal_info') self.initiator = connector['initiator'] ret = self.api_executor.get_iscsi_portal_info() root = ET.fromstring(ret['data']) iscsi_port = root.find('iSCSIPortal').find('servicePort').text LOG.debug('iscsiPort: %s', iscsi_port) target_iqn_prefix = root.find( 'iSCSIPortal').find('targetIQNPrefix').text LOG.debug('targetIQNPrefix: %s', target_iqn_prefix) internal_model_name = (self.nasInfoCache [self.configuration.qnap_management_url][1]) LOG.debug('internal_model_name: %s', internal_model_name) fw_version = (self.nasInfoCache [self.configuration.qnap_management_url][2]) LOG.debug('fw_version: %s', fw_version) target_index = '' target_iqn = '' # create a new target if no target has ACL connector['initiator'] LOG.debug('exist target_index: %s', target_index) if not target_index: target_name = self._gen_random_name() LOG.debug('target_name: %s', target_name) target_index = self.api_executor.create_target( target_name, lun_owner) LOG.debug('targetIndex: %s', target_index) retryCount = 0 retrySleepTime = 2 while retryCount <= 5: target_info = self.api_executor.get_target_info(target_index) if target_info.find('targetIQN').text is not None: break eventlet.sleep(retrySleepTime) retrySleepTime = retrySleepTime + 2 retryCount = retryCount + 1 target_iqn = target_info.find('targetIQN').text LOG.debug('target_iqn: %s', target_iqn) # TS NAS have to remove default ACL default_acl = ( target_iqn_prefix[:target_iqn_prefix.find(":") + 1]) default_acl = default_acl + "all:iscsi.default.ffffff" LOG.debug('default_acl: %s', default_acl) self.api_executor.remove_target_init(target_iqn, default_acl) # add ACL self.api_executor.add_target_init( target_iqn, connector['initiator'], self.configuration.use_chap_auth, self.configuration.chap_username, self.configuration.chap_password) # Get information for multipath target_iqns = [] slotid_list = [] eth_list, slotid_list = Util.retriveFormCache( self.configuration.qnap_management_url, lambda: self.api_executor.get_ethernet_ip(type='data'), 30) LOG.debug('slotid_list: %s', slotid_list) target_portals = [] target_portals.append( self.configuration.target_ip_address + ':' + iscsi_port) # target_iqns.append(target_iqn) for index, eth in enumerate(eth_list): # TS NAS do not have slot_id if not slotid_list: target_iqns.append(target_iqn) else: # To support ALUA, target portal and target inq should # be consistent. # EX: 10.77.230.31:3260 at controller B and it should map # to the target at controller B target_iqns.append( target_iqn[:-2] + '.' + slotid_list[index]) if eth == self.configuration.target_ip_address: continue target_portals.append(eth + ':' + iscsi_port) self.iscsi_port = iscsi_port self.target_index = target_index self.target_iqn = target_iqn self.target_iqns = target_iqns self.target_portals = target_portals return (iscsi_port, target_index, target_iqn, target_iqns, target_portals) @lockutils.synchronized('create_export', 'cinder-', True) def create_export(self, context, volume, connector): start_time = time.time() LOG.debug('in create_export') LOG.debug('volume: %s', volume.__dict__) LOG.debug('connector: %s', connector) lun_naa = self._get_lun_naa_from_volume_metadata(volume) if lun_naa == '': msg = (_("Volume %s does not exist.") % volume.id) LOG.error(msg) raise exception.VolumeDriverException(message=msg) LOG.debug('volume[name]: %s', volume['name']) LOG.debug('volume[display_name]: %s', volume['display_name']) lun_index = '' for metadata in volume['volume_metadata']: if metadata['key'] == 'LUNIndex': lun_index = metadata['value'] break LOG.debug('LUNIndex: %s', lun_index) internal_model_name = (self.nasInfoCache [self.configuration.qnap_management_url][1]) LOG.debug('internal_model_name: %s', internal_model_name) fw_version = self.nasInfoCache[self.configuration .qnap_management_url][2] LOG.debug('fw_version: %s', fw_version) if 'TS' in internal_model_name.upper(): LOG.debug('in TS FW: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) selected_lun = (ET.fromstring(ret['data']).find('LUNInfo') .find('row')) elif 'ES' in internal_model_name.upper(): if fw_version >= "1.1.2" and fw_version <= "1.1.3": LOG.debug('in ES FW before 1.1.2/1.1.3: get_lun_info') selected_lun = self.api_executor.get_lun_info( LUNNAA=lun_naa) elif "1.1.4" <= fw_version <= "2.1.9999": LOG.debug('in ES FW after 1.1.4: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) selected_lun = (ET.fromstring(ret['data']).find('LUNInfo') .find('row')) lun_owner = '' lun_slot_id = '' if selected_lun.find('lun_owner') is not None: lun_owner = selected_lun.find('lun_owner').text LOG.debug('lun_owner: %s', lun_owner) lun_slot_id = '0' if (lun_owner == 'SCA') else '1' LOG.debug('lun_slot_id: %s', lun_slot_id) # LOG.debug('self.initiator: %s', self.initiator) LOG.debug('connector: %s', connector['initiator']) iscsi_port, target_index, target_iqn, target_iqns, target_portals = ( self._get_portal_info(volume, connector, lun_slot_id, lun_owner)) self.api_executor.map_lun(lun_index, target_index) max_wait_sec = 600 try_times = 0 LUNNumber = "" target_lun_id = -999 while True: if 'TS' in internal_model_name.upper(): LOG.debug('in TS FW: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) root = ET.fromstring(ret['data']) target_lun_id = int(root.find('LUNInfo').find('row') .find('LUNTargetList').find('row') .find('LUNNumber').text) try_times += 3 if try_times > max_wait_sec or target_lun_id != -999: break eventlet.sleep(self.TIME_INTERVAL) elif 'ES' in internal_model_name.upper(): if fw_version >= "1.1.2" and fw_version <= "1.1.3": LOG.debug('in ES FW before 1.1.2/1.1.3: get_lun_info') root = self.api_executor.get_lun_info(LUNNAA=lun_naa) if len(list(root.find('LUNTargetList'))) != 0: LUNNumber = root.find('LUNTargetList').find( 'row').find('LUNNumber').text target_lun_id = int(LUNNumber) try_times += 3 if try_times > max_wait_sec or LUNNumber: break eventlet.sleep(self.TIME_INTERVAL) elif "1.1.4" <= fw_version <= "2.1.9999": LOG.debug('in ES FW after 1.1.4: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) root = ET.fromstring(ret['data']) target_lun_id = int(root.find('LUNInfo') .find('row').find('LUNTargetList') .find('row').find('LUNNumber').text) try_times += 3 if try_times > max_wait_sec or target_lun_id != -999: break eventlet.sleep(self.TIME_INTERVAL) else: break else: break properties = {} properties['target_discovered'] = False properties['target_portal'] = (self.configuration.target_ip_address + ':' + iscsi_port) properties['target_iqn'] = target_iqn LOG.debug('properties[target_iqn]: %s', properties['target_iqn']) LOG.debug('target_lun_id: %s', target_lun_id) properties['target_lun'] = target_lun_id properties['volume_id'] = volume['id'] # used by xen currently multipath = connector.get('multipath', False) if multipath: """Below are settings for multipath""" properties['target_portals'] = target_portals properties['target_iqns'] = target_iqns properties['target_luns'] = ( [target_lun_id] * len(target_portals)) LOG.debug('properties: %s', properties) provider_location = '%(host)s:%(port)s,1 %(name)s %(tgt_lun)s' % { 'host': self.configuration.target_ip_address, 'port': iscsi_port, 'name': target_iqn, 'tgt_lun': target_lun_id, } if self.configuration.use_chap_auth: provider_auth = 'CHAP %s %s' % (self.configuration.chap_username, self.configuration.chap_password) else: provider_auth = None elapsed_time = time.time() - start_time LOG.debug('create_export elapsed_time: %s', elapsed_time) LOG.debug('create_export volid: %(volid)s, provider_location: %(loc)s', {'volid': volume['id'], 'loc': provider_location}) return ( {'provider_location': provider_location, 'provider_auth': provider_auth}) def initialize_connection(self, volume, connector): start_time = time.time() LOG.debug('in initialize_connection') if not volume['provider_location']: err = _("Param volume['provider_location'] is invalid.") raise exception.InvalidParameterValue(err=err) result = volume['provider_location'].split(' ') if len(result) < 2: raise exception.InvalidInput(reason=volume['provider_location']) data = result[0].split(',') if len(data) < 2: raise exception.InvalidInput(reason=volume['provider_location']) iqn = result[1] LOG.debug('iqn: %s', iqn) target_lun_id = int(result[2], 10) LOG.debug('target_lun_id: %d', target_lun_id) properties = {} properties['target_discovered'] = False properties['target_portal'] = (self.configuration.target_ip_address + ':' + self.iscsi_port) properties['target_iqn'] = iqn properties['target_lun'] = target_lun_id properties['volume_id'] = volume['id'] # used by xen currently if self.configuration.use_chap_auth: properties['auth_method'] = 'CHAP' properties['auth_username'] = self.configuration.chap_username properties['auth_password'] = self.configuration.chap_password elapsed_time = time.time() - start_time LOG.debug('initialize_connection elapsed_time: %s', elapsed_time) LOG.debug('initialize_connection volid:' ' %(volid)s, properties: %(prop)s', {'volid': volume['id'], 'prop': properties}) return { 'driver_volume_type': 'iscsi', 'data': properties, } def enum(self, *sequential, **named): """Enum method.""" enums = dict(zip(sequential, range(len(sequential))), **named) return type('Enum', (), enums) def terminate_connection(self, volume, connector, **kwargs): """Driver entry point to unattach a volume from an instance.""" start_time = time.time() LOG.debug('in terminate_connection') LOG.debug('volume: %s', volume.__dict__) LOG.debug('connector: %s', connector) # get lun index lun_naa = self._get_lun_naa_from_volume_metadata(volume) LOG.debug('lun_naa: %s', lun_naa) lun_index = '' for metadata in volume['volume_metadata']: if metadata['key'] == 'LUNIndex': lun_index = metadata['value'] break LOG.debug('LUNIndex: %s', lun_index) internal_model_name = (self.nasInfoCache [self.configuration.qnap_management_url][1]) LOG.debug('internal_model_name: %s', internal_model_name) fw_version = self.nasInfoCache[self.configuration .qnap_management_url][2] LOG.debug('fw_version: %s', fw_version) if 'TS' in internal_model_name.upper(): LOG.debug('in TS FW: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) selected_lun = (ET.fromstring(ret['data']).find('LUNInfo') .find('row')) elif 'ES' in internal_model_name.upper(): if fw_version >= "1.1.2" and fw_version <= "1.1.3": LOG.debug('in ES FW before 1.1.2/1.1.3: get_lun_info') selected_lun = self.api_executor.get_lun_info( LUNIndex=lun_index) elif "1.1.4" <= fw_version <= "2.1.9999": LOG.debug('in ES FW after 1.1.4: get_one_lun_info') ret = self.api_executor.get_one_lun_info(lun_index) selected_lun = (ET.fromstring(ret['data']).find('LUNInfo') .find('row')) lun_status = self.enum('creating', 'unmapped', 'mapped') LOG.debug('LUNStatus: %s', selected_lun.find('LUNStatus').text) LOG.debug('lun_status.mapped: %s', str(lun_status.mapped)) # lun does not map to any target if (selected_lun.find('LUNStatus').text) != ( str(lun_status.mapped)): return target_index = (selected_lun.find('LUNTargetList') .find('row').find('targetIndex').text) LOG.debug('target_index: %s', target_index) start_time1 = time.time() self.api_executor.disable_lun(lun_index, target_index) elapsed_time1 = time.time() - start_time1 LOG.debug('terminate_connection disable_lun elapsed_time : %s', elapsed_time1) start_time2 = time.time() self.api_executor.unmap_lun(lun_index, target_index) elapsed_time2 = time.time() - start_time2 LOG.debug('terminate_connection unmap_lun elapsed_time : %s', elapsed_time2) elapsed_time = time.time() - start_time LOG.debug('terminate_connection elapsed_time : %s', elapsed_time) self.api_executor.delete_target(target_index) def update_migrated_volume( self, context, volume, new_volume, original_volume_status): """Return model update for migrated volume.""" LOG.debug('volume: %s', volume.__dict__) LOG.debug('new_volume: %s', new_volume.__dict__) LOG.debug('original_volume_status: %s', original_volume_status) _metadata = self._get_volume_metadata(new_volume) # metadata will not be swap after migration with liberty version # and the metadata of new volume is different with the metadata # of original volume. Therefore, we need to update the migrated volume. if not hasattr(new_volume, '_orig_metadata'): model_update = {'metadata': _metadata} return model_update @utils.synchronized('_attach_volume') def _detach_volume(self, context, attach_info, volume, properties, force=False, remote=False, ignore_errors=False): super(QnapISCSIDriver, self)._detach_volume( context, attach_info, volume, properties, force=force, remote=remote, ignore_errors=ignore_errors ) @utils.synchronized('_attach_volume') def _attach_volume(self, context, volume, properties, remote=False): return super(QnapISCSIDriver, self)._attach_volume(context, volume, properties, remote) def _connection_checker(func): """Decorator to check session has expired or not.""" @functools.wraps(func) def inner_connection_checker(self, *args, **kwargs): LOG.debug('in _connection_checker') for attempts in range(5): try: return func(self, *args, **kwargs) except exception.VolumeBackendAPIException as e: pattern = re.compile( r".*Session id expired$") matches = pattern.match(str(e)) if matches: if attempts < 4: LOG.debug('Session might have expired.' ' Trying to relogin') self._login() continue LOG.error('Re-throwing Exception %s', e) raise return inner_connection_checker class QnapAPIExecutor(object): """Makes QNAP API calls for ES NAS.""" es_create_lun_lock = threading.Lock() es_delete_lun_lock = threading.Lock() es_lun_locks = {} def __init__(self, *args, **kwargs): """Init function.""" self.sid = None self.username = kwargs['username'] self.password = kwargs['password'] self.ip, self.port, self.ssl = ( self._parse_management_url(kwargs['management_url'])) self.verify_ssl = kwargs['verify_ssl'] self._login() def _parse_management_url(self, management_url): # NOTE(Ibad): This parser isn't compatible with IPv6 address. # Typical IPv6 address will have : as delimiters and # URL is represented as https://[3ffe:2a00:100:7031::1]:8080 # since the regular expression below uses : to identify ip and port # it won't work with IPv6 address. pattern = re.compile(r"(http|https)\:\/\/(\S+)\:(\d+)") matches = pattern.match(management_url) if matches.group(1) == 'http': management_ssl = False else: management_ssl = True management_ip = matches.group(2) management_port = matches.group(3) return management_ip, management_port, management_ssl def get_basic_info(self, management_url): """Get the basic information of NAS.""" management_ip, management_port, management_ssl = ( self._parse_management_url(management_url)) response = self._get_response(management_ip, management_port, management_ssl, '/cgi-bin/authLogin.cgi') data = response.text root = ET.fromstring(data) nas_model_name = root.find('model/displayModelName').text internal_model_name = root.find('model/internalModelName').text fw_version = root.find('firmware/version').text return nas_model_name, internal_model_name, fw_version def _get_response(self, host_ip, host_port, use_ssl, action, body=None): """"Execute http request and return response.""" method = 'GET' headers = None protocol = 'https' if use_ssl else 'http' verify = self.verify_ssl if use_ssl else False # NOTE(ibad): URL formed here isn't IPv6 compatible # we should surround host ip with [] when IPv6 is supported # so the final URL can be like https://[3ffe:2a00:100:7031::1]:8080 url = '%s://%s:%s%s' % (protocol, host_ip, host_port, action) if body: method = 'POST' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'charset': 'utf-8' } response = requests.request(method, url, data=body, headers=headers, verify=verify) return response def _execute_and_get_response_details(self, nas_ip, url, post_parm=None): """Will prepare response after executing an http request.""" LOG.debug('_execute_and_get_response_details url: %s', url) LOG.debug('_execute_and_get_response_details post_parm: %s', post_parm) res_details = {} # Make the connection start_time2 = time.time() response = self._get_response( nas_ip, self.port, self.ssl, url, post_parm) elapsed_time2 = time.time() - start_time2 LOG.debug('request elapsed_time: %s', elapsed_time2) # Read the response data = response.text LOG.debug('response status: %s', response.status_code) # Extract http error msg if any error_details = None res_details['data'] = data res_details['error'] = error_details res_details['http_status'] = response.status_code return res_details def execute_login(self): """Login and return sid.""" params = OrderedDict( pwd=base64.b64encode(self.password.encode('utf-8')).decode(), serviceKey='1', user=self.username, ) encoded_params = urllib.parse.urlencode(params) url = ('/cgi-bin/authLogin.cgi?') res_details = self._execute_and_get_response_details( self.ip, url, encoded_params) root = ET.fromstring(res_details['data']) LOG.debug('execute_login data: %s', res_details['data']) session_id = root.find('authSid').text LOG.debug('execute_login session_id: %s', session_id) return session_id def _login(self): """Execute Https Login API.""" self.sid = self.execute_login() def _get_res_details(self, url, **kwargs): sanitized_params = OrderedDict() # Sort the dict of parameters params = utils.create_ordereddict(kwargs) for key, value in params.items(): if value is not None: sanitized_params[key] = str(value) encoded_params = urllib.parse.urlencode(sanitized_params) url = url + encoded_params res_details = self._execute_and_get_response_details(self.ip, url) return res_details @_connection_checker def create_lun(self, volume, pool_name, create_lun_name, reserve, ssd_cache, compress, dedup): """Create lun.""" self.es_create_lun_lock.acquire() lun_thin_allocate = '' if reserve: lun_thin_allocate = '1' else: lun_thin_allocate = '0' try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='add_lun', FileIO='no', LUNThinAllocate=lun_thin_allocate, LUNName=create_lun_name, LUNPath=create_lun_name, poolID=pool_name, lv_ifssd='yes' if ssd_cache else 'no', compression='1' if compress else '0', dedup='sha256' if dedup else 'off', LUNCapacity=volume['size'], lv_threshold='80', sid=self.sid) finally: self.es_create_lun_lock.release() root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Create volume %s failed') % volume['display_name']) return root.find('result').text @_connection_checker def delete_lun(self, vol_id, *args, **kwargs): """Execute delete lun API.""" self.es_delete_lun_lock.acquire() try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='remove_lun', run_background='1', ha_sync='1', LUNIndex=vol_id, sid=self.sid) finally: self.es_delete_lun_lock.release() data_set_is_busy = "-205041" root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) # dataset is busy, retry to delete if root.find('result').text == data_set_is_busy: return True if root.find('result').text < '0': msg = (_('Volume %s delete failed') % vol_id) raise exception.VolumeBackendAPIException(data=msg) return False @_connection_checker def get_specific_poolinfo(self, pool_id): """Execute get specific poolinfo API.""" res_details = self._get_res_details( '/cgi-bin/disk/disk_manage.cgi?', store='poolInfo', func='extra_get', poolID=pool_id, Pool_Info='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('get_specific_poolinfo failed')) pool_list = root.find('Pool_Index') pool_info_tree = pool_list.findall('row') for pool in pool_info_tree: if pool_id == pool.find('poolID').text: return pool @_connection_checker def create_target(self, target_name, controller_name): """Create target on nas and return target index.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='add_target', targetName=target_name, targetAlias=target_name, bTargetDataDigest='0', bTargetHeaderDigest='0', bTargetClusterEnable='1', controller_name=controller_name, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Create target failed')) root = ET.fromstring(res_details['data']) targetIndex = root.find('result').text return targetIndex @_connection_checker def delete_target(self, target_index): """Delete target on nas.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='remove_target', targetIndex=target_index, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text != '0': raise exception.VolumeBackendAPIException( data=_('Delete target failed')) @_connection_checker def add_target_init(self, target_iqn, init_iqn, use_chap_auth, chap_username, chap_password): """Add target acl.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='add_init', targetIQN=target_iqn, initiatorIQN=init_iqn, initiatorAlias=init_iqn, bCHAPEnable='1' if use_chap_auth else '0', CHAPUserName=chap_username, CHAPPasswd=chap_password, bMutualCHAPEnable='0', mutualCHAPUserName='', mutualCHAPPasswd='', ha_sync='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Add target acl failed')) def remove_target_init(self, target_iqn, init_iqn): """Remote target acl.""" pass @_connection_checker def map_lun(self, lun_index, target_index): """Map lun to sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='add_lun', LUNIndex=lun_index, targetIndex=target_index, sid=self.sid) finally: pass root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( "Map lun %(lun_index)s to target %(target_index)s failed") % {'lun_index': str(lun_index), 'target_index': str(target_index)}) @_connection_checker def disable_lun(self, lun_index, target_index): """Disable lun from sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='edit_lun', LUNIndex=lun_index, targetIndex=target_index, LUNEnable=0, sid=self.sid) finally: pass root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( 'Disable lun %(lun_index)s from target %(target_index)s failed' ) % {'lun_index': lun_index, 'target_index': target_index}) @_connection_checker def unmap_lun(self, lun_index, target_index): """Unmap lun from sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='remove_lun', LUNIndex=lun_index, targetIndex=target_index, sid=self.sid) finally: pass root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( 'Unmap lun %(lun_index)s from target %(target_index)s failed') % {'lun_index': lun_index, 'target_index': target_index}) @_connection_checker def get_iscsi_portal_info(self): """Get iscsi portal info.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', iSCSI_portal='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) else: return res_details @_connection_checker def get_lun_info(self, **kwargs): """Execute get_lun_info API.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', lunList='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if (('LUNIndex' in kwargs) or ('LUNName' in kwargs) or ('LUNNAA' in kwargs)): lun_list = root.find('iSCSILUNList') lun_info_tree = lun_list.findall('LUNInfo') for lun in lun_info_tree: if ('LUNIndex' in kwargs): if (kwargs['LUNIndex'] == lun.find('LUNIndex').text): return lun elif ('LUNName' in kwargs): if (kwargs['LUNName'] == lun.find('LUNName').text): return lun elif ('LUNNAA' in kwargs): if (kwargs['LUNNAA'] == lun.find('LUNNAA').text): return lun return None @_connection_checker def get_one_lun_info(self, lunID): """Execute get_one_lun_info API.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', lun_info='1', lunID=lunID, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) else: return res_details @_connection_checker def get_snapshot_info(self, **kwargs): """Execute get_snapshot_info API.""" res_details = self._get_res_details( '/cgi-bin/disk/snapshot.cgi?', func='extra_get', LUNIndex=kwargs['lun_index'], snapshot_list='1', snap_start='0', snap_count='100', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Unexpected response from QNAP API')) snapshot_list = root.find('SnapshotList') if snapshot_list is None: return None snapshot_tree = snapshot_list.findall('row') for snapshot in snapshot_tree: if (kwargs['snapshot_name'] == snapshot.find('snapshot_name').text): return snapshot return None @_connection_checker def create_snapshot_api(self, lun_id, snapshot_name): """Execute CGI to create snapshot from source lun.""" res_details = self._get_res_details( '/cgi-bin/disk/snapshot.cgi?', func='create_snapshot', lunID=lun_id, snapshot_name=snapshot_name, expire_min='0', vital='1', snapshot_type='0', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('create snapshot failed')) @_connection_checker def delete_snapshot_api(self, snapshot_id): """Execute CGI to delete snapshot by snapshot id.""" res_details = self._get_res_details( '/cgi-bin/disk/snapshot.cgi?', func='del_snapshots', snapshotID=snapshot_id, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) # snapshot not exist if root.find('result').text == '-206021': return # lun not exist if root.find('result').text == '-200005': return if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('delete snapshot %s failed') % snapshot_id) @_connection_checker def clone_snapshot(self, snapshot_id, new_lunname): """Execute CGI to clone snapshot as unmap lun.""" res_details = self._get_res_details( '/cgi-bin/disk/snapshot.cgi?', func='clone_qsnapshot', by_lun='1', snapshotID=snapshot_id, new_name=new_lunname, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( 'Clone lun %(lunname)s from snapshot %(snapshot_id)s failed' ) % {'lunname': new_lunname, 'snapshot_id': snapshot_id}) @_connection_checker def edit_lun(self, lun): """Extend lun.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='edit_lun', LUNName=lun['LUNName'], LUNCapacity=lun['LUNCapacity'], LUNIndex=lun['LUNIndex'], LUNThinAllocate=lun['LUNThinAllocate'], LUNPath=lun['LUNPath'], LUNStatus=lun['LUNStatus'], sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Extend lun %s failed') % lun['LUNIndex']) @_connection_checker def get_all_iscsi_portal_setting(self): """Execute get_all_iscsi_portal_setting API.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='get_all', sid=self.sid) return res_details @_connection_checker def get_ethernet_ip(self, **kwargs): """Execute get_ethernet_ip API.""" res_details = self._get_res_details( '/cgi-bin/sys/sysRequest.cgi?', subfunc='net_setting', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if ('type' in kwargs): return_ip = [] return_slot_id = [] ip_list = root.find('func').find('ownContent') ip_list_tree = ip_list.findall('IPInfo') for IP in ip_list_tree: ipv4 = (IP.find('IP').find('IP1').text + '.' + IP.find('IP').find('IP2').text + '.' + IP.find('IP').find('IP3').text + '.' + IP.find('IP').find('IP4').text) if ((kwargs['type'] == 'data') and (IP.find('isManagePort').text != '1') and (IP.find('status').text == '1')): return_slot_id.append(IP.find('interfaceSlotid').text) return_ip.append(ipv4) elif ((kwargs['type'] == 'manage') and (IP.find('isManagePort').text == '1') and (IP.find('status').text == '1')): return_ip.append(ipv4) elif ((kwargs['type'] == 'all') and (IP.find('status').text == '1')): return_ip.append(ipv4) return return_ip, return_slot_id @_connection_checker def get_target_info(self, target_index): """Get target info.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', targetInfo=1, targetIndex=target_index, sid=self.sid) root = ET.fromstring(res_details['data']) LOG.debug('ES get_target_info.authPassed: (%s)', root.find('authPassed').text) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Get target info failed')) target_list = root.find('targetInfo') target_tree = target_list.findall('row') for target in target_tree: if target_index == target.find('targetIndex').text: return target @_connection_checker def get_target_info_by_initiator(self, initiatorIQN): """Get target info by initiatorIQN.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', initiatorIQN=initiatorIQN, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': return "", "" target = root.find('targetACL').find('row') targetIndex = target.find('targetIndex').text targetIQN = target.find('targetIQN').text return targetIndex, targetIQN class QnapAPIExecutorTS(QnapAPIExecutor): """Makes QNAP API calls for TS NAS.""" create_lun_lock = threading.Lock() delete_lun_lock = threading.Lock() lun_locks = {} @_connection_checker def create_lun(self, volume, pool_name, create_lun_name, reserve, ssd_cache, compress, dedup): """Create lun.""" self.create_lun_lock.acquire() lun_thin_allocate = '' if reserve: lun_thin_allocate = '1' else: lun_thin_allocate = '0' try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='add_lun', FileIO='no', LUNThinAllocate=lun_thin_allocate, LUNName=create_lun_name, LUNPath=create_lun_name, poolID=pool_name, lv_ifssd='yes' if ssd_cache else 'no', LUNCapacity=volume['size'], LUNSectorSize='512', lv_threshold='80', sid=self.sid) finally: self.create_lun_lock.release() root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Create volume %s failed') % volume['display_name']) return root.find('result').text @_connection_checker def delete_lun(self, vol_id, *args, **kwargs): """Execute delete lun API.""" self.delete_lun_lock.acquire() try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='remove_lun', run_background='1', ha_sync='1', LUNIndex=vol_id, sid=self.sid) finally: self.delete_lun_lock.release() data_set_is_busy = "-205041" root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) # dataset is busy, retry to delete if root.find('result').text == data_set_is_busy: return True if root.find('result').text < '0': msg = (_('Volume %s delete failed') % vol_id) raise exception.VolumeBackendAPIException(data=msg) return False @lockutils.synchronized('map_unmap_lun_ts') @_connection_checker def map_lun(self, lun_index, target_index): """Map lun to sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='add_lun', LUNIndex=lun_index, targetIndex=target_index, sid=self.sid) finally: pass root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( "Map lun %(lun_index)s to target %(target_index)s failed") % {'lun_index': str(lun_index), 'target_index': str(target_index)}) return root.find('result').text @_connection_checker def disable_lun(self, lun_index, target_index): """Disable lun from sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='edit_lun', LUNIndex=lun_index, targetIndex=target_index, LUNEnable=0, sid=self.sid) finally: pass root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( 'Disable lun %(lun_index)s from target %(target_index)s failed' ) % {'lun_index': lun_index, 'target_index': target_index}) @_connection_checker def unmap_lun(self, lun_index, target_index): """Unmap lun from sepecific target.""" try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='remove_lun', LUNIndex=lun_index, targetIndex=target_index, sid=self.sid) finally: pass # self.lun_locks[lun_index].release() root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException(data=_( 'Unmap lun %(lun_index)s from target %(target_index)s failed') % {'lun_index': lun_index, 'target_index': target_index}) @_connection_checker def remove_target_init(self, target_iqn, init_iqn): """Remove target acl.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='remove_init', targetIQN=target_iqn, initiatorIQN=init_iqn, ha_sync='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Remove target acl failed')) @_connection_checker def get_target_info(self, target_index): """Get nas target info.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_portal_setting.cgi?', func='extra_get', targetInfo=1, targetIndex=target_index, ha_sync='1', sid=self.sid) root = ET.fromstring(res_details['data']) LOG.debug('TS get_target_info.authPassed: (%s)', root.find('authPassed').text) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Get target info failed')) target_list = root.find('targetInfo') target_tree = target_list.findall('row') for target in target_tree: if target_index == target.find('targetIndex').text: return target @_connection_checker def get_ethernet_ip(self, **kwargs): """Execute get_ethernet_ip API.""" res_details = self._get_res_details( '/cgi-bin/sys/sysRequest.cgi?', subfunc='net_setting', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if ('type' in kwargs): return_ip = [] ip_list = root.find('func').find('ownContent') ip_list_tree = ip_list.findall('IPInfo') for IP in ip_list_tree: ipv4 = (IP.find('IP').find('IP1').text + '.' + IP.find('IP').find('IP2').text + '.' + IP.find('IP').find('IP3').text + '.' + IP.find('IP').find('IP4').text) if (IP.find('status').text == '1'): return_ip.append(ipv4) return return_ip, None @_connection_checker def get_snapshot_info(self, **kwargs): """Execute get_snapshot_info API.""" res_details = self._get_res_details( '/cgi-bin/disk/snapshot.cgi?', func='extra_get', LUNIndex=kwargs['lun_index'], smb_snapshot_list='1', smb_snapshot='1', snapshot_list='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Unexpected response from QNAP API')) snapshot_list = root.find('SnapshotList') if snapshot_list is None: return None snapshot_tree = snapshot_list.findall('row') for snapshot in snapshot_tree: if (kwargs['snapshot_name'] == snapshot.find('snapshot_name').text): return snapshot return None @lockutils.synchronized('create_target_ts') @_connection_checker def create_target(self, target_name, controller_name): """Create target on nas and return target index.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='add_target', targetName=target_name, targetAlias=target_name, bTargetDataDigest='0', bTargetHeaderDigest='0', bTargetClusterEnable='1', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Create target failed')) root = ET.fromstring(res_details['data']) targetIndex = root.find('result').text return targetIndex @_connection_checker def delete_target(self, target_index): """Delete target on nas.""" res_details = self._get_res_details( '/cgi-bin/disk/iscsi_target_setting.cgi?', func='remove_target', targetIndex=target_index, sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text != target_index: raise exception.VolumeBackendAPIException( data=_('Delete target failed')) class QnapAPIExecutorTES(QnapAPIExecutor): """Makes QNAP API calls for TES NAS.""" tes_create_lun_lock = threading.Lock() @_connection_checker def create_lun(self, volume, pool_name, create_lun_name, reserve, ssd_cache, compress, dedup): """Create lun.""" self.tes_create_lun_lock.acquire() lun_thin_allocate = '' if reserve: lun_thin_allocate = '1' else: lun_thin_allocate = '0' try: res_details = self._get_res_details( '/cgi-bin/disk/iscsi_lun_setting.cgi?', func='add_lun', FileIO='no', LUNThinAllocate=lun_thin_allocate, LUNName=create_lun_name, LUNPath=create_lun_name, poolID=pool_name, lv_ifssd='yes' if ssd_cache else 'no', compression='1' if compress else '0', dedup='sha256' if dedup else 'off', sync='disabled', LUNCapacity=volume['size'], lv_threshold='80', sid=self.sid) finally: self.tes_create_lun_lock.release() root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if root.find('result').text < '0': raise exception.VolumeBackendAPIException( data=_('Create volume %s failed') % volume['display_name']) return root.find('result').text @_connection_checker def get_ethernet_ip(self, **kwargs): """Execute get_ethernet_ip API.""" res_details = self._get_res_details( '/cgi-bin/sys/sysRequest.cgi?', subfunc='net_setting', sid=self.sid) root = ET.fromstring(res_details['data']) if root.find('authPassed').text == '0': raise exception.VolumeBackendAPIException( data=_('Session id expired')) if ('type' in kwargs): return_ip = [] ip_list = root.find('func').find('ownContent') ip_list_tree = ip_list.findall('IPInfo') for IP in ip_list_tree: ipv4 = (IP.find('IP').find('IP1').text + '.' + IP.find('IP').find('IP2').text + '.' + IP.find('IP').find('IP3').text + '.' + IP.find('IP').find('IP4').text) if (IP.find('status').text == '1'): return_ip.append(ipv4) return return_ip, None class Util(object): _dictCondRetriveFormCache = {} _dictCacheRetriveFormCache = {} _condRetriveFormCache = threading.Condition() @classmethod def retriveFormCache(cls, lockKey, func, keepTime=0): cond = None cls._condRetriveFormCache.acquire() try: if (lockKey not in cls._dictCondRetriveFormCache): cls._dictCondRetriveFormCache[lockKey] = threading.Condition() cond = cls._dictCondRetriveFormCache[lockKey] finally: cls._condRetriveFormCache.release() cond.acquire() try: if (lockKey not in cls._dictCacheRetriveFormCache): # store (startTime, result) in cache. result = func() cls._dictCacheRetriveFormCache[lockKey] = (time.time(), result) startTime, result = cls._dictCacheRetriveFormCache[lockKey] # check if the cache is time-out if ((time.time() - startTime) > keepTime): result = func() cls._dictCacheRetriveFormCache[lockKey] = (time.time(), result) return result finally: cond.release() @classmethod def retry(cls, func, retry=0, retryTime=30): if (retry == 0): retry = 9999 # max is 9999 times if (retryTime == 0): retryTime = 9999 # max is 9999 seconds startTime = time.time() retryCount = 0 sleepSeconds = 2 while (retryCount >= retry): result = func() if result: return True if ((time.time() - startTime) <= retryTime): return False # more than retry times eventlet.sleep(sleepSeconds) sleepSeconds = sleepSeconds + 2 retryCount = retryCount + 1 return False # more than retryTime