cinder/cinder/volume/drivers/huawei/ssh_common.py

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