cinder/cinder/volume/drivers/qnap.py

2204 lines
85 KiB
Python

# Copyright (c) 2016 QNAP Systems, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Volume driver for QNAP Storage.
This driver supports QNAP Storage for iSCSI.
"""
import base64
from collections import OrderedDict
import eventlet
import functools
import re
import threading
import time
from defusedxml import cElementTree 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
import six
from six.moves import urllib
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='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:: default
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.
NOTE: Set driver_ssl_cert_verify as True under backend section to
enable SSL verification.
"""
# ThirdPartySystems wiki page
CI_WIKI_NAME = "QNAP_CI"
VERSION = '1.2.002'
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 = {}
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", "TDS", "TBS"
]
tes_model_types = [
"TES"
]
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"):
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"):
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.0.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.0.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 '<is> True' , '<is> False',
'True' and 'False'.
"""
if not isinstance(extra_spec_value, six.string_types):
extra_spec_value = six.text_type(extra_spec_value)
match = re.match(r'^<is>\s*(?P<value>True|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.find('LUNNAA') is not None:
lun_naa = created_lun.find('LUNNAA').text
try_times = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or lun_naa is not None):
break
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.0.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.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 = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or lun_naa is not None):
break
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')
pass
_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:
snapshot_id = created_snapshot.find('snapshot_id').text
try_times = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or created_snapshot is not None):
break
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.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 = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or lun_naa is not None):
break
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.0.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 = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or target_lun_id != -999):
break
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 = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or LUNNumber != ""):
break
elif "1.1.4" <= fw_version <= "2.0.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 = try_times + 3
eventlet.sleep(self.TIME_INTERVAL)
if(try_times > max_wait_sec or target_lun_id != -999):
break
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.0.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('createing', 'unmapped', 'mapped')
LOG.debug('LUNStatus: %s', selected_lun.find('LUNStatus').text)
LOG.debug('lun_status.mapped: %s', six.text_type(lun_status.mapped))
# lun does not map to any target
if (selected_lun.find('LUNStatus').text) != (
six.text_type(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):
super(QnapISCSIDriver, self)._detach_volume(context, attach_info,
volume, properties,
force, remote)
@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(six.text_type(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()
params['pwd'] = base64.b64encode(self.password.encode("utf-8"))
params['serviceKey'] = '1'
params['user'] = self.username
sanitized_params = OrderedDict()
for key in params:
value = params[key]
if value is not None:
sanitized_params[key] = six.text_type(value)
sanitized_params = urllib.parse.urlencode(sanitized_params)
url = ('/cgi-bin/authLogin.cgi?')
res_details = self._execute_and_get_response_details(
self.ip, url, sanitized_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] = six.text_type(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': six.text_type(lun_index),
'target_index': six.text_type(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'],
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': six.text_type(lun_index),
'target_index': six.text_type(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