# 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)|y or 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 self.extended_lun_dict = {} def do_setup(self, context): """Check config file.""" LOG.debug('do_setup') self._check_conf_file() self.login_info = self._get_login_info() exist_luns = self._get_all_luns_info() self.lun_distribution = self._get_lun_distribution_info(exist_luns) self.luncopy_list = self._get_all_luncopy_name() self.hostgroup_id = self._get_hostgroup_id(HOST_GROUP_NAME) self.extended_lun_dict = self._get_extended_lun(exist_luns) 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, luns): """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. """ 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 _get_extended_lun(self, luns): extended_dict = {} for lun in luns: if lun[6].startswith('ext'): vol_name = lun[6].split('_')[1] add_ids = extended_dict.get(vol_name, []) add_ids = add_ids.append(lun[0]) extended_dict[vol_name] = add_ids return extended_dict 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 multiple. 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. """ 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 or out.find('y or 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 None or not self._check_volume_created(volume_id): err_msg = (_('delete_volume: Volume %(name)s does not exist.') % {'name': volume['name']}) LOG.warn(err_msg) return else: name = volume_name[len(VOL_AND_SNAP_NAME_PREFIX):] added_vol_ids = self.extended_lun_dict.get(name, None) if added_vol_ids: self._del_lun_from_extended_lun(volume_id, added_vol_ids) self.extended_lun_dict.pop(name) self._delete_volume(volume_id) 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 _del_lun_from_extended_lun(self, extended_id, added_ids): cli_cmd = 'rmlunfromextlun -ext %s' % extended_id out = self._execute_cli(cli_cmd) self._assert_cli_operate_out('_del_lun_from_extended_lun', ('Failed to remove LUN from extended ' 'LUN: %s' % extended_id), cli_cmd, out) for id in added_ids: cli_cmd = 'dellun -lun %s' % id out = self._execute_cli(cli_cmd) self._assert_cli_operate_out('_del_lun_from_extended_lun', 'Failed to delete LUN: %s' % id, cli_cmd, out) 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 extend_volume(self, volume, new_size): extended_vol_name = self._name_translate(volume['name']) name = extended_vol_name[len(VOL_AND_SNAP_NAME_PREFIX):] added_vol_ids = self.extended_lun_dict.get(name, []) added_vol_name = ('ext_' + extended_vol_name.split('_')[1] + '_' + str(len(added_vol_ids))) added_vol_size = str(int(new_size) - int(volume['size'])) + 'G' LOG.debug('extend_volume: extended volume name: %(extended_name)s ' 'new added volume name: %(added_name)s ' 'new added volume size: %(added_size)s' % {'extended_name': extended_vol_name, 'added_name': added_vol_name, 'added_size': added_vol_size}) if not volume['provider_location']: err_msg = (_('extend_volume: volume %s does not exist.') % extended_vol_name) LOG.error(err_msg) raise exception.VolumeNotFound(volume_id=extended_vol_name) type_id = volume['volume_type_id'] parameters = self._parse_volume_type(type_id) added_vol_id = self._create_volume(added_vol_name, added_vol_size, parameters) try: self._extend_volume(volume['provider_location'], added_vol_id) except Exception: with excutils.save_and_reraise_exception(): self._delete_volume(added_vol_id) added_vol_ids.append(added_vol_id) self.extended_lun_dict[name] = added_vol_ids def _extend_volume(self, extended_vol_id, added_vol_id): cli_cmd = ('addluntoextlun -extlun %(extended_vol)s ' '-lun %(added_vol)s' % {'extended_vol': extended_vol_id, 'added_vol': added_vol_id}) out = self._execute_cli(cli_cmd) self._assert_cli_operate_out('_extend_volume', ('Failed to extend volume %s' % extended_vol_id), cli_cmd, out) 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 information 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() exist_luns = self._get_all_luns_info() self.lun_distribution = self._get_lun_distribution_info(exist_luns) self.hostgroup_id = self._get_hostgroup_id(HOST_GROUP_NAME) self.extended_lun_dict = self._get_extended_lun(exist_luns) 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 information 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_distribution_info(self, luns): 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 _get_extended_lun(self, luns): extended_dict = {} n = 4 if self.device_type == 'Dorado2100 G2' else 5 for lun in luns: if lun[n].startswith('ext'): vol_name = lun[n].split('_')[1] add_ids = extended_dict.get(vol_name, []) add_ids.append(lun[0]) extended_dict[vol_name] = add_ids return extended_dict 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 extend_volume(self, volume, new_size): if self.device_type == 'Dorado2100 G2': err_msg = (_('extend_volume: %(device)s does not support ' 'extend volume.') % {'device': self.device_type}) LOG.error(err_msg) raise exception.VolumeBackendAPIException(data=err_msg) else: return TseriesCommon.extend_volume(self, volume, new_size) 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