1401 lines
56 KiB
Python
1401 lines
56 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (c) 2013 Huawei Technologies Co., Ltd.
|
|
# Copyright (c) 2012 OpenStack Foundation
|
|
# 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 classes for Huawei OceanStor T series and Dorado series storage arrays.
|
|
|
|
The common classes provide the drivers command line operation using SSH.
|
|
"""
|
|
|
|
import base64
|
|
import re
|
|
import socket
|
|
import threading
|
|
import time
|
|
|
|
from xml.etree import ElementTree as ET
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder.openstack.common import excutils
|
|
from cinder.openstack.common import log as logging
|
|
from cinder import utils
|
|
from cinder.volume.drivers.huawei import huawei_utils
|
|
from cinder.volume import volume_types
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
HOST_GROUP_NAME = 'HostGroup_OpenStack'
|
|
HOST_NAME_PREFIX = 'Host_'
|
|
VOL_AND_SNAP_NAME_PREFIX = 'OpenStack_'
|
|
|
|
|
|
def ssh_read(user, channel, cmd, timeout):
|
|
"""Get results of CLI commands."""
|
|
result = ''
|
|
channel.settimeout(timeout)
|
|
while True:
|
|
try:
|
|
result = result + channel.recv(8192)
|
|
except socket.timeout as err:
|
|
msg = _('ssh_read: Read SSH timeout. %s') % err
|
|
LOG.error(msg)
|
|
raise err
|
|
else:
|
|
# CLI returns welcome information when first log in. So need to
|
|
# deal differently.
|
|
if not re.search('Welcome', result):
|
|
# Complete CLI response starts with CLI cmd and
|
|
# ends with "username:/>".
|
|
if result.startswith(cmd) and result.endswith(user + ':/>'):
|
|
break
|
|
# Some commands need to send 'y'.
|
|
elif re.search('(y/n)', result):
|
|
break
|
|
# Reach maximum limit of SSH connection.
|
|
elif re.search('No response message', result):
|
|
msg = _('No response message. Please check system status.')
|
|
LOG.error(msg)
|
|
raise exception.CinderException(msg)
|
|
elif (re.search(user + ':/>' + cmd, result) and
|
|
result.endswith(user + ':/>')):
|
|
break
|
|
|
|
# Filter the last line: username:/> .
|
|
result = '\r\n'.join(result.split('\r\n')[:-1])
|
|
# Filter welcome information.
|
|
index = result.find(user + ':/>')
|
|
|
|
return (result[index:] if index > -1 else result)
|
|
|
|
|
|
class TseriesCommon():
|
|
"""Common class for Huawei T series storage arrays."""
|
|
|
|
def __init__(self, configuration=None):
|
|
self.configuration = configuration
|
|
self.xml_conf = self.configuration.cinder_huawei_conf_file
|
|
self.login_info = {}
|
|
self.lun_distribution = [0, 0]
|
|
self.hostgroup_id = None
|
|
self.ssh_pool = None
|
|
self.lock_ip = threading.Lock()
|
|
self.luncopy_list = [] # to store LUNCopy name
|
|
|
|
def do_setup(self, context):
|
|
"""Check config file."""
|
|
LOG.debug(_('do_setup'))
|
|
|
|
self._check_conf_file()
|
|
self.login_info = self._get_login_info()
|
|
self.lun_distribution = self._get_lun_distribution_info()
|
|
self.luncopy_list = self._get_all_luncopy_name()
|
|
self.hostgroup_id = self._get_hostgroup_id(HOST_GROUP_NAME)
|
|
|
|
def _check_conf_file(self):
|
|
"""Check config file, make sure essential items are set."""
|
|
root = huawei_utils.parse_xml_file(self.xml_conf)
|
|
check_list = ['Storage/ControllerIP0', 'Storage/ControllerIP1',
|
|
'Storage/UserName', 'Storage/UserPassword']
|
|
for item in check_list:
|
|
if not huawei_utils.is_xml_item_exist(root, item):
|
|
err_msg = (_('_check_conf_file: Config file invalid. '
|
|
'%s must be set.') % item)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
# make sure storage pool is set
|
|
if not huawei_utils.is_xml_item_exist(root, 'LUN/StoragePool', 'Name'):
|
|
err_msg = _('_check_conf_file: Config file invalid. '
|
|
'StoragePool must be set.')
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
# If setting os type, make sure it valid
|
|
if huawei_utils.is_xml_item_exist(root, 'Host', 'OSType'):
|
|
os_list = huawei_utils.os_type.keys()
|
|
if not huawei_utils.is_xml_item_valid(root, 'Host', os_list,
|
|
'OSType'):
|
|
err_msg = (_('_check_conf_file: Config file invalid. '
|
|
'Host OSType is invalid.\n'
|
|
'The valid values are: %(os_list)s')
|
|
% {'os_list': os_list})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
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['ControllerIP0'] =\
|
|
root.findtext('Storage/ControllerIP0').strip()
|
|
logininfo['ControllerIP1'] =\
|
|
root.findtext('Storage/ControllerIP1').strip()
|
|
|
|
need_encode = False
|
|
for key in ['UserName', 'UserPassword']:
|
|
node = root.find('Storage/%s' % key)
|
|
node_text = node.text.strip()
|
|
# 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.info(_('_get_login_info: %s') % err)
|
|
|
|
return logininfo
|
|
|
|
def _change_file_mode(self, filepath):
|
|
utils.execute('chmod', '777', filepath, run_as_root=True)
|
|
|
|
def _get_lun_distribution_info(self):
|
|
"""Get LUN distribution information.
|
|
|
|
For we have two controllers for each array, we want to make all
|
|
LUNs(just for Thick LUN) distributed evenly. The driver uses the
|
|
LUN distribution info to determine in which controller to create
|
|
a new LUN.
|
|
|
|
"""
|
|
|
|
luns = self._get_all_luns_info()
|
|
ctr_info = [0, 0]
|
|
for lun in luns:
|
|
if (lun[6].startswith(VOL_AND_SNAP_NAME_PREFIX) and
|
|
lun[8] == 'THICK'):
|
|
if lun[4] == 'A':
|
|
ctr_info[0] += 1
|
|
else:
|
|
ctr_info[1] += 1
|
|
return ctr_info
|
|
|
|
def check_for_setup_error(self):
|
|
pass
|
|
|
|
def _get_all_luncopy_name(self):
|
|
cli_cmd = 'showluncopy'
|
|
out = self._execute_cli(cli_cmd)
|
|
luncopy_ids = []
|
|
if re.search('LUN Copy Information', out):
|
|
for line in out.split('\r\n')[6:-2]:
|
|
tmp_line = line.split()
|
|
if tmp_line[0].startswith(VOL_AND_SNAP_NAME_PREFIX):
|
|
luncopy_ids.append(tmp_line[0])
|
|
return luncopy_ids
|
|
|
|
def create_volume(self, volume):
|
|
"""Create a new volume."""
|
|
volume_name = self._name_translate(volume['name'])
|
|
|
|
LOG.debug(_('create_volume: volume name: %s') % volume_name)
|
|
|
|
self._update_login_info()
|
|
if int(volume['size']) == 0:
|
|
volume_size = '100M'
|
|
else:
|
|
volume_size = '%sG' % volume['size']
|
|
type_id = volume['volume_type_id']
|
|
parameters = self._parse_volume_type(type_id)
|
|
volume_id = self._create_volume(volume_name, volume_size, parameters)
|
|
return volume_id
|
|
|
|
def _name_translate(self, name):
|
|
"""Form new names for volume and snapshot because of
|
|
32-character limit on names.
|
|
"""
|
|
newname = VOL_AND_SNAP_NAME_PREFIX + str(hash(name))
|
|
|
|
LOG.debug(_('_name_translate: Name in cinder: %(old)s, new name in '
|
|
'storage system: %(new)s') % {'old': name, 'new': newname})
|
|
|
|
return newname
|
|
|
|
def _update_login_info(self):
|
|
"""Update user name and password."""
|
|
self.login_info = self._get_login_info()
|
|
|
|
def _parse_volume_type(self, typeid):
|
|
"""Parse volume type form extra_specs by type id.
|
|
|
|
The keys in extra_specs must be consistent with the element in config
|
|
file. And the keys can starts with "drivers" to make them distinguished
|
|
from capabilities keys, if you like.
|
|
|
|
"""
|
|
|
|
params = self._get_lun_params()
|
|
if typeid is not None:
|
|
ctxt = context.get_admin_context()
|
|
volume_type = volume_types.get_volume_type(ctxt, typeid)
|
|
specs = volume_type.get('extra_specs')
|
|
for key, value in specs.iteritems():
|
|
key_split = key.split(':')
|
|
if len(key_split) > 1:
|
|
if key_split[0] == 'drivers':
|
|
key = key_split[1]
|
|
else:
|
|
continue
|
|
else:
|
|
key = key_split[0]
|
|
|
|
if key in params.keys():
|
|
params[key] = value.strip()
|
|
else:
|
|
conf = self.configuration.cinder_huawei_conf_file
|
|
LOG.warn(_('_parse_volume_type: Unacceptable parameter '
|
|
'%(key)s. Please check this key in extra_specs '
|
|
'and make it consistent with the element in '
|
|
'configuration file %(conf)s.')
|
|
% {'key': key,
|
|
'conf': conf})
|
|
|
|
return params
|
|
|
|
def _create_volume(self, name, size, params):
|
|
"""Create a new volume with the given name and size."""
|
|
cli_cmd = ('createlun -n %(name)s -lunsize %(size)s '
|
|
'-wrtype %(wrtype)s ' % {'name': name,
|
|
'size': size,
|
|
'wrtype': params['WriteType']})
|
|
|
|
# If write type is "write through", no need to set mirror switch.
|
|
if params['WriteType'] != '2':
|
|
cli_cmd = cli_cmd + ('-mirrorsw %(mirrorsw)s '
|
|
% {'mirrorsw': params['MirrorSwitch']})
|
|
|
|
# Differences exist between "Thin" and "thick" LUN in CLI commands.
|
|
luntype = params['LUNType']
|
|
ctr = None
|
|
if luntype == 'Thin':
|
|
cli_cmd = cli_cmd + ('-pool %(pool)s '
|
|
% {'pool': params['StoragePool']})
|
|
else:
|
|
# Make LUN distributed to A/B controllers evenly,
|
|
# just for Thick LUN.
|
|
ctr = self._calculate_lun_ctr()
|
|
cli_cmd = cli_cmd + ('-rg %(raidgroup)s -susize %(susize)s '
|
|
'-c %(ctr)s '
|
|
% {'raidgroup': params['StoragePool'],
|
|
'susize': params['StripUnitSize'],
|
|
'ctr': ctr})
|
|
|
|
prefetch_value_or_times = ''
|
|
pretype = '-pretype %s ' % params['PrefetchType']
|
|
# If constant prefetch, we should specify prefetch value.
|
|
if params['PrefetchType'] == '1':
|
|
prefetch_value_or_times = '-value %s' % params['PrefetchValue']
|
|
# If variable prefetch, we should specify prefetch mutiple.
|
|
elif params['PrefetchType'] == '2':
|
|
prefetch_value_or_times = '-times %s' % params['PrefetchTimes']
|
|
|
|
cli_cmd = cli_cmd + pretype + prefetch_value_or_times
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_volume',
|
|
'Failed to create volume %s' % name,
|
|
cli_cmd, out)
|
|
if ctr:
|
|
self._update_lun_distribution(ctr)
|
|
|
|
return self._get_lun_id(name)
|
|
|
|
def _calculate_lun_ctr(self):
|
|
return ('a' if self.lun_distribution[0] <= self.lun_distribution[1]
|
|
else 'b')
|
|
|
|
def _update_lun_distribution(self, ctr):
|
|
index = (0 if ctr == 'a' else 1)
|
|
self.lun_distribution[index] += 1
|
|
|
|
def _get_lun_params(self):
|
|
params_conf = self._parse_conf_lun_params()
|
|
# Select a pool with maximum capacity.
|
|
pools_dev = self._get_dev_pool_info(params_conf['LUNType'])
|
|
params_conf['StoragePool'] = \
|
|
self._get_maximum_capacity_pool_id(params_conf['StoragePool'],
|
|
pools_dev,
|
|
params_conf['LUNType'])
|
|
return params_conf
|
|
|
|
def _parse_conf_lun_params(self):
|
|
"""Get parameters from config file for creating LUN."""
|
|
# Default LUN parameters.
|
|
conf_params = {'LUNType': 'Thin',
|
|
'StripUnitSize': '64',
|
|
'WriteType': '1',
|
|
'MirrorSwitch': '1',
|
|
'PrefetchType': '3',
|
|
'PrefetchValue': '0',
|
|
'PrefetchTimes': '0',
|
|
'StoragePool': []}
|
|
|
|
root = huawei_utils.parse_xml_file(self.xml_conf)
|
|
|
|
luntype = root.findtext('LUN/LUNType')
|
|
if luntype:
|
|
if luntype.strip() in ['Thick', 'Thin']:
|
|
conf_params['LUNType'] = luntype.strip()
|
|
else:
|
|
err_msg = (_('LUNType must be "Thin" or "Thick". '
|
|
'LUNType:%(type)s') % {'type': luntype})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
stripunitsize = root.findtext('LUN/StripUnitSize')
|
|
if stripunitsize:
|
|
conf_params['StripUnitSize'] = stripunitsize.strip()
|
|
writetype = root.findtext('LUN/WriteType')
|
|
if writetype:
|
|
conf_params['WriteType'] = writetype.strip()
|
|
mirrorswitch = root.findtext('LUN/MirrorSwitch')
|
|
if mirrorswitch:
|
|
conf_params['MirrorSwitch'] = mirrorswitch.strip()
|
|
prefetch = root.find('LUN/Prefetch')
|
|
if prefetch is not None and prefetch.attrib['Type']:
|
|
conf_params['PrefetchType'] = prefetch.attrib['Type'].strip()
|
|
if conf_params['PrefetchType'] == '1':
|
|
conf_params['PrefetchValue'] = prefetch.attrib['Value'].strip()
|
|
elif conf_params['PrefetchType'] == '2':
|
|
conf_params['PrefetchTimes'] = prefetch.attrib['Value'].strip()
|
|
else:
|
|
LOG.debug(_('_parse_conf_lun_params: Use default prefetch type. '
|
|
'Prefetch type: Intelligent'))
|
|
|
|
pools_conf = root.findall('LUN/StoragePool')
|
|
for pool in pools_conf:
|
|
conf_params['StoragePool'].append(pool.attrib['Name'].strip())
|
|
|
|
return conf_params
|
|
|
|
def _get_maximum_capacity_pool_id(self, pools_conf, pools_dev, luntype):
|
|
"""Get the maximum pool from config file.
|
|
|
|
According to the given pools' names in config file,
|
|
we select the pool with maximum free capacity.
|
|
|
|
"""
|
|
|
|
maxpool_id = None
|
|
maxpool_size = 0.0
|
|
nameindex, sizeindex = ((1, 4) if luntype == 'Thin' else (5, 3))
|
|
pools_dev = sorted(pools_dev, key=lambda x: float(x[sizeindex]))
|
|
while len(pools_dev) > 0:
|
|
pool = pools_dev.pop()
|
|
if pool[nameindex] in pools_conf:
|
|
return pool[0]
|
|
|
|
err_msg = (_('_get_maximum_capacity_pool_id: Failed to get pool '
|
|
'id. Please check config file and make sure '
|
|
'the StoragePool %s is created in storage '
|
|
'array.') % pools_conf)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
def _execute_cli(self, cmd):
|
|
"""Build SSH connection and execute CLI commands.
|
|
|
|
If the connection to first controller timeout,
|
|
try to connect to the other controller.
|
|
|
|
"""
|
|
|
|
LOG.debug(_('CLI command: %s') % cmd)
|
|
connect_times = 1
|
|
ip0 = self.login_info['ControllerIP0']
|
|
ip1 = self.login_info['ControllerIP1']
|
|
user = self.login_info['UserName']
|
|
pwd = self.login_info['UserPassword']
|
|
if not self.ssh_pool:
|
|
self.ssh_pool = utils.SSHPool(ip0, 22, 30, user, pwd, max_size=2)
|
|
ssh_client = None
|
|
while True:
|
|
try:
|
|
if connect_times == 2:
|
|
# Switch to the other controller.
|
|
with self.lock_ip:
|
|
if ssh_client:
|
|
if ssh_client.server_ip == self.ssh_pool.ip:
|
|
self.ssh_pool.ip = (ip1
|
|
if self.ssh_pool.ip == ip0
|
|
else ip0)
|
|
old_ip = ssh_client.server_ip
|
|
# Create a new client to replace the old one.
|
|
if getattr(ssh_client, 'chan', None):
|
|
ssh_client.chan.close()
|
|
ssh_client.close()
|
|
ssh_client = self.ssh_pool.create()
|
|
self._reset_transport_timeout(ssh_client, 0.1)
|
|
else:
|
|
self.ssh_pool.ip = ip1
|
|
old_ip = ip0
|
|
|
|
LOG.info(_('_execute_cli: Can not connect to IP '
|
|
'%(old)s, try to connect to the other '
|
|
'IP %(new)s.')
|
|
% {'old': old_ip, 'new': self.ssh_pool.ip})
|
|
|
|
if not ssh_client:
|
|
# Get an SSH client from SSH pool.
|
|
ssh_client = self.ssh_pool.get()
|
|
self._reset_transport_timeout(ssh_client, 0.1)
|
|
# "server_ip" shows the IP of SSH server.
|
|
if not getattr(ssh_client, 'server_ip', None):
|
|
with self.lock_ip:
|
|
setattr(ssh_client, 'server_ip', self.ssh_pool.ip)
|
|
# An SSH client owns one "chan".
|
|
if not getattr(ssh_client, 'chan', None):
|
|
setattr(ssh_client, 'chan',
|
|
utils.create_channel(ssh_client, 600, 800))
|
|
|
|
while True:
|
|
ssh_client.chan.send(cmd + '\n')
|
|
out = ssh_read(user, ssh_client.chan, cmd, 20)
|
|
if out.find('(y/n)') > -1:
|
|
cmd = 'y'
|
|
else:
|
|
# Put SSH client back into SSH pool.
|
|
self.ssh_pool.put(ssh_client)
|
|
return out
|
|
|
|
except Exception as err:
|
|
if connect_times < 2:
|
|
connect_times += 1
|
|
continue
|
|
else:
|
|
if ssh_client:
|
|
self.ssh_pool.remove(ssh_client)
|
|
LOG.error(_('_execute_cli: %s') % err)
|
|
raise err
|
|
|
|
def _reset_transport_timeout(self, ssh, time):
|
|
transport = ssh.get_transport()
|
|
transport.sock.settimeout(time)
|
|
|
|
def delete_volume(self, volume):
|
|
volume_name = self._name_translate(volume['name'])
|
|
|
|
LOG.debug(_('delete_volume: volume name: %s') % volume_name)
|
|
|
|
self._update_login_info()
|
|
volume_id = volume.get('provider_location', None)
|
|
if (volume_id is not None) and self._check_volume_created(volume_id):
|
|
self._delete_volume(volume_id)
|
|
else:
|
|
err_msg = (_('delete_volume: Volume %(name)s does not exist.')
|
|
% {'name': volume['name']})
|
|
LOG.warn(err_msg)
|
|
|
|
def _check_volume_created(self, volume_id):
|
|
cli_cmd = 'showlun -lun %s' % volume_id
|
|
out = self._execute_cli(cli_cmd)
|
|
return (True if re.search('LUN Information', out) else False)
|
|
|
|
def _delete_volume(self, volumeid):
|
|
"""Run CLI command to delete volume."""
|
|
cli_cmd = 'dellun -force -lun %s' % volumeid
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_volume',
|
|
('Failed to delete volume. volume id: %s'
|
|
% volumeid),
|
|
cli_cmd, out)
|
|
|
|
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._name_translate(snapshot['name'])
|
|
volume_name = self._name_translate(volume['name'])
|
|
|
|
LOG.debug(_('create_volume_from_snapshot: snapshot '
|
|
'name: %(snapshot)s, volume name: %(volume)s')
|
|
% {'snapshot': snapshot_name,
|
|
'volume': volume_name})
|
|
|
|
self._update_login_info()
|
|
snapshot_id = snapshot.get('provider_location', None)
|
|
if not snapshot_id:
|
|
snapshot_id = self._get_snapshot_id(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)
|
|
|
|
# Create a target LUN.
|
|
if int(volume['size']) == 0:
|
|
volume_size = '%sG' % snapshot['volume_size']
|
|
else:
|
|
volume_size = '%sG' % volume['size']
|
|
type_id = volume['volume_type_id']
|
|
parameters = self._parse_volume_type(type_id)
|
|
tgt_vol_id = self._create_volume(volume_name, volume_size, parameters)
|
|
self._copy_volume(snapshot_id, tgt_vol_id)
|
|
|
|
return tgt_vol_id
|
|
|
|
def _copy_volume(self, src_vol_id, tgt_vol_id):
|
|
"""Copy a volume or snapshot to target volume."""
|
|
luncopy_name = VOL_AND_SNAP_NAME_PREFIX + src_vol_id + '_' + tgt_vol_id
|
|
self._create_luncopy(luncopy_name, src_vol_id, tgt_vol_id)
|
|
self.luncopy_list.append(luncopy_name)
|
|
luncopy_id = self._get_luncopy_info(luncopy_name)[1]
|
|
try:
|
|
self._start_luncopy(luncopy_id)
|
|
self._wait_for_luncopy(luncopy_name)
|
|
# Delete the target volume if LUNcopy failed.
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
# Need to remove the LUNcopy of the volume first.
|
|
self._delete_luncopy(luncopy_id)
|
|
self.luncopy_list.remove(luncopy_name)
|
|
self._delete_volume(tgt_vol_id)
|
|
# Need to delete LUNcopy finally.
|
|
self._delete_luncopy(luncopy_id)
|
|
self.luncopy_list.remove(luncopy_name)
|
|
|
|
def _create_luncopy(self, luncopyname, srclunid, tgtlunid):
|
|
"""Run CLI command to create LUNcopy."""
|
|
cli_cmd = ('createluncopy -n %(name)s -l 4 -slun %(srclunid)s '
|
|
'-tlun %(tgtlunid)s' % {'name': luncopyname,
|
|
'srclunid': srclunid,
|
|
'tgtlunid': tgtlunid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_luncopy',
|
|
('Failed to create LUNcopy %s'
|
|
% luncopyname),
|
|
cli_cmd, out)
|
|
|
|
def _start_luncopy(self, luncopyid):
|
|
"""Run CLI command to start LUNcopy."""
|
|
cli_cmd = ('chgluncopystatus -luncopy %(luncopyid)s -start'
|
|
% {'luncopyid': luncopyid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_start_luncopy',
|
|
'Failed to start LUNcopy %s' % luncopyid,
|
|
cli_cmd, out)
|
|
|
|
def _wait_for_luncopy(self, luncopyname):
|
|
"""Wait for LUNcopy to complete."""
|
|
while True:
|
|
luncopy_info = self._get_luncopy_info(luncopyname)
|
|
# If state is complete
|
|
if luncopy_info[3] == 'Complete':
|
|
break
|
|
# If status is not normal
|
|
elif luncopy_info[4] != 'Normal':
|
|
err_msg = (_('_wait_for_luncopy: LUNcopy %(luncopyname)s '
|
|
'status is %(status)s.')
|
|
% {'luncopyname': luncopyname,
|
|
'status': luncopy_info[4]})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
time.sleep(10)
|
|
|
|
def _get_luncopy_info(self, luncopyname):
|
|
"""Return a LUNcopy information list."""
|
|
cli_cmd = 'showluncopy'
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_out(re.search('LUN Copy Information', out),
|
|
'_get_luncopy_info',
|
|
'No LUNcopy information was found.',
|
|
cli_cmd, out)
|
|
|
|
for line in out.split('\r\n')[6:-2]:
|
|
tmp_line = line.split()
|
|
if tmp_line[0] == luncopyname:
|
|
return tmp_line
|
|
return None
|
|
|
|
def _delete_luncopy(self, luncopyid):
|
|
"""Run CLI command to delete LUNcopy."""
|
|
cli_cmd = 'delluncopy -luncopy %(id)s' % {'id': luncopyid}
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_luncopy',
|
|
'Failed to delete LUNcopy %s' % luncopyid,
|
|
cli_cmd, out)
|
|
|
|
def create_cloned_volume(self, tgt_volume, src_volume):
|
|
src_vol_name = self._name_translate(src_volume['name'])
|
|
tgt_vol_name = self._name_translate(tgt_volume['name'])
|
|
|
|
LOG.debug(_('create_cloned_volume: src volume: %(src)s, '
|
|
'tgt volume: %(tgt)s') % {'src': src_vol_name,
|
|
'tgt': tgt_vol_name})
|
|
|
|
self._update_login_info()
|
|
src_vol_id = src_volume.get('provider_location', None)
|
|
if not src_vol_id:
|
|
src_vol_id = self._get_lun_id(src_vol_name)
|
|
if src_vol_id is None:
|
|
err_msg = (_('Source volume %(name)s does not exist.')
|
|
% {'name': src_vol_name})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeNotFound(volume_id=src_vol_name)
|
|
|
|
# Create a target volume.
|
|
if int(tgt_volume['size']) == 0:
|
|
tgt_vol_size = '%sG' % src_vol_name['size']
|
|
else:
|
|
tgt_vol_size = '%sG' % tgt_volume['size']
|
|
type_id = tgt_volume['volume_type_id']
|
|
params = self._parse_volume_type(type_id)
|
|
tgt_vol_id = self._create_volume(tgt_vol_name, tgt_vol_size, params)
|
|
self._copy_volume(src_vol_id, tgt_vol_id)
|
|
|
|
return tgt_vol_id
|
|
|
|
def _get_all_luns_info(self):
|
|
cli_cmd = 'showlun'
|
|
out = self._execute_cli(cli_cmd)
|
|
luns = []
|
|
if re.search('LUN Information', out):
|
|
for line in out.split('\r\n')[6:-2]:
|
|
luns.append(line.replace('Not format', 'Notformat').split())
|
|
return luns
|
|
|
|
def _get_lun_id(self, lun_name):
|
|
luns = self._get_all_luns_info()
|
|
if luns:
|
|
for lun in luns:
|
|
if lun[6] == lun_name:
|
|
return lun[0]
|
|
return None
|
|
|
|
def create_snapshot(self, snapshot):
|
|
snapshot_name = self._name_translate(snapshot['name'])
|
|
volume_name = self._name_translate(snapshot['volume_name'])
|
|
|
|
LOG.debug(_('create_snapshot: snapshot name: %(snapshot)s, '
|
|
'volume name: %(volume)s')
|
|
% {'snapshot': snapshot_name,
|
|
'volume': volume_name})
|
|
|
|
if self._resource_pool_enough() is False:
|
|
err_msg = (_('create_snapshot: '
|
|
'Resource pool needs 1GB valid size at least.'))
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
lun_id = self._get_lun_id(volume_name)
|
|
if lun_id is None:
|
|
err_msg = (_('create_snapshot: Volume %(name)s does not exist.')
|
|
% {'name': volume_name})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeNotFound(volume_id=volume_name)
|
|
|
|
self._create_snapshot(snapshot_name, lun_id)
|
|
snapshot_id = self._get_snapshot_id(snapshot_name)
|
|
try:
|
|
self._active_snapshot(snapshot_id)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
self._delete_snapshot(snapshot_id)
|
|
|
|
return snapshot_id
|
|
|
|
def _resource_pool_enough(self):
|
|
"""Check whether resource pools' valid size is more than 1GB."""
|
|
cli_cmd = 'showrespool'
|
|
out = self._execute_cli(cli_cmd)
|
|
for line in out.split('\r\n')[6:-2]:
|
|
tmp_line = line.split()
|
|
if float(tmp_line[3]) < 1024.0:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _create_snapshot(self, snapshotname, srclunid):
|
|
"""Create a snapshot with snapshot name and source LUN ID."""
|
|
cli_cmd = ('createsnapshot -lun %(lunid)s -n %(snapname)s'
|
|
% {'lunid': srclunid,
|
|
'snapname': snapshotname})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_snapshot',
|
|
('Failed to create snapshot %s'
|
|
% snapshotname),
|
|
cli_cmd, out)
|
|
|
|
def _get_snapshot_id(self, snapshotname):
|
|
cli_cmd = 'showsnapshot'
|
|
out = self._execute_cli(cli_cmd)
|
|
if re.search('Snapshot Information', out):
|
|
for line in out.split('\r\n')[6:-2]:
|
|
emp_line = line.split()
|
|
if emp_line[0] == snapshotname:
|
|
return emp_line[1]
|
|
return None
|
|
|
|
def _active_snapshot(self, snapshotid):
|
|
"""Run CLI command to active snapshot."""
|
|
cli_cmd = ('actvsnapshot -snapshot %(snapshotid)s'
|
|
% {'snapshotid': snapshotid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_active_snapshot',
|
|
('Failed to active snapshot %s'
|
|
% snapshotid),
|
|
cli_cmd, out)
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
snapshot_name = self._name_translate(snapshot['name'])
|
|
volume_name = self._name_translate(snapshot['volume_name'])
|
|
|
|
LOG.debug(_('delete_snapshot: snapshot name: %(snapshot)s, '
|
|
'volume name: %(volume)s') % {'snapshot': snapshot_name,
|
|
'volume': volume_name})
|
|
|
|
self._update_login_info()
|
|
snapshot_id = snapshot.get('provider_location', None)
|
|
if ((snapshot_id is not None) and
|
|
self._check_snapshot_created(snapshot_id)):
|
|
# Not allow to delete snapshot if it is copying.
|
|
if self._snapshot_in_luncopy(snapshot_id):
|
|
err_msg = (_('delete_snapshot: Can not delete snapshot %s '
|
|
'for it is a source LUN of LUNCopy.')
|
|
% snapshot_name)
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
self._delete_snapshot(snapshot_id)
|
|
else:
|
|
err_msg = (_('delete_snapshot: Snapshot %(snap)s does not exist.')
|
|
% {'snap': snapshot_name})
|
|
LOG.warn(err_msg)
|
|
|
|
def _check_snapshot_created(self, snapshot_id):
|
|
cli_cmd = 'showsnapshot -snapshot %(snap)s' % {'snap': snapshot_id}
|
|
out = self._execute_cli(cli_cmd)
|
|
return (True if re.search('Snapshot Information', out) else False)
|
|
|
|
def _snapshot_in_luncopy(self, snapshot_id):
|
|
for name in self.luncopy_list:
|
|
if name.startswith(VOL_AND_SNAP_NAME_PREFIX + snapshot_id):
|
|
return True
|
|
return False
|
|
|
|
def _delete_snapshot(self, snapshotid):
|
|
"""Send CLI command to delete snapshot.
|
|
|
|
Firstly, disable the snapshot, then delete it.
|
|
|
|
"""
|
|
|
|
cli_cmd = ('disablesnapshot -snapshot %(snapshotid)s'
|
|
% {'snapshotid': snapshotid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_snapshot',
|
|
('Failed to disable snapshot %s'
|
|
% snapshotid),
|
|
cli_cmd, out)
|
|
|
|
cli_cmd = ('delsnapshot -snapshot %(snapshotid)s'
|
|
% {'snapshotid': snapshotid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_snapshot',
|
|
('Failed to delete snapshot %s'
|
|
% snapshotid),
|
|
cli_cmd, out)
|
|
|
|
def _assert_cli_out(self, condition, func, msg, cmd, cliout):
|
|
"""Assertion for CLI query out."""
|
|
if not condition:
|
|
err_msg = (_('%(func)s: %(msg)s\nCLI command: %(cmd)s\n'
|
|
'CLI out: %(out)s') % {'func': func,
|
|
'msg': msg,
|
|
'cmd': cmd,
|
|
'out': cliout})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
def _assert_cli_operate_out(self, func, msg, cmd, cliout):
|
|
"""Assertion for CLI out string: command operates successfully."""
|
|
condition = re.search('command operates successfully', cliout)
|
|
self._assert_cli_out(condition, func, msg, cmd, cliout)
|
|
|
|
def map_volume(self, host_id, volume_id):
|
|
"""Map a volume to a host."""
|
|
# Map a LUN to a host if not mapped.
|
|
if not self._check_volume_created(volume_id):
|
|
LOG.error(_('map_volume: Volume %s was not found.') % volume_id)
|
|
raise exception.VolumeNotFound(volume_id=volume_id)
|
|
|
|
hostlun_id = None
|
|
map_info = self._get_host_map_info(host_id)
|
|
# Make sure the host LUN ID starts from 1.
|
|
new_hostlun_id = 1
|
|
new_hostlunid_found = False
|
|
if map_info:
|
|
for maping in map_info:
|
|
if maping[2] == volume_id:
|
|
hostlun_id = maping[4]
|
|
break
|
|
elif not new_hostlunid_found:
|
|
if new_hostlun_id < int(maping[4]):
|
|
new_hostlunid_found = True
|
|
else:
|
|
new_hostlun_id = int(maping[4]) + 1
|
|
|
|
if not hostlun_id:
|
|
cli_cmd = ('addhostmap -host %(host_id)s -devlun %(lunid)s '
|
|
'-hostlun %(hostlunid)s'
|
|
% {'host_id': host_id,
|
|
'lunid': volume_id,
|
|
'hostlunid': new_hostlun_id})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
msg = ('Failed to map LUN %s to host %s. host LUN ID: %s'
|
|
% (volume_id, host_id, new_hostlun_id))
|
|
self._assert_cli_operate_out('map_volume', msg, cli_cmd, out)
|
|
|
|
hostlun_id = new_hostlun_id
|
|
|
|
return hostlun_id
|
|
|
|
def add_host(self, host_name, host_ip, initiator=None):
|
|
"""Create a host and add it to hostgroup."""
|
|
# Create an OpenStack hostgroup if not created before.
|
|
hostgroup_name = HOST_GROUP_NAME
|
|
self.hostgroup_id = self._get_hostgroup_id(hostgroup_name)
|
|
if self.hostgroup_id is None:
|
|
self._create_hostgroup(hostgroup_name)
|
|
self.hostgroup_id = self._get_hostgroup_id(hostgroup_name)
|
|
|
|
# Create a host and add it to the hostgroup.
|
|
# Check the old host name to support the upgrade from grizzly to
|
|
# higher versions.
|
|
if initiator:
|
|
old_host_name = HOST_NAME_PREFIX + str(hash(initiator))
|
|
old_host_id = self._get_host_id(old_host_name, self.hostgroup_id)
|
|
if old_host_id is not None:
|
|
return old_host_id
|
|
|
|
host_name = HOST_NAME_PREFIX + host_name
|
|
host_id = self._get_host_id(host_name, self.hostgroup_id)
|
|
if host_id is None:
|
|
os_type = huawei_utils.get_conf_host_os_type(host_ip,
|
|
self.xml_conf)
|
|
self._create_host(host_name, self.hostgroup_id, os_type)
|
|
host_id = self._get_host_id(host_name, self.hostgroup_id)
|
|
|
|
return host_id
|
|
|
|
def _get_hostgroup_id(self, groupname):
|
|
"""Get the given hostgroup ID.
|
|
|
|
If the hostgroup not found, return None.
|
|
|
|
"""
|
|
|
|
cli_cmd = 'showhostgroup'
|
|
out = self._execute_cli(cli_cmd)
|
|
if re.search('Host Group Information', out):
|
|
for line in out.split('\r\n')[6:-2]:
|
|
tmp_line = line.split()
|
|
if tmp_line[1] == groupname:
|
|
return tmp_line[0]
|
|
return None
|
|
|
|
def _create_hostgroup(self, hostgroupname):
|
|
"""Run CLI command to create host group."""
|
|
cli_cmd = 'createhostgroup -n %(name)s' % {'name': hostgroupname}
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_hostgroup',
|
|
('Failed to Create hostgroup %s.'
|
|
% hostgroupname),
|
|
cli_cmd, out)
|
|
|
|
def _get_host_id(self, hostname, hostgroupid):
|
|
"""Get the given host ID."""
|
|
cli_cmd = 'showhost -group %(groupid)s' % {'groupid': hostgroupid}
|
|
out = self._execute_cli(cli_cmd)
|
|
if re.search('Host Information', out):
|
|
for line in out.split('\r\n')[6:-2]:
|
|
tmp_line = line.split()
|
|
if tmp_line[1] == hostname:
|
|
return tmp_line[0]
|
|
return None
|
|
|
|
def _create_host(self, hostname, hostgroupid, type):
|
|
"""Run CLI command to add host."""
|
|
cli_cmd = ('addhost -group %(groupid)s -n %(hostname)s -t %(type)s'
|
|
% {'groupid': hostgroupid,
|
|
'hostname': hostname,
|
|
'type': type})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_host',
|
|
'Failed to create host %s' % hostname,
|
|
cli_cmd, out)
|
|
|
|
def _get_host_port_info(self, hostid):
|
|
"""Run CLI command to get host port information."""
|
|
cli_cmd = ('showhostport -host %(hostid)s' % {'hostid': hostid})
|
|
out = self._execute_cli(cli_cmd)
|
|
if re.search('Host Port Information', out):
|
|
return [line.split() for line in out.split('\r\n')[6:-2]]
|
|
else:
|
|
return None
|
|
|
|
def _get_host_map_info(self, hostid):
|
|
"""Get map infomation of the given host."""
|
|
|
|
cli_cmd = 'showhostmap -host %(hostid)s' % {'hostid': hostid}
|
|
out = self._execute_cli(cli_cmd)
|
|
if re.search('Map Information', out):
|
|
mapinfo = [line.split() for line in out.split('\r\n')[6:-2]]
|
|
# Sorted by host LUN ID.
|
|
return sorted(mapinfo, key=lambda x: int(x[4]))
|
|
else:
|
|
return None
|
|
|
|
def get_lun_details(self, lun_id):
|
|
cli_cmd = 'showlun -lun %s' % lun_id
|
|
out = self._execute_cli(cli_cmd)
|
|
lun_details = {}
|
|
if re.search('LUN Information', out):
|
|
for line in out.split('\r\n')[4:-2]:
|
|
line = line.split('|')
|
|
key = ''.join(line[0].strip().split())
|
|
val = line[1].strip()
|
|
lun_details[key] = val
|
|
return lun_details
|
|
|
|
def change_lun_ctr(self, lun_id, ctr):
|
|
LOG.debug(_('change_lun_ctr: Changing LUN %(lun)s ctr to %(ctr)s.')
|
|
% {'lun': lun_id, 'ctr': ctr})
|
|
|
|
cli_cmd = 'chglun -lun %s -c %s' % (lun_id, ctr)
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('change_lun_ctr',
|
|
'Failed to change owning controller for '
|
|
'LUN %s' % lun_id,
|
|
cli_cmd, out)
|
|
|
|
def remove_map(self, volume_id, host_name, initiator=None):
|
|
"""Remove host map."""
|
|
# Check the old host name to support the upgrade from grizzly to
|
|
# higher versions.
|
|
host_id = None
|
|
if initiator:
|
|
old_host_name = HOST_NAME_PREFIX + str(hash(initiator))
|
|
host_id = self._get_host_id(old_host_name, self.hostgroup_id)
|
|
if host_id is None:
|
|
host_name = HOST_NAME_PREFIX + host_name
|
|
host_id = self._get_host_id(host_name, self.hostgroup_id)
|
|
if host_id is None:
|
|
LOG.error(_('remove_map: Host %s does not exist.') % host_name)
|
|
raise exception.HostNotFound(host=host_name)
|
|
|
|
if not self._check_volume_created(volume_id):
|
|
LOG.error(_('remove_map: Volume %s does not exist.') % volume_id)
|
|
raise exception.VolumeNotFound(volume_id=volume_id)
|
|
|
|
map_id = None
|
|
map_info = self._get_host_map_info(host_id)
|
|
if map_info:
|
|
for maping in map_info:
|
|
if maping[2] == volume_id:
|
|
map_id = maping[0]
|
|
break
|
|
if map_id is not None:
|
|
self._delete_map(map_id)
|
|
else:
|
|
LOG.warn(_('remove_map: No map between host %(host)s and '
|
|
'volume %(volume)s.') % {'host': host_name,
|
|
'volume': volume_id})
|
|
return host_id
|
|
|
|
def _delete_map(self, mapid, attempts=2):
|
|
"""Run CLI command to remove map."""
|
|
cli_cmd = 'delhostmap -force -map %(mapid)s' % {'mapid': mapid}
|
|
while True:
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
# We retry to delete host map 10s later if there are
|
|
# IOs accessing the system.
|
|
if re.search('command operates successfully', out):
|
|
break
|
|
else:
|
|
if (re.search('there are IOs accessing the system', out) and
|
|
(attempts > 0)):
|
|
|
|
LOG.debug(_('_delete_map: There are IOs accessing '
|
|
'the system. Retry to delete host map '
|
|
'%(mapid)s 10s later.') % {'mapid': mapid})
|
|
|
|
time.sleep(10)
|
|
attempts -= 1
|
|
continue
|
|
else:
|
|
err_msg = (_('_delete_map: Failed to delete host map '
|
|
'%(mapid)s.\nCLI out: %(out)s')
|
|
% {'mapid': mapid,
|
|
'times': attempts,
|
|
'out': out})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
def _delete_hostport(self, portid):
|
|
"""Run CLI command to delete host port."""
|
|
cli_cmd = ('delhostport -force -p %(portid)s' % {'portid': portid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_hostport',
|
|
'Failed to delete host port %s.' % portid,
|
|
cli_cmd, out)
|
|
|
|
def _delete_host(self, hostid):
|
|
"""Run CLI command to delete host."""
|
|
cli_cmd = ('delhost -force -host %(hostid)s' % {'hostid': hostid})
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_delete_host',
|
|
'Failed to delete host. %s.' % hostid,
|
|
cli_cmd, out)
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Get volume stats.
|
|
|
|
If 'refresh' is True, run update the stats first.
|
|
"""
|
|
if refresh:
|
|
self._update_volume_stats()
|
|
|
|
return self._stats
|
|
|
|
def _update_volume_stats(self):
|
|
"""Retrieve stats info from volume group."""
|
|
|
|
LOG.debug(_("_update_volume_stats: Updating volume stats."))
|
|
data = {}
|
|
data['vendor_name'] = 'Huawei'
|
|
data['total_capacity_gb'] = 'infinite'
|
|
data['free_capacity_gb'] = self._get_free_capacity()
|
|
data['reserved_percentage'] = 0
|
|
data['QoS_support'] = False
|
|
|
|
self._stats = data
|
|
|
|
def _get_free_capacity(self):
|
|
"""Get total free capacity of pools."""
|
|
self._update_login_info()
|
|
params_conf = self._parse_conf_lun_params()
|
|
lun_type = params_conf['LUNType']
|
|
pools_conf = params_conf['StoragePool']
|
|
pools_dev = self._get_dev_pool_info(lun_type)
|
|
total_free_capacity = 0.0
|
|
for pool_dev in pools_dev:
|
|
for pool_conf in pools_conf:
|
|
if ((lun_type == 'Thick') and
|
|
(pool_dev[5] == pool_conf)):
|
|
total_free_capacity += float(pool_dev[3])
|
|
break
|
|
elif pool_dev[1] == pool_conf:
|
|
total_free_capacity += float(pool_dev[4])
|
|
break
|
|
|
|
return total_free_capacity / 1024
|
|
|
|
def _get_dev_pool_info(self, pooltype):
|
|
"""Get pools information created in storage device.
|
|
|
|
Return a list whose elements are also list.
|
|
|
|
"""
|
|
|
|
cli_cmd = ('showpool' if pooltype == 'Thin' else 'showrg')
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
test = (re.search('Pool Information', out) or
|
|
re.search('RAID Group Information', out))
|
|
self._assert_cli_out(test, '_get_dev_pool_info',
|
|
'No pools information found.', cli_cmd, out)
|
|
|
|
pool = out.split('\r\n')[6:-2]
|
|
return [line.split() for line in pool]
|
|
|
|
|
|
class DoradoCommon(TseriesCommon):
|
|
"""Common class for Huawei Dorado2100 G2 and Dorado5100 storage arrays.
|
|
|
|
Dorados share a lot of common codes with T series storage systems,
|
|
so this class inherited from class TseriesCommon and just rewrite some
|
|
methods.
|
|
|
|
"""
|
|
|
|
def __init__(self, configuration=None):
|
|
TseriesCommon.__init__(self, configuration)
|
|
self.device_type = None
|
|
|
|
def do_setup(self, context):
|
|
"""Check config file."""
|
|
LOG.debug(_('do_setup'))
|
|
|
|
self._check_conf_file()
|
|
self.lun_distribution = self._get_lun_ctr_info()
|
|
self.hostgroup_id = self._get_hostgroup_id(HOST_GROUP_NAME)
|
|
|
|
def _check_conf_file(self):
|
|
"""Check the config file, make sure the key elements are set."""
|
|
root = huawei_utils.parse_xml_file(self.xml_conf)
|
|
# Check login infomation
|
|
check_list = ['Storage/ControllerIP0', 'Storage/ControllerIP1',
|
|
'Storage/UserName', 'Storage/UserPassword']
|
|
for item in check_list:
|
|
if not huawei_utils.is_xml_item_exist(root, item):
|
|
err_msg = (_('_check_conf_file: Config file invalid. '
|
|
'%s must be set.') % item)
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
# Check storage pool
|
|
# No need for Dorado2100 G2
|
|
self.login_info = self._get_login_info()
|
|
self.device_type = self._get_device_type()
|
|
if self.device_type == 'Dorado5100':
|
|
if not huawei_utils.is_xml_item_exist(root, 'LUN/StoragePool',
|
|
'Name'):
|
|
err_msg = (_('_check_conf_file: Config file invalid. '
|
|
'StoragePool must be specified.'))
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
# If setting os type, make sure it valid
|
|
if huawei_utils.is_xml_item_exist(root, 'Host', 'OSType'):
|
|
os_list = huawei_utils.os_type.keys()
|
|
if not huawei_utils.is_xml_item_valid(root, 'Host', os_list,
|
|
'OSType'):
|
|
err_msg = (_('_check_conf_file: Config file invalid. '
|
|
'Host OSType is invalid.\n'
|
|
'The valid values are: %(os_list)s')
|
|
% {'os_list': os_list})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
def _get_device_type(self):
|
|
"""Run CLI command to get system type."""
|
|
cli_cmd = 'showsys'
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_out(re.search('System Information', out),
|
|
'_get_device_type',
|
|
'Failed to get system information',
|
|
cli_cmd, out)
|
|
|
|
for line in out.split('\r\n')[4:-2]:
|
|
if re.search('Device Type', line):
|
|
if re.search('Dorado2100 G2$', line):
|
|
return 'Dorado2100 G2'
|
|
elif re.search('Dorado5100$', line):
|
|
return 'Dorado5100'
|
|
else:
|
|
LOG.error(_('_get_device_type: The driver only supports '
|
|
'Dorado5100 and Dorado 2100 G2 now.'))
|
|
raise exception.InvalidResults()
|
|
|
|
def _get_lun_ctr_info(self):
|
|
luns = self._get_all_luns_info()
|
|
ctr_info = [0, 0]
|
|
(c, n) = ((2, 4) if self.device_type == 'Dorado2100 G2' else (3, 5))
|
|
for lun in luns:
|
|
if lun[n].startswith(VOL_AND_SNAP_NAME_PREFIX):
|
|
if lun[c] == 'A':
|
|
ctr_info[0] += 1
|
|
else:
|
|
ctr_info[1] += 1
|
|
return ctr_info
|
|
|
|
def _create_volume(self, name, size, params):
|
|
"""Create a new volume with the given name and size."""
|
|
cli_cmd = ('createlun -n %(name)s -lunsize %(size)s '
|
|
'-wrtype %(wrtype)s '
|
|
% {'name': name,
|
|
'size': size,
|
|
'wrtype': params['WriteType']})
|
|
|
|
# If write type is "write through", no need to set mirror switch.
|
|
if params['WriteType'] != '2':
|
|
cli_cmd = cli_cmd + ('-mirrorsw %(mirrorsw)s '
|
|
% {'mirrorsw': params['MirrorSwitch']})
|
|
|
|
ctr = self._calculate_lun_ctr()
|
|
# Dorado5100 does not support thin LUN.
|
|
if self.device_type == 'Dorado5100':
|
|
cli_cmd = cli_cmd + ('-rg %(raidgroup)s -susize %(susize)s '
|
|
'-c %(ctr)s'
|
|
% {'raidgroup': params['StoragePool'],
|
|
'susize': params['StripUnitSize'],
|
|
'ctr': ctr})
|
|
else:
|
|
if params['LUNType'] == 'Thin':
|
|
# Not allowed to specify ctr for thin LUN.
|
|
ctr_str = ''
|
|
luntype_str = '-type 2'
|
|
else:
|
|
ctr_str = ' -c %s' % ctr
|
|
luntype_str = '-type 3'
|
|
|
|
cli_cmd = cli_cmd + luntype_str + ctr_str
|
|
|
|
out = self._execute_cli(cli_cmd)
|
|
|
|
self._assert_cli_operate_out('_create_volume',
|
|
'Failed to create volume %s' % name,
|
|
cli_cmd, out)
|
|
|
|
self._update_lun_distribution(ctr)
|
|
|
|
return self._get_lun_id(name)
|
|
|
|
def _get_lun_id(self, name):
|
|
luns = self._get_all_luns_info()
|
|
if luns:
|
|
n_index = (4 if 'Dorado2100 G2' == self.device_type else 5)
|
|
for lun in luns:
|
|
if lun[n_index] == name:
|
|
return lun[0]
|
|
return None
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
err_msg = (_('create_volume_from_snapshot: %(device)s does '
|
|
'not support create volume from snapshot.')
|
|
% {'device': self.device_type})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
err_msg = (_('create_cloned_volume: %(device)s does '
|
|
'not support clone volume.')
|
|
% {'device': self.device_type})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
|
|
def create_snapshot(self, snapshot):
|
|
if self.device_type == 'Dorado2100 G2':
|
|
err_msg = (_('create_snapshot: %(device)s does not support '
|
|
'snapshot.') % {'device': self.device_type})
|
|
LOG.error(err_msg)
|
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
|
else:
|
|
return TseriesCommon.create_snapshot(self, snapshot)
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
if self.device_type == 'Dorado2100 G2':
|
|
return
|
|
else:
|
|
TseriesCommon.delete_snapshot(self, snapshot)
|
|
|
|
def _get_lun_params(self):
|
|
params_conf = self._parse_conf_lun_params()
|
|
# Select a pool with maximum capacity.
|
|
if self.device_type == 'Dorado5100':
|
|
pools_dev = self._get_dev_pool_info('Thick')
|
|
params_conf['StoragePool'] = \
|
|
self._get_maximum_capacity_pool_id(params_conf['StoragePool'],
|
|
pools_dev, 'Thick')
|
|
return params_conf
|
|
|
|
def _parse_conf_lun_params(self):
|
|
"""Get parameters from config file for creating LUN."""
|
|
# Default LUN parameters.
|
|
conf_params = {'LUNType': 'Thin',
|
|
'StripUnitSize': '64',
|
|
'WriteType': '1',
|
|
'MirrorSwitch': '1'}
|
|
|
|
root = huawei_utils.parse_xml_file(self.xml_conf)
|
|
|
|
luntype = root.findtext('LUN/LUNType')
|
|
if luntype:
|
|
if luntype.strip() in ['Thick', 'Thin']:
|
|
conf_params['LUNType'] = luntype.strip()
|
|
else:
|
|
err_msg = (_('LUNType must be "Thin" or "Thick". '
|
|
'LUNType:%(type)s') % {'type': luntype})
|
|
LOG.error(err_msg)
|
|
raise exception.InvalidInput(reason=err_msg)
|
|
|
|
# Here we do not judge whether the parameters are set correct.
|
|
# CLI will return error responses if the parameters are invalid.
|
|
stripunitsize = root.findtext('LUN/StripUnitSize')
|
|
if stripunitsize:
|
|
conf_params['StripUnitSize'] = stripunitsize.strip()
|
|
writetype = root.findtext('LUN/WriteType')
|
|
if writetype:
|
|
conf_params['WriteType'] = writetype.strip()
|
|
mirrorswitch = root.findtext('LUN/MirrorSwitch')
|
|
if mirrorswitch:
|
|
conf_params['MirrorSwitch'] = mirrorswitch.strip()
|
|
|
|
# No need to set StoragePool for Dorado2100 G2.
|
|
if self.device_type == 'Dorado2100 G2':
|
|
return conf_params
|
|
|
|
pools_conf = root.findall('LUN/StoragePool')
|
|
conf_params['StoragePool'] = []
|
|
for pool in pools_conf:
|
|
conf_params['StoragePool'].append(pool.attrib['Name'].strip())
|
|
|
|
return conf_params
|
|
|
|
def _get_free_capacity(self):
|
|
"""Get total free capacity of pools."""
|
|
self._update_login_info()
|
|
lun_type = ('Thin' if self.device_type == 'Dorado2100 G2' else 'Thick')
|
|
pools_dev = self._get_dev_pool_info(lun_type)
|
|
total_free_capacity = 0.0
|
|
for pool_dev in pools_dev:
|
|
if self.device_type == 'Dorado2100 G2':
|
|
total_free_capacity += float(pool_dev[2])
|
|
continue
|
|
else:
|
|
params_conf = self._parse_conf_lun_params()
|
|
pools_conf = params_conf['StoragePool']
|
|
for pool_conf in pools_conf:
|
|
if pool_dev[5] == pool_conf:
|
|
total_free_capacity += float(pool_dev[3])
|
|
break
|
|
|
|
return total_free_capacity / 1024
|