1744 lines
65 KiB
Python
1744 lines
65 KiB
Python
# Copyright (c) 2013 - 2014 Huawei Technologies Co., Ltd.
|
|
# 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.
|
|
"""Common class for Huawei 18000 storage drivers."""
|
|
|
|
import base64
|
|
import json
|
|
import socket
|
|
import time
|
|
import uuid
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
from oslo_utils import units
|
|
import six
|
|
from six.moves import http_cookiejar
|
|
from six.moves import urllib
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LI, _LW
|
|
from cinder.openstack.common import loopingcall
|
|
from cinder import utils
|
|
from cinder.volume import qos_specs
|
|
from cinder.volume import volume_types
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DEFAULT_WAIT_TIMEOUT = 3600 * 24 * 30
|
|
DEFAULT_WAIT_INTERVAL = 5
|
|
|
|
HOSTGROUP_PREFIX = 'OpenStack_HostGroup_'
|
|
LUNGROUP_PREFIX = 'OpenStack_LunGroup_'
|
|
MAPPING_VIEW_PREFIX = 'OpenStack_Mapping_View_'
|
|
QOS_NAME_PREFIX = 'OpenStack_'
|
|
huawei_valid_keys = ['maxIOPS', 'minIOPS', 'minBandWidth',
|
|
'maxBandWidth', 'latency', 'IOType']
|
|
|
|
|
|
class RestCommon(object):
|
|
"""Common class for Huawei OceanStor 18000 storage system."""
|
|
|
|
def __init__(self, configuration):
|
|
self.configuration = configuration
|
|
self.cookie = http_cookiejar.CookieJar()
|
|
self.url = None
|
|
self.productversion = None
|
|
self.headers = {"Connection": "keep-alive",
|
|
"Content-Type": "application/json"}
|
|
|
|
def call(self, url=False, data=None, method=None):
|
|
"""Send requests to 18000 server.
|
|
Send HTTPS call, get response in JSON.
|
|
Convert response into Python Object and return it.
|
|
"""
|
|
|
|
handler = urllib.request.HTTPCookieProcessor(self.cookie)
|
|
opener = urllib.request.build_opener(handler)
|
|
urllib.request.install_opener(opener)
|
|
|
|
try:
|
|
socket.setdefaulttimeout(720)
|
|
req = urllib.request.Request(url, data, self.headers)
|
|
if method:
|
|
req.get_method = lambda: method
|
|
res = urllib.request.urlopen(req).read().decode("utf-8")
|
|
|
|
if "xx/sessions" not in url:
|
|
LOG.info(_LI('\n\n\n\nRequest URL: %(url)s\n\n'
|
|
'Call Method: %(method)s\n\n'
|
|
'Request Data: %(data)s\n\n'
|
|
'Response Data:%(res)s\n\n'), {'url': url,
|
|
'method': method,
|
|
'data': data,
|
|
'res': res})
|
|
|
|
except Exception as err:
|
|
LOG.error(_LE('\nBad response from server: %s.'), err)
|
|
raise
|
|
|
|
try:
|
|
res_json = json.loads(res)
|
|
except Exception as err:
|
|
LOG.error(_LE('JSON transfer error: %s.'), err)
|
|
raise
|
|
|
|
return res_json
|
|
|
|
def login(self):
|
|
"""Log in 18000 array."""
|
|
|
|
login_info = self._get_login_info()
|
|
url = login_info['RestURL'] + "xx/sessions"
|
|
data = json.dumps({"username": login_info['UserName'],
|
|
"password": login_info['UserPassword'],
|
|
"scope": "0"})
|
|
result = self.call(url, data)
|
|
if (result['error']['code'] != 0) or ("data" not in result):
|
|
msg = (_("Login error, reason is: %s.") % result)
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
deviceid = result['data']['deviceid']
|
|
self.url = login_info['RestURL'] + deviceid
|
|
self.headers['iBaseToken'] = result['data']['iBaseToken']
|
|
return deviceid
|
|
|
|
def _init_lun_parameters(self, name, parameters):
|
|
"""Init basic LUN parameters."""
|
|
lunparam = {"TYPE": "11",
|
|
"NAME": name,
|
|
"PARENTTYPE": "216",
|
|
"PARENTID": parameters['pool_id'],
|
|
"DESCRIPTION": parameters['volume_description'],
|
|
"ALLOCTYPE": parameters['LUNType'],
|
|
"CAPACITY": parameters['volume_size'],
|
|
"WRITEPOLICY": parameters['WriteType'],
|
|
"MIRRORPOLICY": parameters['MirrorSwitch'],
|
|
"PREFETCHPOLICY": parameters['PrefetchType'],
|
|
"PREFETCHVALUE": parameters['PrefetchValue']}
|
|
|
|
return lunparam
|
|
|
|
def _assert_rest_result(self, result, err_str):
|
|
error_code = result['error']['code']
|
|
if error_code != 0:
|
|
msg = (_('%(err)s\nresult: %(res)s.') % {'err': err_str,
|
|
'res': result})
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
def _assert_data_in_result(self, result, msg):
|
|
if "data" not in result:
|
|
err_msg = (_('%s "data" was not in result.') % msg)
|
|
LOG.error(err_msg)
|
|
raise exception.CinderException(err_msg)
|
|
|
|
def _create_volume(self, lun_param):
|
|
url = self.url + "/lun"
|
|
data = json.dumps(lun_param)
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create volume error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def create_volume(self, volume):
|
|
|
|
poolinfo = self._find_pool_info()
|
|
volume_name = self._encode_name(volume['id'])
|
|
volume_description = volume['name']
|
|
volume_size = self._get_volume_size(volume)
|
|
|
|
LOG.info(_LI('Create Volume: %(volume)s Size: %(size)s.'),
|
|
{'volume': volume_name, 'size': volume_size})
|
|
|
|
params = self._get_lun_conf_params()
|
|
params['pool_id'] = poolinfo['ID']
|
|
params['volume_size'] = volume_size
|
|
params['volume_description'] = volume_description
|
|
|
|
# Prepare lun parameters.
|
|
lun_param = self._init_lun_parameters(volume_name, params)
|
|
|
|
# Create LUN on the array.
|
|
lun_info = self._create_volume(lun_param)
|
|
lunid = lun_info['ID']
|
|
|
|
type_id = volume.get('volume_type_id', None)
|
|
policy_id = None
|
|
|
|
if type_id is not None:
|
|
volume_type = self._get_volume_type(type_id)
|
|
qos = self._get_qos_by_volume_type(volume_type)
|
|
|
|
if qos is None:
|
|
msg = (_('Find QoS configuration error!'))
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
try:
|
|
# Check QoS priority. if high, change lun priority to high.
|
|
if self._check_qos_high_priority(qos) is True:
|
|
self._change_lun_priority(lunid)
|
|
|
|
# Create QoS policy and active.
|
|
policy_id = self._create_qos_policy(qos, lunid)
|
|
self._active_deactive_qos(policy_id, True)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
if policy_id is not None:
|
|
self._delete_qos_policy(policy_id)
|
|
|
|
self._delete_lun(lunid)
|
|
|
|
return lun_info
|
|
|
|
def _get_volume_size(self, volume):
|
|
"""Calculate the volume size.
|
|
|
|
We should divide the given volume size by 512 for the 18000 system
|
|
calculates volume size with sectors, which is 512 bytes.
|
|
"""
|
|
|
|
volume_size = units.Gi / 512 # 1G
|
|
if int(volume['size']) != 0:
|
|
volume_size = int(volume['size']) * units.Gi / 512
|
|
|
|
return volume_size
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def delete_volume(self, volume):
|
|
"""Delete a volume.
|
|
|
|
Three steps: first, remove associate from lungroup.
|
|
Second, remove associate from QoS policy. Third, remove the lun.
|
|
"""
|
|
|
|
name = self._encode_name(volume['id'])
|
|
lun_id = volume.get('provider_location', None)
|
|
LOG.info(_LI('Delete Volume: %(name)s array lun id: %(lun_id)s.'),
|
|
{'name': name, 'lun_id': lun_id})
|
|
if lun_id:
|
|
if self._check_lun_exist(lun_id) is True:
|
|
# Get qos_id by lun_id.
|
|
qos_id = self._get_qosid_by_lunid(lun_id)
|
|
|
|
if qos_id != "":
|
|
qos_info = self._get_qos_info(qos_id)
|
|
qos_status = qos_info['RUNNINGSTATUS']
|
|
# 2: Active status.
|
|
if qos_status == '2':
|
|
self._active_deactive_qos(qos_id, False)
|
|
|
|
self._delete_qos_policy(qos_id)
|
|
self._delete_lun(lun_id)
|
|
else:
|
|
LOG.warning(_LW("Can't find lun or lungroup on the array."))
|
|
|
|
def _check_lun_exist(self, lun_id):
|
|
url = self.url + "/lun/" + lun_id
|
|
data = json.dumps({"TYPE": "11",
|
|
"ID": lun_id})
|
|
result = self.call(url, data, "GET")
|
|
error_code = result['error']['code']
|
|
if error_code != 0:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _delete_lun(self, lun_id):
|
|
url = self.url + "/lun/" + lun_id
|
|
data = json.dumps({"TYPE": "11",
|
|
"ID": lun_id})
|
|
result = self.call(url, data, "DELETE")
|
|
self._assert_rest_result(result, 'Delete lun error.')
|
|
|
|
def _read_xml(self):
|
|
"""Open xml file and parse the content."""
|
|
filename = self.configuration.cinder_huawei_conf_file
|
|
try:
|
|
tree = ET.parse(filename)
|
|
root = tree.getroot()
|
|
except Exception as err:
|
|
LOG.error(_LE('_read_xml: %s'), err)
|
|
raise
|
|
return root
|
|
|
|
def _encode_name(self, name):
|
|
uuid_str = name.replace("-", "")
|
|
vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
|
|
vol_encoded = base64.urlsafe_b64encode(vol_uuid.bytes)
|
|
newuuid = vol_encoded.replace("=", "")
|
|
return newuuid
|
|
|
|
def _find_pool_info(self):
|
|
root = self._read_xml()
|
|
pool_name = root.findtext('LUN/StoragePool')
|
|
if not pool_name:
|
|
err_msg = (_("Invalid resource pool: %s.") % pool_name)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(err_msg)
|
|
|
|
url = self.url + "/storagepool"
|
|
result = self.call(url, None)
|
|
self._assert_rest_result(result, 'Query resource pool error.')
|
|
|
|
poolinfo = {}
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if pool_name.strip() == item['NAME']:
|
|
poolinfo['ID'] = item['ID']
|
|
poolinfo['CAPACITY'] = item['USERFREECAPACITY']
|
|
poolinfo['TOTALCAPACITY'] = item['USERTOTALCAPACITY']
|
|
break
|
|
|
|
if not poolinfo:
|
|
msg = (_('Get pool info error, pool name is: %s.') % pool_name)
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
return poolinfo
|
|
|
|
def _get_volume_by_name(self, name):
|
|
url = self.url + "/lun?range=[0-65535]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get volume by name error!')
|
|
|
|
volume_id = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if name == item['NAME']:
|
|
volume_id = item['ID']
|
|
break
|
|
return volume_id
|
|
|
|
def _active_snapshot(self, snapshot_id):
|
|
activeurl = self.url + "/snapshot/activate"
|
|
data = json.dumps({"SNAPSHOTLIST": [snapshot_id]})
|
|
result = self.call(activeurl, data)
|
|
self._assert_rest_result(result, 'Active snapshot error.')
|
|
|
|
def _create_snapshot(self, snapshot):
|
|
snapshot_name = self._encode_name(snapshot['id'])
|
|
snapshot_description = snapshot['id']
|
|
volume_name = self._encode_name(snapshot['volume_id'])
|
|
|
|
LOG.info(_LI('_create_snapshot:snapshot name: %(snapshot)s, '
|
|
'volume name: %(volume)s.'),
|
|
{'snapshot': snapshot_name,
|
|
'volume': volume_name})
|
|
|
|
lun_id = self._get_volume_by_name(volume_name)
|
|
if lun_id is None:
|
|
msg = (_("Can't find lun info on the array, "
|
|
"lun name is: %(name)s") % {'name': volume_name})
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
url = self.url + "/snapshot"
|
|
data = json.dumps({"TYPE": "27",
|
|
"NAME": snapshot_name,
|
|
"PARENTTYPE": "11",
|
|
"DESCRIPTION": snapshot_description,
|
|
"PARENTID": lun_id})
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create snapshot error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def create_snapshot(self, snapshot):
|
|
snapshot_info = self._create_snapshot(snapshot)
|
|
snapshot_id = snapshot_info['ID']
|
|
self._active_snapshot(snapshot_id)
|
|
|
|
return snapshot_info
|
|
|
|
def _check_snapshot_exist(self, snapshot_id):
|
|
url = self.url + "/snapshot/" + snapshot_id
|
|
data = json.dumps({"TYPE": "27",
|
|
"ID": snapshot_id})
|
|
result = self.call(url, data, "GET")
|
|
error_code = result['error']['code']
|
|
if error_code != 0:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _stop_snapshot(self, snapshot_id):
|
|
url = self.url + "/snapshot/stop"
|
|
stopdata = json.dumps({"ID": snapshot_id})
|
|
result = self.call(url, stopdata, "PUT")
|
|
self._assert_rest_result(result, 'Stop snapshot error.')
|
|
|
|
def _delete_snapshot(self, snapshotid):
|
|
url = self.url + "/snapshot/%s" % snapshotid
|
|
data = json.dumps({"TYPE": "27", "ID": snapshotid})
|
|
result = self.call(url, data, "DELETE")
|
|
self._assert_rest_result(result, 'Delete snapshot error.')
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def delete_snapshot(self, snapshot):
|
|
snapshot_name = self._encode_name(snapshot['id'])
|
|
volume_name = self._encode_name(snapshot['volume_id'])
|
|
|
|
LOG.info(_LI('stop_snapshot:snapshot name: %(snapshot)s, '
|
|
'volume name: %(volume)s.'),
|
|
{'snapshot': snapshot_name,
|
|
'volume': volume_name})
|
|
|
|
snapshot_id = snapshot.get('provider_location', None)
|
|
if snapshot_id is None:
|
|
snapshot_id = self._get_snapshotid_by_name(snapshot_name)
|
|
|
|
if snapshot_id is not None:
|
|
if self._check_snapshot_exist(snapshot_id) is True:
|
|
self._stop_snapshot(snapshot_id)
|
|
self._delete_snapshot(snapshot_id)
|
|
else:
|
|
LOG.warning(_LW("Can't find snapshot on the array."))
|
|
else:
|
|
LOG.warning(_LW("Can't find snapshot on the array."))
|
|
|
|
def _get_snapshotid_by_name(self, name):
|
|
url = self.url + "/snapshot?range=[0-65535]"
|
|
data = json.dumps({"TYPE": "27"})
|
|
result = self.call(url, data, "GET")
|
|
self._assert_rest_result(result, 'Get snapshot id error.')
|
|
|
|
snapshot_id = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if name == item['NAME']:
|
|
snapshot_id = item['ID']
|
|
break
|
|
|
|
return snapshot_id
|
|
|
|
def _copy_volume(self, volume, copy_name, src_lun, tgt_lun):
|
|
|
|
luncopy_id = self._create_luncopy(copy_name,
|
|
src_lun, tgt_lun)
|
|
event_type = 'LUNcopyWaitInterval'
|
|
wait_interval = self._get_wait_interval(event_type)
|
|
wait_interval = int(wait_interval)
|
|
try:
|
|
self._start_luncopy(luncopy_id)
|
|
|
|
def _luncopy_complete():
|
|
luncopy_info = self._get_luncopy_info(luncopy_id)
|
|
if luncopy_info['status'] == '40':
|
|
# luncopy_info['status'] means for the running status of
|
|
# the luncopy. If luncopy_info['status'] is equal to '40',
|
|
# this luncopy is completely ready.
|
|
return True
|
|
elif luncopy_info['state'] != '1':
|
|
# luncopy_info['state'] means for the healthy status of the
|
|
# luncopy. If luncopy_info['state'] is not equal to '1',
|
|
# this means that an error occurred during the LUNcopy
|
|
# operation and we should abort it.
|
|
err_msg = (_(
|
|
'An error occurred during the LUNcopy operation. '
|
|
'LUNcopy name: %(luncopyname)s. '
|
|
'LUNcopy status: %(luncopystatus)s. '
|
|
'LUNcopy state: %(luncopystate)s.')
|
|
% {'luncopyname': luncopy_id,
|
|
'luncopystatus': luncopy_info['status'],
|
|
'luncopystate': luncopy_info['state']})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
self._wait_for_condition(_luncopy_complete, wait_interval)
|
|
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
self._delete_luncopy(luncopy_id)
|
|
self.delete_volume(volume)
|
|
|
|
self._delete_luncopy(luncopy_id)
|
|
|
|
def _get_wait_interval(self, event_type):
|
|
"""Get wait interval from huawei conf file."""
|
|
root = self._read_xml()
|
|
wait_interval = root.findtext('LUN/%s' % event_type)
|
|
if wait_interval:
|
|
return wait_interval
|
|
else:
|
|
LOG.info(_LI(
|
|
"Wait interval for %(event_type)s is not configured in huawei "
|
|
"conf file. Use default: %(default_wait_interval)d."),
|
|
{"event_type": event_type,
|
|
"default_wait_interval": DEFAULT_WAIT_INTERVAL})
|
|
return DEFAULT_WAIT_INTERVAL
|
|
|
|
def _get_default_timeout(self):
|
|
"""Get timeout from huawei conf file."""
|
|
root = self._read_xml()
|
|
timeout = root.findtext('LUN/Timeout')
|
|
if timeout is None:
|
|
timeout = DEFAULT_WAIT_TIMEOUT
|
|
LOG.info(_LI(
|
|
"Timeout is not configured in huawei conf file. "
|
|
"Use default: %(default_timeout)d."),
|
|
{"default_timeout": timeout})
|
|
|
|
return timeout
|
|
|
|
def _wait_for_condition(self, func, interval, timeout=None):
|
|
start_time = time.time()
|
|
if timeout is None:
|
|
timeout = self._get_default_timeout()
|
|
|
|
def _inner():
|
|
try:
|
|
res = func()
|
|
except Exception as ex:
|
|
res = False
|
|
LOG.debug('_wait_for_condition: %(func_name)s '
|
|
'failed for %(exception)s.',
|
|
{'func_name': func.__name__,
|
|
'exception': ex})
|
|
if res:
|
|
raise loopingcall.LoopingCallDone()
|
|
|
|
if int(time.time()) - start_time > timeout:
|
|
msg = (_('_wait_for_condition: %s timed out.')
|
|
% func.__name__)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_inner)
|
|
timer.start(interval=interval).wait()
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Create a volume from a snapshot.
|
|
|
|
We use LUNcopy to copy a new volume from snapshot.
|
|
The time needed increases as volume size does.
|
|
"""
|
|
|
|
snapshot_name = self._encode_name(snapshot['id'])
|
|
|
|
snapshot_id = snapshot.get('provider_location', None)
|
|
if snapshot_id is None:
|
|
snapshot_id = self._get_snapshotid_by_name(snapshot_name)
|
|
if snapshot_id is None:
|
|
err_msg = (_(
|
|
'create_volume_from_snapshot: Snapshot %(name)s '
|
|
'does not exist.')
|
|
% {'name': snapshot_name})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
lun_info = self.create_volume(volume)
|
|
tgt_lun_id = lun_info['ID']
|
|
luncopy_name = self._encode_name(volume['id'])
|
|
|
|
LOG.info(_LI('create_volume_from_snapshot: src_lun_id: '
|
|
'%(src_lun_id)s, tgt_lun_id: %(tgt_lun_id)s, '
|
|
'copy_name: %(copy_name)s'),
|
|
{'src_lun_id': snapshot_id,
|
|
'tgt_lun_id': tgt_lun_id,
|
|
'copy_name': luncopy_name})
|
|
|
|
event_type = 'LUNReadyWaitInterval'
|
|
wait_interval = self._get_wait_interval(event_type)
|
|
|
|
def _volume_ready():
|
|
url = self.url + "/lun/" + tgt_lun_id
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get volume by id failed!')
|
|
|
|
if "data" in result:
|
|
if (result['data']['HEALTHSTATUS'] == "1" and
|
|
result['data']['RUNNINGSTATUS'] == "27"):
|
|
return True
|
|
return False
|
|
|
|
self._wait_for_condition(_volume_ready,
|
|
wait_interval,
|
|
wait_interval * 3)
|
|
self._copy_volume(volume, luncopy_name, snapshot_id, tgt_lun_id)
|
|
|
|
return lun_info
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Clone a new volume from an existing volume."""
|
|
|
|
# Form the snapshot structure.
|
|
snapshot = {'id': uuid.uuid4().__str__(), 'volume_id': src_vref['id']}
|
|
|
|
# Create snapshot.
|
|
self.create_snapshot(snapshot)
|
|
|
|
try:
|
|
# Create volume from snapshot.
|
|
lun_info = self.create_volume_from_snapshot(volume, snapshot)
|
|
finally:
|
|
try:
|
|
# Delete snapshot.
|
|
self.delete_snapshot(snapshot)
|
|
except exception.CinderException:
|
|
LOG.warning(_LW('Failure deleting the snapshot '
|
|
'%(snapshot_id)s of volume %(volume_id)s.'),
|
|
{'snapshot_id': snapshot['id'],
|
|
'volume_id': src_vref['id']})
|
|
|
|
return lun_info
|
|
|
|
def _create_luncopy(self, luncopyname, srclunid, tgtlunid):
|
|
"""Create a luncopy."""
|
|
url = self.url + "/luncopy"
|
|
data = json.dumps({"TYPE": 219,
|
|
"NAME": luncopyname,
|
|
"DESCRIPTION": luncopyname,
|
|
"COPYSPEED": 2,
|
|
"LUNCOPYTYPE": "1",
|
|
"SOURCELUN": ("INVALID;%s;INVALID;INVALID;INVALID"
|
|
% srclunid),
|
|
"TARGETLUN": ("INVALID;%s;INVALID;INVALID;INVALID"
|
|
% tgtlunid)})
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create lun copy error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _add_host_into_hostgroup(self, host_id):
|
|
"""Associate host to hostgroup.
|
|
|
|
If hostgroup doesn't exist, create one.
|
|
|
|
"""
|
|
host_group_name = HOSTGROUP_PREFIX + host_id
|
|
hostgroup_id = self._find_hostgroup(host_group_name)
|
|
|
|
LOG.info(_LI('_add_host_into_hostgroup, hostgroup name: %(name)s, '
|
|
'hostgroup id: %(id)s.'),
|
|
{'name': host_group_name,
|
|
'id': hostgroup_id})
|
|
|
|
if hostgroup_id is None:
|
|
hostgroup_id = self._create_hostgroup(host_group_name)
|
|
|
|
is_associated = self._is_host_associate_to_hostgroup(hostgroup_id,
|
|
host_id)
|
|
if is_associated is False:
|
|
self._associate_host_to_hostgroup(hostgroup_id, host_id)
|
|
|
|
return hostgroup_id
|
|
|
|
def _mapping_hostgroup_and_lungroup(self, volume_name,
|
|
hostgroup_id, host_id):
|
|
"""Add hostgroup and lungroup to view."""
|
|
lungroup_name = LUNGROUP_PREFIX + host_id
|
|
mapping_view_name = MAPPING_VIEW_PREFIX + host_id
|
|
lungroup_id = self._find_lungroup(lungroup_name)
|
|
lun_id = self._get_volume_by_name(volume_name)
|
|
view_id = self._find_mapping_view(mapping_view_name)
|
|
|
|
LOG.info(_LI('_mapping_hostgroup_and_lungroup, lun_group: '
|
|
'%(lun_group)s, view_id: %(view_id)s, lun_id: '
|
|
'%(lun_id)s.'), {'lun_group': lungroup_id,
|
|
'view_id': view_id,
|
|
'lun_id': lun_id})
|
|
|
|
try:
|
|
# Create lungroup and add LUN into to lungroup.
|
|
if lungroup_id is None:
|
|
lungroup_id = self._create_lungroup(lungroup_name)
|
|
is_associated = self._is_lun_associated_to_lungroup(lungroup_id,
|
|
lun_id)
|
|
if not is_associated:
|
|
self._associate_lun_to_lungroup(lungroup_id, lun_id)
|
|
|
|
if view_id is None:
|
|
view_id = self._add_mapping_view(mapping_view_name)
|
|
self._associate_hostgroup_to_view(view_id, hostgroup_id)
|
|
self._associate_lungroup_to_view(view_id, lungroup_id)
|
|
else:
|
|
if not self._hostgroup_associated(view_id, hostgroup_id):
|
|
self._associate_hostgroup_to_view(view_id, hostgroup_id)
|
|
if not self._lungroup_associated(view_id, lungroup_id):
|
|
self._associate_lungroup_to_view(view_id, lungroup_id)
|
|
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Error occurred when adding hostgroup and '
|
|
'lungroup to view. Remove lun from lungroup '
|
|
'now.'))
|
|
self._remove_lun_from_lungroup(lungroup_id, lun_id)
|
|
|
|
return lun_id
|
|
|
|
def _ensure_initiator_added(self, initiator_name, hostid):
|
|
added = self._initiator_is_added_to_array(initiator_name)
|
|
if not added:
|
|
self._add_initiator_to_array(initiator_name)
|
|
if self._is_initiator_associated_to_host(initiator_name) is False:
|
|
self._associate_initiator_to_host(initiator_name, hostid)
|
|
else:
|
|
if self._is_initiator_associated_to_host(initiator_name) is False:
|
|
self._associate_initiator_to_host(initiator_name, hostid)
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def initialize_connection_iscsi(self, volume, connector):
|
|
"""Map a volume to a host and return target iSCSI information."""
|
|
|
|
LOG.info(_LI('Enter initialize_connection_iscsi.'))
|
|
initiator_name = connector['initiator']
|
|
volume_name = self._encode_name(volume['id'])
|
|
|
|
LOG.info(_LI('initiator name: %(initiator_name)s, '
|
|
'volume name: %(volume)s.'),
|
|
{'initiator_name': initiator_name,
|
|
'volume': volume_name})
|
|
|
|
(iscsi_iqn, target_ip) = self._get_iscsi_params(connector)
|
|
LOG.info(_LI('initialize_connection_iscsi,iscsi_iqn: %(iscsi_iqn)s, '
|
|
'target_ip: %(target_ip)s.'),
|
|
{'iscsi_iqn': iscsi_iqn,
|
|
'target_ip': target_ip})
|
|
|
|
# Create host_group if not exist.
|
|
host_name = connector['host']
|
|
hostid = self._find_host(host_name)
|
|
if hostid is None:
|
|
hostid = self._add_host(host_name)
|
|
|
|
# Add initiator to the host.
|
|
self._ensure_initiator_added(initiator_name, hostid)
|
|
hostgroup_id = self._add_host_into_hostgroup(hostid)
|
|
|
|
# Mapping lungroup and hostgroup to view.
|
|
lun_id = self._mapping_hostgroup_and_lungroup(volume_name,
|
|
hostgroup_id, hostid)
|
|
|
|
hostlunid = self._find_host_lun_id(hostid, lun_id)
|
|
|
|
LOG.info(_LI("initialize_connection_iscsi, host lun id is: %s."),
|
|
hostlunid)
|
|
|
|
# Return iSCSI properties.
|
|
properties = {}
|
|
properties['target_discovered'] = False
|
|
properties['target_portal'] = ('%s:%s' % (target_ip, '3260'))
|
|
properties['target_iqn'] = iscsi_iqn
|
|
properties['target_lun'] = int(hostlunid)
|
|
properties['volume_id'] = volume['id']
|
|
|
|
LOG.info(_LI("initialize_connection_iscsi success. Return data: %s."),
|
|
properties)
|
|
return {'driver_volume_type': 'iscsi', 'data': properties}
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def initialize_connection_fc(self, volume, connector):
|
|
wwns = connector['wwpns']
|
|
host_name = connector['host']
|
|
volume_name = self._encode_name(volume['id'])
|
|
|
|
LOG.info(_LI('initialize_connection_fc, initiator: %(initiator_name)s,'
|
|
' volume name: %(volume)s.'),
|
|
{'initiator_name': wwns,
|
|
'volume': volume_name})
|
|
|
|
# Create host_group if not exist.
|
|
hostid = self._find_host(host_name)
|
|
if hostid is None:
|
|
hostid = self._add_host(host_name)
|
|
|
|
# Add host into hostgroup.
|
|
hostgroup_id = self._add_host_into_hostgroup(hostid)
|
|
|
|
free_wwns = self._get_connected_free_wwns()
|
|
LOG.info(_LI("initialize_connection_fc, the array has free wwns: %s"),
|
|
free_wwns)
|
|
for wwn in wwns:
|
|
if wwn in free_wwns:
|
|
self._add_fc_port_to_host(hostid, wwn)
|
|
|
|
lun_id = self._mapping_hostgroup_and_lungroup(volume_name,
|
|
hostgroup_id, hostid)
|
|
host_lun_id = self._find_host_lun_id(hostid, lun_id)
|
|
|
|
tgt_port_wwns = []
|
|
for wwn in wwns:
|
|
tgtwwpns = self._get_fc_target_wwpns(wwn)
|
|
if tgtwwpns:
|
|
tgt_port_wwns.append(tgtwwpns)
|
|
|
|
init_targ_map = {}
|
|
for initiator in wwns:
|
|
init_targ_map[initiator] = tgt_port_wwns
|
|
|
|
# Return FC properties.
|
|
info = {'driver_volume_type': 'fibre_channel',
|
|
'data': {'target_lun': int(host_lun_id),
|
|
'target_discovered': True,
|
|
'target_wwn': tgt_port_wwns,
|
|
'volume_id': volume['id'],
|
|
'initiator_target_map': init_targ_map}}
|
|
|
|
LOG.info(_LI("initialize_connection_fc, return data is: %s."), info)
|
|
|
|
return info
|
|
|
|
def _get_iscsi_tgt_port(self):
|
|
url = self.url + "/iscsidevicename"
|
|
result = self.call(url, None)
|
|
|
|
msg = 'Get iSCSI target port error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data'][0]['CMO_ISCSI_DEVICE_NAME']
|
|
|
|
def _find_hostgroup(self, groupname):
|
|
"""Get the given hostgroup id."""
|
|
url = self.url + "/hostgroup?range=[0-8191]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get hostgroup information error.')
|
|
|
|
host_group_id = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if groupname == item['NAME']:
|
|
host_group_id = item['ID']
|
|
break
|
|
return host_group_id
|
|
|
|
def _find_lungroup(self, lungroupname):
|
|
"""Get the given hostgroup id."""
|
|
url = self.url + "/lungroup?range=[0-8191]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get lungroup information error.')
|
|
|
|
lun_group_id = None
|
|
if 'data' in result:
|
|
for item in result['data']:
|
|
if lungroupname == item['NAME']:
|
|
lun_group_id = item['ID']
|
|
break
|
|
return lun_group_id
|
|
|
|
def _create_hostgroup(self, hostgroupname):
|
|
url = self.url + "/hostgroup"
|
|
data = json.dumps({"TYPE": "14", "NAME": hostgroupname})
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create hostgroup error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _create_lungroup(self, lungroupname):
|
|
url = self.url + "/lungroup"
|
|
data = json.dumps({"DESCRIPTION": lungroupname,
|
|
"APPTYPE": '0',
|
|
"GROUPTYPE": '0',
|
|
"NAME": lungroupname})
|
|
result = self.call(url, data)
|
|
|
|
msg = 'Create lungroup error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _delete_lungroup(self, lungroupid):
|
|
url = self.url + "/LUNGroup/" + lungroupid
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Delete lungroup error.')
|
|
|
|
def _lungroup_associated(self, viewid, lungroupid):
|
|
url_subfix = ("/mappingview/associate?TYPE=245&"
|
|
"ASSOCIATEOBJTYPE=256&ASSOCIATEOBJID=%s" % lungroupid)
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Check lungroup associated error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if viewid == item['ID']:
|
|
return True
|
|
return False
|
|
|
|
def _hostgroup_associated(self, viewid, hostgroupid):
|
|
url_subfix = ("/mappingview/associate?TYPE=245&"
|
|
"ASSOCIATEOBJTYPE=14&ASSOCIATEOBJID=%s" % hostgroupid)
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Check hostgroup associated error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if viewid == item['ID']:
|
|
return True
|
|
return False
|
|
|
|
def _find_host_lun_id(self, hostid, lunid):
|
|
|
|
url = self.url + ("/lun/associate?TYPE=11&ASSOCIATEOBJTYPE=21"
|
|
"&ASSOCIATEOBJID=%s" % (hostid))
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Find host lun id error.')
|
|
|
|
host_lun_id = 1
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if lunid == item['ID']:
|
|
associate_data = result['data'][0]['ASSOCIATEMETADATA']
|
|
try:
|
|
hostassoinfo = json.loads(associate_data)
|
|
host_lun_id = hostassoinfo['HostLUNID']
|
|
break
|
|
except Exception as err:
|
|
LOG.error(_LE("JSON transfer data error. %s"), err)
|
|
raise
|
|
return host_lun_id
|
|
|
|
def _find_host(self, hostname):
|
|
"""Get the given host ID."""
|
|
url = self.url + "/host?range=[0-65534]"
|
|
data = json.dumps({"TYPE": "21"})
|
|
result = self.call(url, data, "GET")
|
|
self._assert_rest_result(result, 'Find host in hostgroup error.')
|
|
|
|
host_id = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if hostname == item['NAME']:
|
|
host_id = item['ID']
|
|
break
|
|
return host_id
|
|
|
|
def _add_host(self, hostname):
|
|
"""Add a new host."""
|
|
url = self.url + "/host"
|
|
data = json.dumps({"TYPE": "21",
|
|
"NAME": hostname,
|
|
"OPERATIONSYSTEM": "0"})
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Add new host error.')
|
|
|
|
if "data" in result:
|
|
return result['data']['ID']
|
|
else:
|
|
return None
|
|
|
|
def _is_host_associate_to_hostgroup(self, hostgroup_id, host_id):
|
|
"""Check whether the host is associated to the hostgroup."""
|
|
url_subfix = ("/host/associate?TYPE=21&"
|
|
"ASSOCIATEOBJTYPE=14&ASSOCIATEOBJID=%s" % hostgroup_id)
|
|
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Check hostgroup associated error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if host_id == item['ID']:
|
|
return True
|
|
|
|
return False
|
|
|
|
def _is_lun_associated_to_lungroup(self, lungroup_id, lun_id):
|
|
"""Check whether the lun is associated to the lungroup."""
|
|
url_subfix = ("/lun/associate?TYPE=11&"
|
|
"ASSOCIATEOBJTYPE=256&ASSOCIATEOBJID=%s" % lungroup_id)
|
|
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Check lungroup associate error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if lun_id == item['ID']:
|
|
return True
|
|
|
|
return False
|
|
|
|
def _associate_host_to_hostgroup(self, hostgroup_id, host_id):
|
|
url = self.url + "/hostgroup/associate"
|
|
data = json.dumps({"TYPE": "14",
|
|
"ID": hostgroup_id,
|
|
"ASSOCIATEOBJTYPE": "21",
|
|
"ASSOCIATEOBJID": host_id})
|
|
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Associate host to hostgroup error.')
|
|
|
|
def _associate_lun_to_lungroup(self, lungroupid, lunid):
|
|
"""Associate lun to lungroup."""
|
|
url = self.url + "/lungroup/associate"
|
|
data = json.dumps({"ID": lungroupid,
|
|
"ASSOCIATEOBJTYPE": "11",
|
|
"ASSOCIATEOBJID": lunid})
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Associate lun to lungroup error.')
|
|
|
|
def _remove_lun_from_lungroup(self, lungroupid, lunid):
|
|
"""Remove lun from lungroup."""
|
|
|
|
url = self.url + ("/lungroup/associate?ID=%s"
|
|
"&ASSOCIATEOBJTYPE=11&ASSOCIATEOBJID=%s"
|
|
% (lungroupid, lunid))
|
|
|
|
result = self.call(url, None, 'DELETE')
|
|
self._assert_rest_result(result,
|
|
'Delete associated lun from lungroup error.')
|
|
|
|
def _initiator_is_added_to_array(self, ininame):
|
|
"""Check whether the initiator is already added on the array."""
|
|
url = self.url + "/iscsi_initiator?range=[0-65535]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result,
|
|
'Check initiator added to array error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if item["ID"] == ininame:
|
|
return True
|
|
return False
|
|
|
|
def _is_initiator_associated_to_host(self, ininame):
|
|
"""Check whether the initiator is associated to the host."""
|
|
url = self.url + "/iscsi_initiator?range=[0-65535]"
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result,
|
|
'Check initiator associated to host error.')
|
|
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if item['ID'] == ininame and item['ISFREE'] == "true":
|
|
return False
|
|
return True
|
|
|
|
def _add_initiator_to_array(self, ininame):
|
|
"""Add a new initiator to storage device."""
|
|
url = self.url + "/iscsi_initiator/"
|
|
data = json.dumps({"TYPE": "222",
|
|
"ID": ininame,
|
|
"USECHAP": "false"})
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Add initiator to array error.')
|
|
|
|
def _associate_initiator_to_host(self, ininame, hostid):
|
|
"""Associate initiator with the host."""
|
|
url = self.url + "/iscsi_initiator/" + ininame
|
|
data = json.dumps({"TYPE": "222",
|
|
"ID": ininame,
|
|
"USECHAP": "false",
|
|
"PARENTTYPE": "21",
|
|
"PARENTID": hostid})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Associate initiator to host error.')
|
|
|
|
def _find_mapping_view(self, name):
|
|
"""Find mapping view."""
|
|
url = self.url + "/mappingview?range=[0-65535]"
|
|
data = json.dumps({"TYPE": "245"})
|
|
result = self.call(url, data, "GET")
|
|
|
|
msg = 'Find map view error.'
|
|
self._assert_rest_result(result, msg)
|
|
viewid = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if name == item['NAME']:
|
|
viewid = item['ID']
|
|
break
|
|
|
|
return viewid
|
|
|
|
def _add_mapping_view(self, name):
|
|
url = self.url + "/mappingview"
|
|
data = json.dumps({"NAME": name, "TYPE": "245"})
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Add map view error.')
|
|
|
|
return result['data']['ID']
|
|
|
|
def _associate_hostgroup_to_view(self, viewID, hostGroupID):
|
|
url = self.url + "/MAPPINGVIEW/CREATE_ASSOCIATE"
|
|
data = json.dumps({"ASSOCIATEOBJTYPE": "14",
|
|
"ASSOCIATEOBJID": hostGroupID,
|
|
"TYPE": "245",
|
|
"ID": viewID})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Associate host to view error.')
|
|
|
|
def _associate_lungroup_to_view(self, viewID, lunGroupID):
|
|
url = self.url + "/MAPPINGVIEW/CREATE_ASSOCIATE"
|
|
data = json.dumps({"ASSOCIATEOBJTYPE": "256",
|
|
"ASSOCIATEOBJID": lunGroupID,
|
|
"TYPE": "245",
|
|
"ID": viewID})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Associate lungroup to view error.')
|
|
|
|
def _delete_lungroup_mapping_view(self, view_id, lungroup_id):
|
|
"""Remove lungroup associate from the mapping view."""
|
|
url = self.url + "/mappingview/REMOVE_ASSOCIATE"
|
|
data = json.dumps({"ASSOCIATEOBJTYPE": "256",
|
|
"ASSOCIATEOBJID": lungroup_id,
|
|
"TYPE": "245",
|
|
"ID": view_id})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Delete lungroup from view error.')
|
|
|
|
def _delete_hostgoup_mapping_view(self, view_id, hostgroup_id):
|
|
"""Remove hostgroup associate from the mapping view."""
|
|
url = self.url + "/mappingview/REMOVE_ASSOCIATE"
|
|
data = json.dumps({"ASSOCIATEOBJTYPE": "14",
|
|
"ASSOCIATEOBJID": hostgroup_id,
|
|
"TYPE": "245",
|
|
"ID": view_id})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Delete hostgroup from view error.')
|
|
|
|
def _delete_mapping_view(self, view_id):
|
|
"""Remove mapping view from the storage."""
|
|
url = self.url + "/mappingview/" + view_id
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Delete map view error.')
|
|
|
|
def _get_lunnum_from_lungroup(self, lungroup_id):
|
|
"""Check if there are still other luns associated to the lungroup."""
|
|
url_subfix = ("/lun/count?TYPE=11&ASSOCIATEOBJTYPE=256&"
|
|
"ASSOCIATEOBJID=%s" % lungroup_id)
|
|
url = self.url + url_subfix
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Find lun number error.')
|
|
if "data" in result:
|
|
lunnum = result['data']['COUNT']
|
|
return lunnum
|
|
return None
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def terminate_connection_iscsi(self, volume, connector):
|
|
"""Delete map between a volume and a host."""
|
|
initiator_name = connector['initiator']
|
|
volume_name = self._encode_name(volume['id'])
|
|
lun_id = volume.get('provider_location', None)
|
|
LOG.info(_LI('terminate_connection:volume name: %(volume)s, '
|
|
'initiator name: %(ini)s, lun_id: %(lunid)s.'),
|
|
{'volume': volume_name,
|
|
'ini': initiator_name,
|
|
'lunid': lun_id})
|
|
|
|
if lun_id:
|
|
if self._check_lun_exist(lun_id) is True:
|
|
# Get lungroupid by lun_id.
|
|
lungroup_id = self._get_lungroupid_by_lunid(lun_id)
|
|
|
|
if lungroup_id is None:
|
|
LOG.info(_LI("Can't find lun in lungroup."))
|
|
else:
|
|
self._remove_lun_from_lungroup(lungroup_id, lun_id)
|
|
LOG.info(_LI(
|
|
"Check if there are still other luns associated"
|
|
" to the lungroup."))
|
|
left_lunnum = self._get_lunnum_from_lungroup(lungroup_id)
|
|
return left_lunnum
|
|
|
|
else:
|
|
LOG.warning(_LW("Can't find lun on the array."))
|
|
|
|
def terminate_connection_fc(self, volume, connector):
|
|
"""Delete map between a volume and a host."""
|
|
wwns = connector['wwpns']
|
|
left_lunnum = self.terminate_connection_iscsi(volume, connector)
|
|
|
|
tgt_port_wwns = []
|
|
for wwn in wwns:
|
|
tgtwwpns = self._get_fc_target_wwpns(wwn)
|
|
if tgtwwpns:
|
|
tgt_port_wwns.append(tgtwwpns)
|
|
|
|
init_targ_map = {}
|
|
for initiator in wwns:
|
|
init_targ_map[initiator] = tgt_port_wwns
|
|
|
|
if left_lunnum and left_lunnum > 0:
|
|
info = {'driver_volume_type': 'fibre_channel',
|
|
'data': {}}
|
|
else:
|
|
info = {'driver_volume_type': 'fibre_channel',
|
|
'data': {'target_wwn': tgt_port_wwns,
|
|
'initiator_target_map': init_targ_map}}
|
|
|
|
return info
|
|
|
|
def login_out(self):
|
|
"""logout the session."""
|
|
url = self.url + "/sessions"
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Log out of session error.')
|
|
|
|
def _start_luncopy(self, luncopyid):
|
|
"""Start a LUNcopy."""
|
|
url = self.url + "/LUNCOPY/start"
|
|
data = json.dumps({"TYPE": "219", "ID": luncopyid})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Start lun copy error.')
|
|
|
|
def _get_capacity(self):
|
|
"""Get free capacity and total capacity of the pools."""
|
|
poolinfo = self._find_pool_info()
|
|
pool_capacity = {'total_capacity': 0.0,
|
|
'CAPACITY': 0.0}
|
|
|
|
if poolinfo:
|
|
total = int(poolinfo['TOTALCAPACITY']) / 1024.0 / 1024.0 / 2
|
|
free = int(poolinfo['CAPACITY']) / 1024.0 / 1024.0 / 2
|
|
pool_capacity['total_capacity'] = total
|
|
pool_capacity['free_capacity'] = free
|
|
|
|
return pool_capacity
|
|
|
|
def _get_lun_conf_params(self):
|
|
"""Get parameters from config file for creating lun."""
|
|
# Default lun set information.
|
|
lunsetinfo = {'LUNType': 'Thick',
|
|
'StripUnitSize': '64',
|
|
'WriteType': '1',
|
|
'MirrorSwitch': '1',
|
|
'PrefetchType': '3',
|
|
'PrefetchValue': '0',
|
|
'PrefetchTimes': '0'}
|
|
|
|
root = self._read_xml()
|
|
luntype = root.findtext('LUN/LUNType')
|
|
if luntype:
|
|
if luntype.strip() in ['Thick', 'Thin']:
|
|
lunsetinfo['LUNType'] = luntype.strip()
|
|
if luntype.strip() == 'Thick':
|
|
lunsetinfo['LUNType'] = 0
|
|
if luntype.strip() == 'Thin':
|
|
lunsetinfo['LUNType'] = 1
|
|
|
|
elif luntype is not '' and luntype is not None:
|
|
err_msg = (_(
|
|
'Config file is wrong. LUNType must be "Thin"'
|
|
' or "Thick". LUNType: %(fetchtype)s.')
|
|
% {'fetchtype': luntype})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
stripunitsize = root.findtext('LUN/StripUnitSize')
|
|
if stripunitsize is not None:
|
|
lunsetinfo['StripUnitSize'] = stripunitsize.strip()
|
|
writetype = root.findtext('LUN/WriteType')
|
|
if writetype is not None:
|
|
lunsetinfo['WriteType'] = writetype.strip()
|
|
mirrorswitch = root.findtext('LUN/MirrorSwitch')
|
|
if mirrorswitch is not None:
|
|
lunsetinfo['MirrorSwitch'] = mirrorswitch.strip()
|
|
|
|
prefetch = root.find('LUN/Prefetch')
|
|
fetchtype = prefetch.attrib['Type']
|
|
if prefetch is not None and prefetch.attrib['Type']:
|
|
if fetchtype in ['0', '1', '2', '3']:
|
|
lunsetinfo['PrefetchType'] = fetchtype.strip()
|
|
typevalue = prefetch.attrib['Value'].strip()
|
|
if lunsetinfo['PrefetchType'] == '1':
|
|
double_value = int(typevalue) * 2
|
|
typevalue_double = six.text_type(double_value)
|
|
lunsetinfo['PrefetchValue'] = typevalue_double
|
|
elif lunsetinfo['PrefetchType'] == '2':
|
|
lunsetinfo['PrefetchValue'] = typevalue
|
|
else:
|
|
err_msg = (_(
|
|
'PrefetchType config is wrong. PrefetchType'
|
|
' must be in 0,1,2,3. PrefetchType is: %(fetchtype)s.')
|
|
% {'fetchtype': fetchtype})
|
|
LOG.error(err_msg)
|
|
raise exception.CinderException(err_msg)
|
|
else:
|
|
LOG.info(_LI(
|
|
'Use default PrefetchType. '
|
|
'PrefetchType: Intelligent.'))
|
|
|
|
return lunsetinfo
|
|
|
|
def _get_luncopy_info(self, luncopyid):
|
|
"""Get LUNcopy information."""
|
|
url = self.url + "/LUNCOPY?range=[0-100000]"
|
|
data = json.dumps({"TYPE": "219", })
|
|
result = self.call(url, data, "GET")
|
|
self._assert_rest_result(result, 'Get lun copy information error.')
|
|
|
|
luncopyinfo = {}
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if luncopyid == item['ID']:
|
|
luncopyinfo['name'] = item['NAME']
|
|
luncopyinfo['id'] = item['ID']
|
|
luncopyinfo['state'] = item['HEALTHSTATUS']
|
|
luncopyinfo['status'] = item['RUNNINGSTATUS']
|
|
break
|
|
return luncopyinfo
|
|
|
|
def _delete_luncopy(self, luncopyid):
|
|
"""Delete a LUNcopy."""
|
|
url = self.url + "/LUNCOPY/%s" % luncopyid
|
|
result = self.call(url, None, "DELETE")
|
|
self._assert_rest_result(result, 'Delete lun copy error.')
|
|
|
|
def _get_connected_free_wwns(self):
|
|
"""Get free connected FC port WWNs.
|
|
|
|
If no new ports connected, return an empty list.
|
|
"""
|
|
url = self.url + "/fc_initiator?ISFREE=true&range=[0-1000]"
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get connected free FC wwn error.'
|
|
self._assert_rest_result(result, msg)
|
|
|
|
wwns = []
|
|
if 'data' in result:
|
|
for item in result['data']:
|
|
wwns.append(item['ID'])
|
|
|
|
return wwns
|
|
|
|
def _add_fc_port_to_host(self, hostid, wwn):
|
|
"""Add a FC port to the host."""
|
|
url = self.url + "/fc_initiator/" + wwn
|
|
data = json.dumps({"TYPE": "223",
|
|
"ID": wwn,
|
|
"PARENTTYPE": 21,
|
|
"PARENTID": hostid})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Add FC port to host error.')
|
|
|
|
def _get_iscsi_port_info(self, ip):
|
|
"""Get iscsi port info in order to build the iscsi target iqn."""
|
|
url = self.url + "/eth_port"
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get iSCSI port information error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
iscsi_port_info = None
|
|
for item in result['data']:
|
|
if ip == item['IPV4ADDR']:
|
|
iscsi_port_info = item['LOCATION']
|
|
break
|
|
|
|
return iscsi_port_info
|
|
|
|
def _get_iscsi_conf(self):
|
|
"""Get iSCSI info from config file."""
|
|
iscsiinfo = {}
|
|
root = self._read_xml()
|
|
TargetIP = root.findtext('iSCSI/DefaultTargetIP').strip()
|
|
iscsiinfo['DefaultTargetIP'] = TargetIP
|
|
initiator_list = []
|
|
|
|
for dic in root.findall('iSCSI/Initiator'):
|
|
# Strip values of dic.
|
|
tmp_dic = {}
|
|
for k in dic.items():
|
|
tmp_dic[k[0]] = k[1].strip()
|
|
|
|
initiator_list.append(tmp_dic)
|
|
|
|
iscsiinfo['Initiator'] = initiator_list
|
|
|
|
return iscsiinfo
|
|
|
|
def _get_tgt_iqn(self, iscsiip):
|
|
"""Get target iSCSI iqn."""
|
|
|
|
ip_info = self._get_iscsi_port_info(iscsiip)
|
|
iqn_prefix = self._get_iscsi_tgt_port()
|
|
|
|
LOG.info(_LI('Request ip info is: %s.'), ip_info)
|
|
split_list = ip_info.split(".")
|
|
newstr = split_list[1] + split_list[2]
|
|
LOG.info(_LI('New str info is: %s.'), newstr)
|
|
|
|
if ip_info:
|
|
if newstr[0] == 'A':
|
|
ctr = "0"
|
|
elif newstr[0] == 'B':
|
|
ctr = "1"
|
|
interface = '0' + newstr[1]
|
|
port = '0' + newstr[3]
|
|
iqn_suffix = ctr + '02' + interface + port
|
|
for i in range(0, len(iqn_suffix)):
|
|
if iqn_suffix[i] != '0':
|
|
iqn_suffix = iqn_suffix[i:]
|
|
break
|
|
iqn = iqn_prefix + ':' + iqn_suffix + ':' + iscsiip
|
|
LOG.info(_LI('_get_tgt_iqn: iSCSI target iqn is: %s.'), iqn)
|
|
return iqn
|
|
else:
|
|
return None
|
|
|
|
def _get_fc_target_wwpns(self, wwn):
|
|
url = (self.url +
|
|
"/host_link?INITIATOR_TYPE=223&INITIATOR_PORT_WWN=" + wwn)
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get FC target wwpn error.'
|
|
self._assert_rest_result(result, msg)
|
|
|
|
fc_wwpns = None
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if wwn == item['INITIATOR_PORT_WWN']:
|
|
fc_wwpns = item['TARGET_PORT_WWN']
|
|
break
|
|
|
|
return fc_wwpns
|
|
|
|
def update_volume_stats(self):
|
|
capacity = self._get_capacity()
|
|
data = {}
|
|
data['vendor_name'] = 'Huawei'
|
|
data['total_capacity_gb'] = capacity['total_capacity']
|
|
data['free_capacity_gb'] = capacity['free_capacity']
|
|
data['reserved_percentage'] = 0
|
|
data['QoS_support'] = True
|
|
data['Tier_support'] = True
|
|
return data
|
|
|
|
def _find_qos_policy_info(self, policy_name):
|
|
url = self.url + "/ioclass"
|
|
result = self.call(url, None, "GET")
|
|
|
|
msg = 'Get QoS policy error.'
|
|
self._assert_rest_result(result, msg)
|
|
|
|
qos_info = {}
|
|
if "data" in result:
|
|
for item in result['data']:
|
|
if policy_name == item['NAME']:
|
|
qos_info['ID'] = item['ID']
|
|
lun_list = json.loads(item['LUNLIST'])
|
|
qos_info['LUNLIST'] = lun_list
|
|
qos_info['RUNNINGSTATUS'] = item['RUNNINGSTATUS']
|
|
break
|
|
|
|
return qos_info
|
|
|
|
def _update_qos_policy_lunlist(self, lunlist, policy_id):
|
|
url = self.url + "/ioclass/" + policy_id
|
|
data = json.dumps({"TYPE": "230",
|
|
"ID": policy_id,
|
|
"LUNLIST": lunlist})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Update QoS policy error.')
|
|
|
|
def _get_login_info(self):
|
|
"""Get login IP, username and password from config file."""
|
|
logininfo = {}
|
|
filename = self.configuration.cinder_huawei_conf_file
|
|
tree = ET.parse(filename)
|
|
root = tree.getroot()
|
|
logininfo['RestURL'] = root.findtext('Storage/RestURL').strip()
|
|
|
|
need_encode = False
|
|
for key in ['UserName', 'UserPassword']:
|
|
node = root.find('Storage/%s' % key)
|
|
node_text = node.text
|
|
# Prefix !$$$ means encoded already.
|
|
if node_text.find('!$$$') > -1:
|
|
logininfo[key] = base64.b64decode(node_text[4:])
|
|
else:
|
|
logininfo[key] = node_text
|
|
node.text = '!$$$' + base64.b64encode(node_text)
|
|
need_encode = True
|
|
if need_encode:
|
|
self._change_file_mode(filename)
|
|
try:
|
|
tree.write(filename, 'UTF-8')
|
|
except Exception as err:
|
|
LOG.warning(_LW('Unable to access config file. %s'), err)
|
|
|
|
return logininfo
|
|
|
|
def _change_file_mode(self, filepath):
|
|
utils.execute('chmod', '640', filepath, run_as_root=True)
|
|
|
|
def _check_conf_file(self):
|
|
"""Check the config file, make sure the essential items are set."""
|
|
root = self._read_xml()
|
|
resturl = root.findtext('Storage/RestURL')
|
|
username = root.findtext('Storage/UserName')
|
|
pwd = root.findtext('Storage/UserPassword')
|
|
pool_node = root.findall('LUN/StoragePool')
|
|
|
|
if (not resturl) or (not username) or (not pwd):
|
|
err_msg = (_(
|
|
'_check_conf_file: Config file invalid. RestURL,'
|
|
' UserName and UserPassword must be set.'))
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
if not pool_node:
|
|
err_msg = (_(
|
|
'_check_conf_file: Config file invalid. '
|
|
'StoragePool must be set.'))
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
def _get_iscsi_params(self, connector):
|
|
"""Get target iSCSI params, including iqn, IP."""
|
|
initiator = connector['initiator']
|
|
iscsi_conf = self._get_iscsi_conf()
|
|
target_ip = None
|
|
for ini in iscsi_conf['Initiator']:
|
|
if ini['Name'] == initiator:
|
|
target_ip = ini['TargetIP']
|
|
break
|
|
# If didn't specify target IP for some initiator, use default IP.
|
|
if not target_ip:
|
|
if iscsi_conf['DefaultTargetIP']:
|
|
target_ip = iscsi_conf['DefaultTargetIP']
|
|
|
|
else:
|
|
msg = (_(
|
|
'_get_iscsi_params: Failed to get target IP '
|
|
'for initiator %(ini)s, please check config file.')
|
|
% {'ini': initiator})
|
|
LOG.error(msg)
|
|
raise exception.InvalidInput(reason=msg)
|
|
|
|
# If didn't get target IP for rest, Automated assembly target ip.
|
|
target_iqn = self._get_tgt_iqn_from_rest(target_ip)
|
|
|
|
if not target_iqn:
|
|
target_iqn = self._get_tgt_iqn(target_ip)
|
|
|
|
return (target_iqn, target_ip)
|
|
|
|
def _get_tgt_iqn_from_rest(self, target_ip):
|
|
url = self.url + "/iscsi_tgt_port"
|
|
result = self.call(url, None, "GET")
|
|
|
|
target_iqn = None
|
|
if result['error']['code'] != 0:
|
|
LOG.warning(_LW("Can't find target iqn from rest."))
|
|
return target_iqn
|
|
|
|
if 'data' in result:
|
|
for item in result['data']:
|
|
if target_ip in item['ID']:
|
|
target_iqn = item['ID']
|
|
|
|
if not target_iqn:
|
|
LOG.warning(_LW("Can't find target iqn from rest."))
|
|
return target_iqn
|
|
|
|
split_list = target_iqn.split(",")
|
|
target_iqn_before = split_list[0]
|
|
|
|
split_list_new = target_iqn_before.split("+")
|
|
target_iqn = split_list_new[1]
|
|
|
|
return target_iqn
|
|
|
|
@utils.synchronized('huawei', external=True)
|
|
def extend_volume(self, volume, new_size):
|
|
"""Extends a Huawei volume."""
|
|
|
|
LOG.info(_LI('Entering extend_volume.'))
|
|
volume_size = self._get_volume_size(volume)
|
|
new_volume_size = int(new_size) * units.Gi / 512
|
|
volume_name = self._encode_name(volume['id'])
|
|
|
|
LOG.info(_LI('Extend Volume: %(volumename)s, oldsize: %(oldsize)s '
|
|
'newsize: %(newsize)s.'),
|
|
{'volumename': volume_name,
|
|
'oldsize': volume_size,
|
|
'newsize': new_volume_size})
|
|
|
|
lun_id = self._get_volume_by_name(volume_name)
|
|
|
|
if lun_id is None:
|
|
msg = (_(
|
|
"Can't find lun info on the array, lun name is: %(name)s.")
|
|
% {'name': volume_name})
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
|
|
url = self.url + "/lun/expand"
|
|
data = json.dumps({"TYPE": 11, "ID": lun_id,
|
|
"CAPACITY": new_volume_size})
|
|
result = self.call(url, data, 'PUT')
|
|
|
|
msg = 'Extend volume error.'
|
|
self._assert_rest_result(result, msg)
|
|
self._assert_data_in_result(result, msg)
|
|
|
|
return result['data']['ID']
|
|
|
|
def _get_volume_type(self, type_id):
|
|
ctxt = context.get_admin_context()
|
|
return volume_types.get_volume_type(ctxt, type_id)
|
|
|
|
def _get_qos_by_volume_type(self, volume_type):
|
|
qos = {}
|
|
qos_specs_id = volume_type.get('qos_specs_id')
|
|
specs = volume_type.get('extra_specs')
|
|
|
|
# NOTE(kmartin): We prefer the qos_specs association
|
|
# and override any existing extra-specs settings
|
|
# if present.
|
|
if qos_specs_id is not None:
|
|
kvs = qos_specs.get_qos_specs(context.get_admin_context(),
|
|
qos_specs_id)['specs']
|
|
else:
|
|
kvs = specs
|
|
|
|
LOG.info(_LI('The QoS sepcs is: %s.'), kvs)
|
|
for key, value in kvs.iteritems():
|
|
if key in huawei_valid_keys:
|
|
qos[key.upper()] = value
|
|
|
|
return qos
|
|
|
|
def _get_qos_value(self, qos, key, default=None):
|
|
if key in qos:
|
|
return qos[key]
|
|
else:
|
|
return default
|
|
|
|
def _create_qos_policy(self, qos, lun_id):
|
|
|
|
# Get local time.
|
|
localtime = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
|
|
# Package QoS name.
|
|
qos_name = QOS_NAME_PREFIX + lun_id + '_' + localtime
|
|
baseData = {"TYPE": "230",
|
|
"NAME": qos_name,
|
|
"LUNLIST": ["%s" % lun_id],
|
|
"CLASSTYPE": "1",
|
|
"SCHEDULEPOLICY": "2",
|
|
"SCHEDULESTARTTIME": "1410969600",
|
|
"STARTTIME": "08:00",
|
|
"DURATION": "86400",
|
|
"CYCLESET": "[1,2,3,4,5,6,0]"
|
|
}
|
|
|
|
mergedata = dict(baseData.items() + qos.items())
|
|
url = self.url + "/ioclass/"
|
|
data = json.dumps(mergedata)
|
|
|
|
result = self.call(url, data)
|
|
self._assert_rest_result(result, 'Create QoS policy error.')
|
|
|
|
return result['data']['ID']
|
|
|
|
def _delete_qos_policy(self, qos_id):
|
|
"""Delete a QoS policy."""
|
|
|
|
url = self.url + "/ioclass/" + qos_id
|
|
data = json.dumps({"TYPE": "230",
|
|
"ID": qos_id})
|
|
|
|
result = self.call(url, data, 'DELETE')
|
|
self._assert_rest_result(result, 'Delete QoS policy error.')
|
|
|
|
def _active_deactive_qos(self, qos_id, enablestatus):
|
|
"""Active or deactive QoS.
|
|
|
|
enablestatus: true (active)
|
|
enbalestatus: false (deactive)
|
|
"""
|
|
|
|
url = self.url + "/ioclass/active/" + qos_id
|
|
data = json.dumps({"TYPE": 230,
|
|
"ID": qos_id,
|
|
"ENABLESTATUS": enablestatus})
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Active or Deactive QoS error.')
|
|
|
|
def _get_qos_info(self, qos_id):
|
|
"""Get QoS information."""
|
|
|
|
url = self.url + "/ioclass/" + qos_id
|
|
data = json.dumps({"TYPE": "230",
|
|
"ID": qos_id})
|
|
result = self.call(url, data, "GET")
|
|
self._assert_rest_result(result, 'Get QoS information error.')
|
|
|
|
return result['data']
|
|
|
|
def _check_qos_high_priority(self, qos):
|
|
"""Check QoS priority."""
|
|
|
|
for key, value in qos.iteritems():
|
|
if (key.find('MIN') == 0) or (key.find('LATENCY') == 0):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _change_lun_priority(self, lunid):
|
|
"""Change lun priority to high."""
|
|
|
|
url = self.url + "/lun/" + lunid
|
|
data = json.dumps({"TYPE": "11",
|
|
"ID": lunid,
|
|
"IOPRIORITY": "3"})
|
|
|
|
result = self.call(url, data, "PUT")
|
|
self._assert_rest_result(result, 'Change lun priority error.')
|
|
|
|
def _get_qosid_by_lunid(self, lunid):
|
|
"""Get qosid by lunid."""
|
|
|
|
url = self.url + "/lun/" + lunid
|
|
data = json.dumps({"TYPE": "11",
|
|
"ID": lunid})
|
|
|
|
result = self.call(url, data, "GET")
|
|
self._assert_rest_result(result, 'Get qosid by lunid error.')
|
|
|
|
return result['data']['IOCLASSID']
|
|
|
|
def _get_lungroupid_by_lunid(self, lunid):
|
|
"""Get lungroupid by lunid."""
|
|
|
|
url = self.url + ("/lungroup/associate?TYPE=256"
|
|
"&ASSOCIATEOBJTYPE=11&ASSOCIATEOBJID=%s" % lunid)
|
|
|
|
result = self.call(url, None, "GET")
|
|
self._assert_rest_result(result, 'Get lungroupid by lunid error.')
|
|
|
|
lun_group_id = None
|
|
# Lun only in one lungroup.
|
|
if 'data' in result:
|
|
for item in result['data']:
|
|
lun_group_id = item['ID']
|
|
|
|
return lun_group_id
|