From e7362103c67579bf7caf1437afc3d4518923c8a6 Mon Sep 17 00:00:00 2001 From: wang yong Date: Sat, 7 Oct 2017 22:06:45 +0800 Subject: [PATCH] Inspur Cinder iSCSI driver Features that Inspur Driver support: Create, list, delete, attach (map), and detach (unmap) volumes Create, list, and delete volume snapshots Copy an image to a volume Copy a volume to an image Clone a volume Extend a volume Retype a volume Create a volume from a snapshot Manage an existing volume Consistency group create,update,delete Consistency group snapshot create,delete Group create,update,delete Group snapshot create,delete Replication V2.1 ThirdPartySystems: INSPUR CI Implements: Blueprint inspur-instorage-driver Change-Id: I06a8eb38f35ccff125282c8886458bfe99fe196e --- cinder/opts.py | 8 + .../unit/volume/drivers/inspur/__init__.py | 0 .../drivers/inspur/instorage/__init__.py | 0 .../volume/drivers/inspur/instorage/fakes.py | 2194 ++++++++++ .../drivers/inspur/instorage/test_common.py | 1775 ++++++++ .../inspur/instorage/test_helper_routines.py | 256 ++ .../inspur/instorage/test_iscsi_driver.py | 430 ++ .../inspur/instorage/test_replication.py | 1002 +++++ cinder/volume/drivers/inspur/__init__.py | 0 .../drivers/inspur/instorage/__init__.py | 0 .../inspur/instorage/instorage_common.py | 3629 +++++++++++++++++ .../inspur/instorage/instorage_const.py | 40 + .../inspur/instorage/instorage_iscsi.py | 298 ++ .../drivers/inspur/instorage/replication.py | 240 ++ ...pur-instorage-driver-40371862c9559238.yaml | 5 + 15 files changed, 9877 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/inspur/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/__init__.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/test_helper_routines.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/test_iscsi_driver.py create mode 100644 cinder/tests/unit/volume/drivers/inspur/instorage/test_replication.py create mode 100644 cinder/volume/drivers/inspur/__init__.py create mode 100644 cinder/volume/drivers/inspur/instorage/__init__.py create mode 100644 cinder/volume/drivers/inspur/instorage/instorage_common.py create mode 100644 cinder/volume/drivers/inspur/instorage/instorage_const.py create mode 100644 cinder/volume/drivers/inspur/instorage/instorage_iscsi.py create mode 100644 cinder/volume/drivers/inspur/instorage/replication.py create mode 100644 releasenotes/notes/bp-inspur-instorage-driver-40371862c9559238.yaml diff --git a/cinder/opts.py b/cinder/opts.py index 2e98f1ec0ea..20990687b09 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -123,6 +123,10 @@ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc as \ from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi as \ cinder_volume_drivers_ibm_storwize_svc_storwizesvciscsi from cinder.volume.drivers import infinidat as cinder_volume_drivers_infinidat +from cinder.volume.drivers.inspur.instorage import instorage_common as \ + cinder_volume_drivers_inspur_instorage_instoragecommon +from cinder.volume.drivers.inspur.instorage import instorage_iscsi as \ + cinder_volume_drivers_inspur_instorage_instorageiscsi from cinder.volume.drivers.kaminario import kaminario_common as \ cinder_volume_drivers_kaminario_kaminariocommon from cinder.volume.drivers.lenovo import lenovo_common as \ @@ -241,6 +245,10 @@ def list_opts(): [cinder_volume_api.az_cache_time_opt], cinder_volume_driver.volume_opts, cinder_volume_driver.iser_opts, + cinder_volume_drivers_inspur_instorage_instoragecommon. + instorage_mcs_opts, + cinder_volume_drivers_inspur_instorage_instorageiscsi. + instorage_mcs_iscsi_opts, cinder_volume_manager.volume_manager_opts, cinder_wsgi_eventletserver.socket_opts, )), diff --git a/cinder/tests/unit/volume/drivers/inspur/__init__.py b/cinder/tests/unit/volume/drivers/inspur/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/__init__.py b/cinder/tests/unit/volume/drivers/inspur/instorage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py b/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py new file mode 100644 index 00000000000..67560ffc287 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/fakes.py @@ -0,0 +1,2194 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +import re + +from oslo_concurrency import processutils +from oslo_utils import units +import six + +from cinder import exception +from cinder import utils + +from cinder.volume.drivers.inspur.instorage import instorage_const +from cinder.volume.drivers.inspur.instorage import instorage_iscsi + +MCS_POOLS = ['openstack', 'openstack1'] + + +def get_test_pool(get_all=False): + if get_all: + return MCS_POOLS + else: + return MCS_POOLS[0] + + +class FakeInStorageMCSISCSIDriver(instorage_iscsi.InStorageMCSISCSIDriver): + + def __init__(self, *args, **kwargs): + super(FakeInStorageMCSISCSIDriver, self).__init__(*args, **kwargs) + + def set_fake_storage(self, fake): + self.fake_storage = fake + + def _run_ssh(self, cmd, check_exit_code=True, attempts=1): + utils.check_ssh_injection(cmd) + ret = self.fake_storage.execute_command(cmd, check_exit_code) + + return ret + + +class FakeInStorage(object): + + def __init__(self, pool_name): + self._flags = {'instorage_mcs_volpool_name': pool_name} + self._volumes_list = {} + self._hosts_list = {} + self._mappings_list = {} + self._lcmappings_list = {} + self._lcconsistgrp_list = {} + self._rcrelationship_list = {} + self._partnership_list = {} + self._partnershipcandidate_list = {} + self._system_list = {'instorage-mcs-sim': + {'id': '0123456789ABCDEF', + 'name': 'instorage-mcs-sim'}, + 'aux-mcs-sim': {'id': 'ABCDEF0123456789', + 'name': 'aux-mcs-sim'}} + self._other_pools = {'openstack2': {}, 'openstack3': {}} + self._next_cmd_error = { + 'lsportip': '', + 'lsfabric': '', + 'lsiscsiauth': '', + 'lsnodecanister': '', + 'mkvdisk': '', + 'lsvdisk': '', + 'lslcmap': '', + 'prestartlcmap': '', + 'startlcmap': '', + 'rmlcmap': '', + 'lslicense': '', + 'lsguicapabilities': '', + 'lshost': '', + 'lsrcrelationship': '' + } + self._errors = { + 'CMMVC5701E': ('', 'CMMVC5701E No object ID was specified.'), + 'CMMVC6035E': ('', 'CMMVC6035E The action failed as the ' + 'object already exists.'), + 'CMMVC5753E': ('', 'CMMVC5753E The specified object does not ' + 'exist or is not a suitable candidate.'), + 'CMMVC5707E': ('', 'CMMVC5707E Required parameters are missing.'), + 'CMMVC6581E': ('', 'CMMVC6581E The command has failed because ' + 'the maximum number of allowed iSCSI ' + 'qualified names (IQNs) has been reached, ' + 'or the IQN is already assigned or is not ' + 'valid.'), + 'CMMVC5754E': ('', 'CMMVC5754E The specified object does not ' + 'exist, or the name supplied does not meet ' + 'the naming rules.'), + 'CMMVC6071E': ('', 'CMMVC6071E The VDisk-to-host mapping was ' + 'not created because the VDisk is already ' + 'mapped to a host.'), + 'CMMVC5879E': ('', 'CMMVC5879E The VDisk-to-host mapping was ' + 'not created because a VDisk is already ' + 'mapped to this host with this SCSI LUN.'), + 'CMMVC5840E': ('', 'CMMVC5840E The virtual disk (VDisk) was ' + 'not deleted because it is mapped to a ' + 'host or because it is part of a LocalCopy ' + 'or Remote Copy mapping, or is involved in ' + 'an image mode migrate.'), + 'CMMVC6527E': ('', 'CMMVC6527E The name that you have entered ' + 'is not valid. The name can contain letters, ' + 'numbers, spaces, periods, dashes, and ' + 'underscores. The name must begin with a ' + 'letter or an underscore. The name must not ' + 'begin or end with a space.'), + 'CMMVC5871E': ('', 'CMMVC5871E The action failed because one or ' + 'more of the configured port names is in a ' + 'mapping.'), + 'CMMVC5924E': ('', 'CMMVC5924E The LocalCopy mapping was not ' + 'created because the source and target ' + 'virtual disks (VDisks) are different sizes.'), + 'CMMVC6303E': ('', 'CMMVC6303E The create failed because the ' + 'source and target VDisks are the same.'), + 'CMMVC7050E': ('', 'CMMVC7050E The command failed because at ' + 'least one node in the I/O group does not ' + 'support compressed VDisks.'), + 'CMMVC6430E': ('', 'CMMVC6430E The command failed because the ' + 'target and source managed disk groups must ' + 'be different.'), + 'CMMVC6353E': ('', 'CMMVC6353E The command failed because the ' + 'copy specified does not exist.'), + 'CMMVC6446E': ('', 'The command failed because the managed disk ' + 'groups have different extent sizes.'), + # Catch-all for invalid state transitions: + 'CMMVC5903E': ('', 'CMMVC5903E The LocalCopy mapping was not ' + 'changed because the mapping or consistency ' + 'group is another state.'), + 'CMMVC5709E': ('', 'CMMVC5709E [-%(VALUE)s] is not a supported ' + 'parameter.'), + 'CMMVC5982E': ('', 'CMMVC5982E The operation was not performed ' + 'because it is not valid given the current ' + 'relationship state.'), + 'CMMVC5963E': ('', 'CMMVC5963E No direction has been defined.'), + + } + self._lc_transitions = {'begin': {'make': 'idle_or_copied'}, + 'idle_or_copied': {'prepare': 'preparing', + 'delete': 'end', + 'delete_force': 'end'}, + 'preparing': {'flush_failed': 'stopped', + 'wait': 'prepared'}, + 'end': None, + 'stopped': {'prepare': 'preparing', + 'delete_force': 'end'}, + 'prepared': {'stop': 'stopped', + 'start': 'copying'}, + 'copying': {'wait': 'idle_or_copied', + 'stop': 'stopping'}, + # Assume the worst case where stopping->stopped + # rather than stopping idle_or_copied + 'stopping': {'wait': 'stopped'}, + } + + self._lc_cg_transitions = {'begin': {'make': 'empty'}, + 'empty': {'add': 'idle_or_copied'}, + 'idle_or_copied': {'prepare': 'preparing', + 'delete': 'end', + 'delete_force': 'end'}, + 'preparing': {'flush_failed': 'stopped', + 'wait': 'prepared'}, + 'end': None, + 'stopped': {'prepare': 'preparing', + 'delete_force': 'end'}, + 'prepared': {'stop': 'stopped', + 'start': 'copying', + 'delete_force': 'end', + 'delete': 'end'}, + 'copying': {'wait': 'idle_or_copied', + 'stop': 'stopping', + 'delete_force': 'end', + 'delete': 'end'}, + # Assume the case where stopping->stopped + # rather than stopping idle_or_copied + 'stopping': {'wait': 'stopped'}, + } + self._rc_transitions = {'inconsistent_stopped': + {'start': 'inconsistent_copying', + 'stop': 'inconsistent_stopped', + 'delete': 'end', + 'delete_force': 'end'}, + 'inconsistent_copying': { + 'wait': 'consistent_synchronized', + 'start': 'inconsistent_copying', + 'stop': 'inconsistent_stopped', + 'delete': 'end', + 'delete_force': 'end'}, + 'consistent_synchronized': { + 'start': 'consistent_synchronized', + 'stop': 'consistent_stopped', + 'stop_access': 'idling', + 'delete': 'end', + 'delete_force': 'end'}, + 'consistent_stopped': + {'start': 'consistent_synchronized', + 'stop': 'consistent_stopped', + 'delete': 'end', + 'delete_force': 'end'}, + 'end': None, + 'idling': { + 'start': 'inconsistent_copying', + 'stop': 'inconsistent_stopped', + 'stop_access': 'idling', + 'delete': 'end', + 'delete_force': 'end'}, + } + + def _state_transition(self, function, lcmap): + if (function == 'wait' and + 'wait' not in self._lc_transitions[lcmap['status']]): + return ('', '') + + if lcmap['status'] == 'copying' and function == 'wait': + if lcmap['copyrate'] != '0': + if lcmap['progress'] == '0': + lcmap['progress'] = '50' + else: + lcmap['progress'] = '100' + lcmap['status'] = 'idle_or_copied' + return ('', '') + else: + try: + curr_state = lcmap['status'] + lcmap['status'] = self._lc_transitions[curr_state][function] + return ('', '') + except Exception: + return self._errors['CMMVC5903E'] + + def _lc_cg_state_transition(self, function, lc_consistgrp): + if (function == 'wait' and + 'wait' not in self._lc_transitions[lc_consistgrp['status']]): + return ('', '') + + try: + curr_state = lc_consistgrp['status'] + new_state = self._lc_cg_transitions[curr_state][function] + lc_consistgrp['status'] = new_state + return ('', '') + except Exception: + return self._errors['CMMVC5903E'] + + # Find an unused ID + @staticmethod + def _find_unused_id(d): + ids = [] + for v in d.values(): + ids.append(int(v['id'])) + ids.sort() + for index, n in enumerate(ids): + if n > index: + return six.text_type(index) + return six.text_type(len(ids)) + + # Check if name is valid + @staticmethod + def _is_invalid_name(name): + if re.match(r'^[a-zA-Z_][\w._-]*$', name): + return False + return True + + # Convert argument string to dictionary + @staticmethod + def _cmd_to_dict(arg_list): + no_param_args = [ + 'autodelete', + 'bytes', + 'compressed', + 'force', + 'nohdr', + 'nofmtdisk', + 'async', + 'access', + 'start' + ] + one_param_args = [ + 'chapsecret', + 'cleanrate', + 'copy', + 'copyrate', + 'delim', + 'intier', + 'filtervalue', + 'grainsize', + 'hbawwpn', + 'host', + 'iogrp', + 'iscsiname', + 'mdiskgrp', + 'name', + 'rsize', + 'scsi', + 'size', + 'source', + 'target', + 'unit', + 'vdisk', + 'warning', + 'wwpn', + 'primary', + 'consistgrp', + 'master', + 'aux', + 'cluster', + 'linkbandwidthmbits', + 'backgroundcopyrate' + ] + no_or_one_param_args = [ + 'autoexpand', + ] + + # Handle the special case of lsnode which is a two-word command + # Use the one word version of the command internally + if arg_list[0] in ('mcsinq', 'mcsop'): + if arg_list[1] == 'lsnode': + if len(arg_list) > 4: # e.g. mcsinq lsnode -delim ! + ret = {'cmd': 'lsnode', 'node_id': arg_list[-1]} + else: + ret = {'cmd': 'lsnodecanister'} + else: + ret = {'cmd': arg_list[1]} + arg_list.pop(0) + else: + ret = {'cmd': arg_list[0]} + + skip = False + for i in range(1, len(arg_list)): + if skip: + skip = False + continue + # Check for a quoted command argument for volumes and strip + # quotes so that the simulater can match it later. Just + # match against test naming convensions for now. + if arg_list[i][0] == '"' and ('volume' in arg_list[i] or + 'snapshot' in arg_list[i]): + arg_list[i] = arg_list[i][1:-1] + if arg_list[i][0] == '-': + if arg_list[i][1:] in no_param_args: + ret[arg_list[i][1:]] = True + elif arg_list[i][1:] in one_param_args: + ret[arg_list[i][1:]] = arg_list[i + 1] + skip = True + elif arg_list[i][1:] in no_or_one_param_args: + if i == (len(arg_list) - 1) or arg_list[i + 1][0] == '-': + ret[arg_list[i][1:]] = True + else: + ret[arg_list[i][1:]] = arg_list[i + 1] + skip = True + else: + raise exception.InvalidInput( + reason='unrecognized argument %s' % arg_list[i]) + else: + ret['obj'] = arg_list[i] + return ret + + @staticmethod + def _print_info_cmd(rows, delim=' ', nohdr=False, **kwargs): + """Generic function for printing information.""" + if nohdr: + del rows[0] + + for index in range(len(rows)): + rows[index] = delim.join(rows[index]) + return ('%s' % '\n'.join(rows), '') + + @staticmethod + def _print_info_obj_cmd(header, row, delim=' ', nohdr=False): + """Generic function for printing information for a specific object.""" + objrows = [] + for idx, val in enumerate(header): + objrows.append([val, row[idx]]) + + if nohdr: + for index in range(len(objrows)): + objrows[index] = ' '.join(objrows[index][1:]) + for index in range(len(objrows)): + objrows[index] = delim.join(objrows[index]) + return ('%s' % '\n'.join(objrows), '') + + @staticmethod + def _convert_bytes_units(bytestr): + num = int(bytestr) + unit_array = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + unit_index = 0 + + while num > 1024: + num = num / 1024 + unit_index += 1 + + return '%d%s' % (num, unit_array[unit_index]) + + @staticmethod + def _convert_units_bytes(num, unit): + unit_array = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + unit_index = 0 + + while unit.lower() != unit_array[unit_index].lower(): + num = num * 1024 + unit_index += 1 + + return six.text_type(num) + + def _cmd_lslicense(self, **kwargs): + rows = [None] * 3 + rows[0] = ['used_compression_capacity', '0.08'] + rows[1] = ['license_compression_capacity', '0'] + if self._next_cmd_error['lslicense'] == 'no_compression': + self._next_cmd_error['lslicense'] = '' + rows[2] = ['license_compression_enclosures', '0'] + else: + rows[2] = ['license_compression_enclosures', '1'] + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lsguicapabilities(self, **kwargs): + rows = [None] * 2 + if self._next_cmd_error['lsguicapabilities'] == 'no_compression': + self._next_cmd_error['lsguicapabilities'] = '' + rows[0] = ['license_scheme', '0'] + else: + rows[0] = ['license_scheme', '1813'] + rows[1] = ['product_key', instorage_const.DEV_MODEL_INSTORAGE] + return self._print_info_cmd(rows=rows, **kwargs) + + # Print mostly made-up stuff in the correct syntax + def _cmd_lssystem(self, **kwargs): + rows = [None] * 3 + rows[0] = ['id', '0123456789ABCDEF'] + rows[1] = ['name', 'instorage-mcs-sim'] + rows[2] = ['code_level', '3.1.1.0 (build 87.0.1311291000)'] + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lssystem_aux(self, **kwargs): + rows = [None] * 3 + rows[0] = ['id', 'ABCDEF0123456789'] + rows[1] = ['name', 'aux-mcs-sim'] + rows[2] = ['code_level', '3.1.1.0 (build 87.0.1311291000)'] + return self._print_info_cmd(rows=rows, **kwargs) + + # Print mostly made-up stuff in the correct syntax, assume -bytes passed + def _cmd_lsmdiskgrp(self, **kwargs): + pool_num = len(self._flags['instorage_mcs_volpool_name']) + rows = [] + rows.append(['id', 'name', 'status', 'mdisk_count', + 'vdisk_count', 'capacity', 'extent_size', + 'free_capacity', 'virtual_capacity', 'used_capacity', + 'real_capacity', 'overallocation', 'warning', + 'in_tier', 'in_tier_status']) + for i in range(pool_num): + row_data = [str(i + 1), + self._flags['instorage_mcs_volpool_name'][i], 'online', + '1', six.text_type(len(self._volumes_list)), + '3573412790272', '256', '3529926246400', + '1693247906775', + '26843545600', '38203734097', '47', '80', 'auto', + 'inactive'] + rows.append(row_data) + rows.append([str(pool_num + 1), 'openstack2', 'online', + '1', '0', '3573412790272', '256', + '3529432325160', '1693247906775', '26843545600', + '38203734097', '47', '80', 'auto', 'inactive']) + rows.append([str(pool_num + 2), 'openstack3', 'online', + '1', '0', '3573412790272', '128', + '3529432325160', '1693247906775', '26843545600', + '38203734097', '47', '80', 'auto', 'inactive']) + if 'obj' not in kwargs: + return self._print_info_cmd(rows=rows, **kwargs) + else: + pool_name = kwargs['obj'].strip('\'\"') + if pool_name == kwargs['obj']: + raise exception.InvalidInput( + reason='obj missing quotes %s' % kwargs['obj']) + elif pool_name in self._flags['instorage_mcs_volpool_name']: + for each_row in rows: + if pool_name in each_row: + row = each_row + break + elif pool_name == 'openstack2': + row = rows[-2] + elif pool_name == 'openstack3': + row = rows[-1] + else: + return self._errors['CMMVC5754E'] + objrows = [] + for idx, val in enumerate(rows[0]): + objrows.append([val, row[idx]]) + if 'nohdr' in kwargs: + for index in range(len(objrows)): + objrows[index] = ' '.join(objrows[index][1:]) + + if 'delim' in kwargs: + for index in range(len(objrows)): + objrows[index] = kwargs['delim'].join(objrows[index]) + + return ('%s' % '\n'.join(objrows), '') + + # Print mostly made-up stuff in the correct syntax + def _cmd_lsnodecanister(self, **kwargs): + rows = [None] * 3 + rows[0] = ['id', 'name', 'UPS_serial_number', 'WWNN', 'status', + 'IO_group_id', 'IO_group_name', 'config_node', + 'UPS_unique_id', 'hardware', 'iscsi_name', 'iscsi_alias', + 'panel_name', 'enclosure_id', 'canister_id', + 'enclosure_serial_number'] + rows[1] = [ + '1', + 'node1', + '', + '123456789ABCDEF0', + 'online', + '0', + 'io_grp0', + 'yes', + '123456789ABCDEF0', + '100', + 'iqn.1982-01.com.inspur:1234.sim.node1', + '', + '01-1', + '1', + '1', + '0123ABC'] + rows[2] = [ + '2', + 'node2', + '', + '123456789ABCDEF1', + 'online', + '0', + 'io_grp0', + 'no', + '123456789ABCDEF1', + '100', + 'iqn.1982-01.com.inspur:1234.sim.node2', + '', + '01-2', + '1', + '2', + '0123ABC'] + + if self._next_cmd_error['lsnodecanister'] == 'header_mismatch': + rows[0].pop(2) + self._next_cmd_error['lsnodecanister'] = '' + if self._next_cmd_error['lsnodecanister'] == 'remove_field': + for row in rows: + row.pop(0) + self._next_cmd_error['lsnodecanister'] = '' + + return self._print_info_cmd(rows=rows, **kwargs) + + # Print information of every single node of MCS + def _cmd_lsnode(self, **kwargs): + node_infos = dict() + node_infos['1'] = r'''id!1 +name!node1 +port_id!500507680210C744 +port_status!active +port_speed!8Gb +port_id!500507680220C744 +port_status!active +port_speed!8Gb +''' + node_infos['2'] = r'''id!2 +name!node2 +port_id!500507680220C745 +port_status!active +port_speed!8Gb +port_id!500507680230C745 +port_status!inactive +port_speed!N/A +''' + node_id = kwargs.get('node_id', None) + stdout = node_infos.get(node_id, '') + return stdout, '' + + # Print made up stuff for the ports + def _cmd_lsportfc(self, **kwargs): + node_1 = [None] * 7 + node_1[0] = ['id', 'fc_io_port_id', 'port_id', 'type', + 'port_speed', 'node_id', 'node_name', 'WWPN', + 'nportid', 'status', 'attachment'] + node_1[1] = ['0', '1', '1', 'fc', '8Gb', '1', 'node1', + '5005076802132ADE', '012E00', 'active', 'switch'] + node_1[2] = ['1', '2', '2', 'fc', '8Gb', '1', 'node1', + '5005076802232ADE', '012E00', 'active', 'switch'] + node_1[3] = ['2', '3', '3', 'fc', '8Gb', '1', 'node1', + '5005076802332ADE', '9B0600', 'active', 'switch'] + node_1[4] = ['3', '4', '4', 'fc', '8Gb', '1', 'node1', + '5005076802432ADE', '012A00', 'active', 'switch'] + node_1[5] = ['4', '5', '5', 'fc', '8Gb', '1', 'node1', + '5005076802532ADE', '014A00', 'active', 'switch'] + node_1[6] = ['5', '6', '4', 'ethernet', 'N/A', '1', 'node1', + '5005076802632ADE', '000000', + 'inactive_unconfigured', 'none'] + + node_2 = [None] * 7 + node_2[0] = ['id', 'fc_io_port_id', 'port_id', 'type', + 'port_speed', 'node_id', 'node_name', 'WWPN', + 'nportid', 'status', 'attachment'] + node_2[1] = ['6', '7', '7', 'fc', '8Gb', '2', 'node2', + '5005086802132ADE', '012E00', 'active', 'switch'] + node_2[2] = ['7', '8', '8', 'fc', '8Gb', '2', 'node2', + '5005086802232ADE', '012E00', 'active', 'switch'] + node_2[3] = ['8', '9', '9', 'fc', '8Gb', '2', 'node2', + '5005086802332ADE', '9B0600', 'active', 'switch'] + node_2[4] = ['9', '10', '10', 'fc', '8Gb', '2', 'node2', + '5005086802432ADE', '012A00', 'active', 'switch'] + node_2[5] = ['10', '11', '11', 'fc', '8Gb', '2', 'node2', + '5005086802532ADE', '014A00', 'active', 'switch'] + node_2[6] = ['11', '12', '12', 'ethernet', 'N/A', '2', 'node2', + '5005086802632ADE', '000000', + 'inactive_unconfigured', 'none'] + node_infos = [node_1, node_2] + node_id = int(kwargs['filtervalue'].split('=')[1]) - 1 + + return self._print_info_cmd(rows=node_infos[node_id], **kwargs) + + # Print mostly made-up stuff in the correct syntax + def _cmd_lsportip(self, **kwargs): + if self._next_cmd_error['lsportip'] == 'ip_no_config': + self._next_cmd_error['lsportip'] = '' + ip_addr1 = '' + ip_addr2 = '' + gw = '' + else: + ip_addr1 = '1.234.56.78' + ip_addr2 = '1.234.56.79' + ip_addr3 = '1.234.56.80' + ip_addr4 = '1.234.56.81' + gw = '1.234.56.1' + + rows = [None] * 17 + rows[0] = ['id', 'node_id', 'node_name', 'IP_address', 'mask', + 'gateway', 'IP_address_6', 'prefix_6', 'gateway_6', 'MAC', + 'duplex', 'state', 'speed', 'failover', 'link_state'] + rows[1] = ['1', '1', 'node1', ip_addr1, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:00', 'Full', + 'online', '1Gb/s', 'no', 'active'] + rows[2] = ['1', '1', 'node1', '', '', '', '', '', '', + '01:23:45:67:89:00', 'Full', 'online', '1Gb/s', 'yes', ''] + rows[3] = ['2', '1', 'node1', ip_addr3, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:01', 'Full', + 'configured', '1Gb/s', 'no', 'active'] + rows[4] = ['2', '1', 'node1', '', '', '', '', '', '', + '01:23:45:67:89:01', 'Full', 'unconfigured', '1Gb/s', + 'yes', 'inactive'] + rows[5] = ['3', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no', ''] + rows[6] = ['3', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes', ''] + rows[7] = ['4', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no', ''] + rows[8] = ['4', '1', 'node1', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes', ''] + rows[9] = ['1', '2', 'node2', ip_addr2, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:02', 'Full', + 'online', '1Gb/s', 'no', ''] + rows[10] = ['1', '2', 'node2', '', '', '', '', '', '', + '01:23:45:67:89:02', 'Full', 'online', '1Gb/s', 'yes', ''] + rows[11] = ['2', '2', 'node2', ip_addr4, '255.255.255.0', + gw, '', '', '', '01:23:45:67:89:03', 'Full', + 'configured', '1Gb/s', 'no', 'inactive'] + rows[12] = ['2', '2', 'node2', '', '', '', '', '', '', + '01:23:45:67:89:03', 'Full', 'unconfigured', '1Gb/s', + 'yes', ''] + rows[13] = ['3', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no', ''] + rows[14] = ['3', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes', ''] + rows[15] = ['4', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'no', ''] + rows[16] = ['4', '2', 'node2', '', '', '', '', '', '', '', '', + 'unconfigured', '', 'yes', ''] + + if self._next_cmd_error['lsportip'] == 'header_mismatch': + rows[0].pop(2) + self._next_cmd_error['lsportip'] = '' + if self._next_cmd_error['lsportip'] == 'remove_field': + for row in rows: + row.pop(1) + self._next_cmd_error['lsportip'] = '' + + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lsfabric(self, **kwargs): + if self._next_cmd_error['lsfabric'] == 'no_hosts': + return ('', '') + host_name = kwargs['host'].strip('\'\"') if 'host' in kwargs else None + target_wwpn = kwargs['wwpn'] if 'wwpn' in kwargs else None + host_infos = [] + for hv in self._hosts_list.values(): + if (not host_name) or (hv['host_name'] == host_name): + if not target_wwpn or target_wwpn in hv['wwpns']: + host_infos.append(hv) + break + if not len(host_infos): + return ('', '') + rows = [] + rows.append(['remote_wwpn', 'remote_nportid', 'id', 'node_name', + 'local_wwpn', 'local_port', 'local_nportid', 'state', + 'name', 'cluster_name', 'type']) + for host_info in host_infos: + for wwpn in host_info['wwpns']: + rows.append([wwpn, '123456', host_info['id'], 'nodeN', + 'AABBCCDDEEFF0011', '1', '0123ABC', 'active', + host_info['host_name'], '', 'host']) + if self._next_cmd_error['lsfabric'] == 'header_mismatch': + rows[0].pop(0) + self._next_cmd_error['lsfabric'] = '' + if self._next_cmd_error['lsfabric'] == 'remove_field': + for row in rows: + row.pop(0) + self._next_cmd_error['lsfabric'] = '' + if self._next_cmd_error['lsfabric'] == 'remove_rows': + rows = [] + return self._print_info_cmd(rows=rows, **kwargs) + + def _get_lcmap_info(self, vol_name): + ret_vals = { + 'fc_id': '', + 'fc_name': '', + 'lc_map_count': '0', + } + for lcmap in self._lcmappings_list.values(): + if ((lcmap['source'] == vol_name) or + (lcmap['target'] == vol_name)): + ret_vals['fc_id'] = lcmap['id'] + ret_vals['fc_name'] = lcmap['name'] + ret_vals['lc_map_count'] = '1' + return ret_vals + + # List information about vdisks + def _cmd_lsvdisk(self, **kwargs): + rows = [] + rows.append(['id', 'name', 'IO_group_id', 'IO_group_name', + 'status', 'mdisk_grp_id', 'mdisk_grp_name', + 'capacity', 'type', 'FC_id', 'FC_name', 'RC_id', + 'RC_name', 'vdisk_UID', 'lc_map_count', 'copy_count', + 'fast_write_state', 'se_copy_count', 'RC_change']) + + for vol in self._volumes_list.values(): + if (('filtervalue' not in kwargs) or + (kwargs['filtervalue'] == 'name=' + vol['name']) or + (kwargs['filtervalue'] == 'vdisk_UID=' + vol['uid'])): + lcmap_info = self._get_lcmap_info(vol['name']) + + if 'bytes' in kwargs: + cap = self._convert_bytes_units(vol['capacity']) + else: + cap = vol['capacity'] + rows.append([six.text_type(vol['id']), vol['name'], + vol['IO_group_id'], + vol['IO_group_name'], 'online', '0', + get_test_pool(), + cap, 'striped', + lcmap_info['fc_id'], lcmap_info['fc_name'], + '', '', vol['uid'], + lcmap_info['lc_map_count'], '1', 'empty', + '1', 'no']) + if 'obj' not in kwargs: + return self._print_info_cmd(rows=rows, **kwargs) + else: + if kwargs['obj'] not in self._volumes_list: + return self._errors['CMMVC5754E'] + vol = self._volumes_list[kwargs['obj']] + lcmap_info = self._get_lcmap_info(vol['name']) + cap = vol['capacity'] + cap_u = vol['used_capacity'] + cap_r = vol['real_capacity'] + cap_f = vol['free_capacity'] + if 'bytes' not in kwargs: + for item in [cap, cap_u, cap_r, cap_f]: + item = self._convert_bytes_units(item) + rows = [] + + rows.append(['id', six.text_type(vol['id'])]) + rows.append(['name', vol['name']]) + rows.append(['IO_group_id', vol['IO_group_id']]) + rows.append(['IO_group_name', vol['IO_group_name']]) + rows.append(['status', 'online']) + rows.append(['capacity', cap]) + rows.append(['formatted', vol['formatted']]) + rows.append(['mdisk_id', '']) + rows.append(['mdisk_name', '']) + rows.append(['FC_id', lcmap_info['fc_id']]) + rows.append(['FC_name', lcmap_info['fc_name']]) + rows.append(['RC_id', vol['RC_id']]) + rows.append(['RC_name', vol['RC_name']]) + rows.append(['vdisk_UID', vol['uid']]) + rows.append(['throttling', '0']) + + if self._next_cmd_error['lsvdisk'] == 'blank_pref_node': + rows.append(['preferred_node_id', '']) + self._next_cmd_error['lsvdisk'] = '' + elif self._next_cmd_error['lsvdisk'] == 'no_pref_node': + self._next_cmd_error['lsvdisk'] = '' + else: + rows.append(['preferred_node_id', '1']) + rows.append(['fast_write_state', 'empty']) + rows.append(['cache', 'readwrite']) + rows.append(['udid', '']) + rows.append(['lc_map_count', lcmap_info['lc_map_count']]) + rows.append(['sync_rate', '50']) + rows.append(['copy_count', '1']) + rows.append(['se_copy_count', '0']) + rows.append(['mirror_write_priority', 'latency']) + rows.append(['RC_change', 'no']) + + for copy in vol['copies'].values(): + rows.append(['copy_id', copy['id']]) + rows.append(['status', copy['status']]) + rows.append(['primary', copy['primary']]) + rows.append(['mdisk_grp_id', copy['mdisk_grp_id']]) + rows.append(['mdisk_grp_name', copy['mdisk_grp_name']]) + rows.append(['type', 'striped']) + rows.append(['used_capacity', cap_u]) + rows.append(['real_capacity', cap_r]) + rows.append(['free_capacity', cap_f]) + rows.append(['in_tier', copy['in_tier']]) + rows.append(['compressed_copy', copy['compressed_copy']]) + rows.append(['autoexpand', vol['autoexpand']]) + rows.append(['warning', vol['warning']]) + rows.append(['grainsize', vol['grainsize']]) + + if 'nohdr' in kwargs: + for index in range(len(rows)): + rows[index] = ' '.join(rows[index][1:]) + + if 'delim' in kwargs: + for index in range(len(rows)): + rows[index] = kwargs['delim'].join(rows[index]) + return ('%s' % '\n'.join(rows), '') + + def _cmd_lsiogrp(self, **kwargs): + rows = [None] * 6 + rows[0] = ['id', 'name', 'node_count', 'vdisk_count', 'host_count'] + rows[1] = ['0', 'io_grp0', '2', '0', '4'] + rows[2] = ['1', 'io_grp1', '2', '0', '4'] + rows[3] = ['2', 'io_grp2', '0', '0', '4'] + rows[4] = ['3', 'io_grp3', '0', '0', '4'] + rows[5] = ['4', 'recovery_io_grp', '0', '0', '0'] + return self._print_info_cmd(rows=rows, **kwargs) + + # List information about hosts + def _cmd_lshost(self, **kwargs): + if 'obj' not in kwargs: + rows = [] + rows.append(['id', 'name', 'port_count', 'iogrp_count', 'status']) + + found = False + # Sort hosts by names to give predictable order for tests + # depend on it. + for host_name in sorted(self._hosts_list.keys()): + host = self._hosts_list[host_name] + filterstr = 'name=' + host['host_name'] + if (('filtervalue' not in kwargs) or + (kwargs['filtervalue'] == filterstr)): + rows.append([host['id'], host['host_name'], '1', '4', + 'offline']) + found = True + if found: + return self._print_info_cmd(rows=rows, **kwargs) + else: + return ('', '') + else: + if self._next_cmd_error['lshost'] == 'missing_host': + self._next_cmd_error['lshost'] = '' + return self._errors['CMMVC5754E'] + elif self._next_cmd_error['lshost'] == 'bigger_troubles': + return self._errors['CMMVC6527E'] + host_name = kwargs['obj'].strip('\'\"') + if host_name not in self._hosts_list: + return self._errors['CMMVC5754E'] + if (self._next_cmd_error['lshost'] == 'fail_fastpath' and + host_name == 'DifferentHost'): + return self._errors['CMMVC5701E'] + host = self._hosts_list[host_name] + rows = [] + rows.append(['id', host['id']]) + rows.append(['name', host['host_name']]) + rows.append(['port_count', '1']) + rows.append(['type', 'generic']) + rows.append(['mask', '1111']) + rows.append(['iogrp_count', '4']) + rows.append(['status', 'online']) + for port in host['iscsi_names']: + rows.append(['iscsi_name', port]) + rows.append(['node_logged_in_count', '0']) + rows.append(['state', 'offline']) + for port in host['wwpns']: + rows.append(['WWPN', port]) + rows.append(['node_logged_in_count', '0']) + rows.append(['state', 'active']) + + if 'nohdr' in kwargs: + for index in range(len(rows)): + rows[index] = ' '.join(rows[index][1:]) + + if 'delim' in kwargs: + for index in range(len(rows)): + rows[index] = kwargs['delim'].join(rows[index]) + + return ('%s' % '\n'.join(rows), '') + + # List iSCSI authorization information about hosts + def _cmd_lsiscsiauth(self, **kwargs): + if self._next_cmd_error['lsiscsiauth'] == 'no_info': + self._next_cmd_error['lsiscsiauth'] = '' + return ('', '') + rows = [] + rows.append(['type', 'id', 'name', 'iscsi_auth_method', + 'iscsi_chap_secret']) + + for host in self._hosts_list.values(): + method = 'none' + secret = '' + if 'chapsecret' in host: + method = 'chap' + secret = host['chapsecret'] + rows.append(['host', host['id'], host['host_name'], method, + secret]) + return self._print_info_cmd(rows=rows, **kwargs) + + # List information about host->vdisk mappings + def _cmd_lshostvdiskmap(self, **kwargs): + host_name = kwargs['obj'].strip('\'\"') + + if host_name not in self._hosts_list: + return self._errors['CMMVC5754E'] + + rows = [] + rows.append(['id', 'name', 'SCSI_id', 'vdisk_id', 'vdisk_name', + 'vdisk_UID']) + + for mapping in self._mappings_list.values(): + if (host_name == '') or (mapping['host'] == host_name): + volume = self._volumes_list[mapping['vol']] + rows.append([mapping['id'], mapping['host'], + mapping['lun'], volume['id'], + volume['name'], volume['uid']]) + + return self._print_info_cmd(rows=rows, **kwargs) + + # List information about vdisk->host mappings + def _cmd_lsvdiskhostmap(self, **kwargs): + mappings_found = 0 + vdisk_name = kwargs['obj'].strip('\'\"') + + if vdisk_name not in self._volumes_list: + return self._errors['CMMVC5753E'] + + rows = [] + rows.append(['id name', 'SCSI_id', 'host_id', 'host_name', 'vdisk_UID', + 'IO_group_id', 'IO_group_name']) + + for mapping in self._mappings_list.values(): + if (mapping['vol'] == vdisk_name): + mappings_found += 1 + volume = self._volumes_list[mapping['vol']] + host = self._hosts_list[mapping['host']] + rows.append([volume['id'], mapping['lun'], host['id'], + host['host_name'], volume['uid'], + volume['IO_group_id'], volume['IO_group_name']]) + + if mappings_found: + return self._print_info_cmd(rows=rows, **kwargs) + else: + return ('', '') + + def _cmd_lsvdisklcmappings(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + vdisk = kwargs['obj'] + rows = [] + rows.append(['id', 'name']) + for v in self._lcmappings_list.values(): + if v['source'] == vdisk or v['target'] == vdisk: + rows.append([v['id'], v['name']]) + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lslcmap(self, **kwargs): + rows = [] + rows.append(['id', 'name', 'source_vdisk_id', 'source_vdisk_name', + 'target_vdisk_id', 'target_vdisk_name', 'group_id', + 'group_name', 'status', 'progress', 'copy_rate', + 'clean_progress', 'incremental', 'partner_FC_id', + 'partner_FC_name', 'restoring', 'start_time', + 'rc_controlled']) + + # Assume we always get a filtervalue argument + filter_key = kwargs['filtervalue'].split('=')[0] + filter_value = kwargs['filtervalue'].split('=')[1] + to_delete = [] + for k, v in self._lcmappings_list.items(): + if six.text_type(v[filter_key]) == filter_value: + source = self._volumes_list[v['source']] + target = self._volumes_list[v['target']] + self._state_transition('wait', v) + + if self._next_cmd_error['lslcmap'] == 'speed_up': + self._next_cmd_error['lslcmap'] = '' + curr_state = v['status'] + while self._state_transition('wait', v) == ("", ""): + if curr_state == v['status']: + break + curr_state = v['status'] + + if ((v['status'] == 'idle_or_copied' and v['autodelete'] and + v['progress'] == '100') or (v['status'] == 'end')): + to_delete.append(k) + else: + rows.append([v['id'], v['name'], source['id'], + source['name'], target['id'], target['name'], + '', '', v['status'], v['progress'], + v['copyrate'], '100', 'off', '', '', 'no', '', + 'no']) + + for d in to_delete: + del self._lcmappings_list[d] + + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lslcconsistgrp(self, **kwargs): + rows = [] + + if 'obj' not in kwargs: + rows.append(['id', 'name', 'status' 'start_time']) + + for lcconsistgrp in self._lcconsistgrp_list.values(): + rows.append([lcconsistgrp['id'], + lcconsistgrp['name'], + lcconsistgrp['status'], + lcconsistgrp['start_time']]) + return self._print_info_cmd(rows=rows, **kwargs) + else: + lcconsistgrp = None + cg_id = 0 + for cg_id in self._lcconsistgrp_list.keys(): + if self._lcconsistgrp_list[cg_id]['name'] == kwargs['obj']: + lcconsistgrp = self._lcconsistgrp_list[cg_id] + rows = [] + rows.append(['id', six.text_type(cg_id)]) + rows.append(['name', lcconsistgrp['name']]) + rows.append(['status', lcconsistgrp['status']]) + rows.append(['autodelete', + six.text_type(lcconsistgrp['autodelete'])]) + rows.append(['start_time', + six.text_type(lcconsistgrp['start_time'])]) + + for lcmap_id in lcconsistgrp['lcmaps'].keys(): + rows.append(['FC_mapping_id', six.text_type(lcmap_id)]) + rows.append(['FC_mapping_name', + lcconsistgrp['lcmaps'][lcmap_id]]) + + if 'delim' in kwargs: + for index in range(len(rows)): + rows[index] = kwargs['delim'].join(rows[index]) + self._lc_cg_state_transition('wait', lcconsistgrp) + return ('%s' % '\n'.join(rows), '') + + def _cmd_lsvdiskcopy(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5804E'] + name = kwargs['obj'] + vol = self._volumes_list[name] + rows = [] + rows.append(['vdisk_id', 'vdisk_name', 'copy_id', 'status', 'sync', + 'primary', 'mdisk_grp_id', 'mdisk_grp_name', 'capacity', + 'type', 'se_copy', 'in_tier', 'in_tier_status', + 'compressed_copy']) + for copy in vol['copies'].values(): + rows.append([vol['id'], vol['name'], copy['id'], + copy['status'], copy['sync'], copy['primary'], + copy['mdisk_grp_id'], copy['mdisk_grp_name'], + vol['capacity'], 'striped', 'yes', copy['in_tier'], + 'inactive', copy['compressed_copy']]) + if 'copy' not in kwargs: + return self._print_info_cmd(rows=rows, **kwargs) + else: + copy_id = kwargs['copy'].strip('\'\"') + if copy_id not in vol['copies']: + return self._errors['CMMVC6353E'] + copy = vol['copies'][copy_id] + rows = [] + rows.append(['vdisk_id', vol['id']]) + rows.append(['vdisk_name', vol['name']]) + rows.append(['capacity', vol['capacity']]) + rows.append(['copy_id', copy['id']]) + rows.append(['status', copy['status']]) + rows.append(['sync', copy['sync']]) + copy['sync'] = 'yes' + rows.append(['primary', copy['primary']]) + rows.append(['mdisk_grp_id', copy['mdisk_grp_id']]) + rows.append(['mdisk_grp_name', copy['mdisk_grp_name']]) + rows.append(['in_tier', copy['in_tier']]) + rows.append(['in_tier_status', 'inactive']) + rows.append(['compressed_copy', copy['compressed_copy']]) + rows.append(['autoexpand', vol['autoexpand']]) + + if 'delim' in kwargs: + for index in range(len(rows)): + rows[index] = kwargs['delim'].join(rows[index]) + + return ('%s' % '\n'.join(rows), '') + + # list vdisk sync process + def _cmd_lsvdisksyncprogress(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5804E'] + name = kwargs['obj'] + copy_id = kwargs.get('copy', None) + vol = self._volumes_list[name] + rows = [] + rows.append(['vdisk_id', 'vdisk_name', 'copy_id', 'progress', + 'estimated_completion_time']) + copy_found = False + for copy in vol['copies'].values(): + if not copy_id or copy_id == copy['id']: + copy_found = True + row = [vol['id'], name, copy['id']] + if copy['sync'] == 'yes': + row.extend(['100', '']) + else: + row.extend(['50', '140210115226']) + copy['sync'] = 'yes' + rows.append(row) + if not copy_found: + return self._errors['CMMVC5804E'] + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lsrcrelationship(self, **kwargs): + rows = [] + rows.append(['id', 'name', 'master_cluster_id', 'master_cluster_name', + 'master_vdisk_id', 'master_vdisk_name', 'aux_cluster_id', + 'aux_cluster_name', 'aux_vdisk_id', 'aux_vdisk_name', + 'consistency_group_id', 'primary', + 'consistency_group_name', 'state', 'bg_copy_priority', + 'progress', 'freeze_time', 'status', 'sync', + 'copy_type', 'cycling_mode', 'cycle_period_seconds', + 'master_change_vdisk_id', 'master_change_vdisk_name', + 'aux_change_vdisk_id', 'aux_change_vdisk_name']) + + # Assume we always get a filtervalue argument + filter_key = kwargs['filtervalue'].split('=')[0] + filter_value = kwargs['filtervalue'].split('=')[1] + for k, v in self._rcrelationship_list.items(): + if six.text_type(v[filter_key]) == filter_value: + self._rc_state_transition('wait', v) + + if self._next_cmd_error['lsrcrelationship'] == 'speed_up': + self._next_cmd_error['lsrcrelationship'] = '' + curr_state = v['status'] + while self._rc_state_transition('wait', v) == ("", ""): + if curr_state == v['status']: + break + curr_state = v['status'] + + rows.append([v['id'], v['name'], v['master_cluster_id'], + v['master_cluster_name'], v['master_vdisk_id'], + v['master_vdisk_name'], v['aux_cluster_id'], + v['aux_cluster_name'], v['aux_vdisk_id'], + v['aux_vdisk_name'], v['consistency_group_id'], + v['primary'], v['consistency_group_name'], + v['state'], v['bg_copy_priority'], v['progress'], + v['freeze_time'], v['status'], v['sync'], + v['copy_type'], v['cycling_mode'], + v['cycle_period_seconds'], + v['master_change_vdisk_id'], + v['master_change_vdisk_name'], + v['aux_change_vdisk_id'], + v['aux_change_vdisk_name']]) + + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lspartnershipcandidate(self, **kwargs): + rows = [None] * 4 + master_sys = self._system_list['instorage-mcs-sim'] + aux_sys = self._system_list['aux-mcs-sim'] + rows[0] = ['id', 'configured', 'name'] + rows[1] = [master_sys['id'], 'no', master_sys['name']] + rows[2] = [aux_sys['id'], 'no', aux_sys['name']] + rows[3] = ['0123456789001234', 'no', 'fake_mcs'] + return self._print_info_cmd(rows=rows, **kwargs) + + def _cmd_lspartnership(self, **kwargs): + rows = [] + rows.append(['id', 'name', 'location', 'partnership', + 'type', 'cluster_ip', 'event_log_sequence']) + + master_sys = self._system_list['instorage-mcs-sim'] + if master_sys['name'] not in self._partnership_list: + local_info = {} + local_info['id'] = master_sys['id'] + local_info['name'] = master_sys['name'] + local_info['location'] = 'local' + local_info['type'] = '' + local_info['cluster_ip'] = '' + local_info['event_log_sequence'] = '' + local_info['chap_secret'] = '' + local_info['linkbandwidthmbits'] = '' + local_info['backgroundcopyrate'] = '' + local_info['partnership'] = '' + self._partnership_list[master_sys['id']] = local_info + + # Assume we always get a filtervalue argument + filter_key = kwargs['filtervalue'].split('=')[0] + filter_value = kwargs['filtervalue'].split('=')[1] + for k, v in self._partnership_list.items(): + if six.text_type(v[filter_key]) == filter_value: + rows.append([v['id'], v['name'], v['location'], + v['partnership'], v['type'], v['cluster_ip'], + v['event_log_sequence']]) + return self._print_info_cmd(rows=rows, **kwargs) + + def _get_mdiskgrp_id(self, mdiskgrp): + grp_num = len(self._flags['instorage_mcs_volpool_name']) + if mdiskgrp in self._flags['instorage_mcs_volpool_name']: + for i in range(grp_num): + if mdiskgrp == self._flags['instorage_mcs_volpool_name'][i]: + return i + 1 + elif mdiskgrp == 'openstack2': + return grp_num + 1 + elif mdiskgrp == 'openstack3': + return grp_num + 2 + else: + return None + + # Create a vdisk + def _cmd_mkvdisk(self, **kwargs): + # We only save the id/uid, name, and size - all else will be made up + volume_info = {} + volume_info['id'] = self._find_unused_id(self._volumes_list) + volume_info['uid'] = ('ABCDEF' * 3) + ('0' * 14) + volume_info['id'] + + mdiskgrp = kwargs['mdiskgrp'].strip('\'\"') + if mdiskgrp == kwargs['mdiskgrp']: + raise exception.InvalidInput( + reason='mdiskgrp missing quotes %s' % kwargs['mdiskgrp']) + mdiskgrp_id = self._get_mdiskgrp_id(mdiskgrp) + volume_info['mdisk_grp_name'] = mdiskgrp + volume_info['mdisk_grp_id'] = str(mdiskgrp_id) + + if 'name' in kwargs: + volume_info['name'] = kwargs['name'].strip('\'\"') + else: + volume_info['name'] = 'vdisk' + volume_info['id'] + + # Assume size and unit are given, store it in bytes + capacity = int(kwargs['size']) + unit = kwargs['unit'] + volume_info['capacity'] = self._convert_units_bytes(capacity, unit) + volume_info['IO_group_id'] = kwargs['iogrp'] + volume_info['IO_group_name'] = 'io_grp%s' % kwargs['iogrp'] + volume_info['RC_name'] = '' + volume_info['RC_id'] = '' + + if 'intier' in kwargs: + if kwargs['intier'] == 'on': + volume_info['in_tier'] = 'on' + else: + volume_info['in_tier'] = 'off' + + if 'rsize' in kwargs: + volume_info['formatted'] = 'no' + # Fake numbers + volume_info['used_capacity'] = '786432' + volume_info['real_capacity'] = '21474816' + volume_info['free_capacity'] = '38219264' + if 'warning' in kwargs: + volume_info['warning'] = kwargs['warning'].rstrip('%') + else: + volume_info['warning'] = '80' + if 'autoexpand' in kwargs: + volume_info['autoexpand'] = 'on' + else: + volume_info['autoexpand'] = 'off' + if 'grainsize' in kwargs: + volume_info['grainsize'] = kwargs['grainsize'] + else: + volume_info['grainsize'] = '32' + if 'compressed' in kwargs: + volume_info['compressed_copy'] = 'yes' + else: + volume_info['compressed_copy'] = 'no' + else: + volume_info['used_capacity'] = volume_info['capacity'] + volume_info['real_capacity'] = volume_info['capacity'] + volume_info['free_capacity'] = '0' + volume_info['warning'] = '' + volume_info['autoexpand'] = '' + volume_info['grainsize'] = '' + volume_info['compressed_copy'] = 'no' + volume_info['formatted'] = 'yes' + if 'nofmtdisk' in kwargs: + if kwargs['nofmtdisk']: + volume_info['formatted'] = 'no' + + vol_cp = {'id': '0', + 'status': 'online', + 'sync': 'yes', + 'primary': 'yes', + 'mdisk_grp_id': str(mdiskgrp_id), + 'mdisk_grp_name': mdiskgrp, + 'in_tier': volume_info['in_tier'], + 'compressed_copy': volume_info['compressed_copy']} + volume_info['copies'] = {'0': vol_cp} + + if volume_info['name'] in self._volumes_list: + return self._errors['CMMVC6035E'] + else: + self._volumes_list[volume_info['name']] = volume_info + return ('Virtual Disk, id [%s], successfully created' % + (volume_info['id']), '') + + # Delete a vdisk + def _cmd_rmvdisk(self, **kwargs): + force = True if 'force' in kwargs else False + + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + + if vol_name not in self._volumes_list: + return self._errors['CMMVC5753E'] + + if not force: + for mapping in self._mappings_list.values(): + if mapping['vol'] == vol_name: + return self._errors['CMMVC5840E'] + for lcmap in self._lcmappings_list.values(): + if ((lcmap['source'] == vol_name) or + (lcmap['target'] == vol_name)): + return self._errors['CMMVC5840E'] + + del self._volumes_list[vol_name] + return ('', '') + + def _cmd_expandvdisksize(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + + # Assume unit is gb + if 'size' not in kwargs: + return self._errors['CMMVC5707E'] + size = int(kwargs['size']) + + if vol_name not in self._volumes_list: + return self._errors['CMMVC5753E'] + + curr_size = int(self._volumes_list[vol_name]['capacity']) + addition = size * units.Gi + self._volumes_list[vol_name]['capacity'] = ( + six.text_type(curr_size + addition)) + return ('', '') + + def _add_port_to_host(self, host_info, **kwargs): + if 'iscsiname' in kwargs: + added_key = 'iscsi_names' + added_val = kwargs['iscsiname'].strip('\'\"') + elif 'hbawwpn' in kwargs: + added_key = 'wwpns' + added_val = kwargs['hbawwpn'].strip('\'\"') + else: + return self._errors['CMMVC5707E'] + + host_info[added_key].append(added_val) + + for v in self._hosts_list.values(): + if v['id'] == host_info['id']: + continue + for port in v[added_key]: + if port == added_val: + return self._errors['CMMVC6581E'] + return ('', '') + + # Make a host + def _cmd_mkhost(self, **kwargs): + host_info = {} + host_info['id'] = self._find_unused_id(self._hosts_list) + + if 'name' in kwargs: + host_name = kwargs['name'].strip('\'\"') + else: + host_name = 'host' + six.text_type(host_info['id']) + + if self._is_invalid_name(host_name): + return self._errors['CMMVC6527E'] + + if host_name in self._hosts_list: + return self._errors['CMMVC6035E'] + + host_info['host_name'] = host_name + host_info['iscsi_names'] = [] + host_info['wwpns'] = [] + + out, err = self._add_port_to_host(host_info, **kwargs) + if not len(err): + self._hosts_list[host_name] = host_info + return ('Host, id [%s], successfully created' % + (host_info['id']), '') + else: + return (out, err) + + # Add ports to an existing host + def _cmd_addhostport(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + host_name = kwargs['obj'].strip('\'\"') + + if host_name not in self._hosts_list: + return self._errors['CMMVC5753E'] + + host_info = self._hosts_list[host_name] + return self._add_port_to_host(host_info, **kwargs) + + # Change host properties + def _cmd_chhost(self, **kwargs): + if 'chapsecret' not in kwargs: + return self._errors['CMMVC5707E'] + secret = kwargs['obj'].strip('\'\"') + + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + host_name = kwargs['obj'].strip('\'\"') + + if host_name not in self._hosts_list: + return self._errors['CMMVC5753E'] + + self._hosts_list[host_name]['chapsecret'] = secret + return ('', '') + + # Remove a host + def _cmd_rmhost(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + + host_name = kwargs['obj'].strip('\'\"') + if host_name not in self._hosts_list: + return self._errors['CMMVC5753E'] + + for v in self._mappings_list.values(): + if (v['host'] == host_name): + return self._errors['CMMVC5871E'] + + del self._hosts_list[host_name] + return ('', '') + + # Create a vdisk-host mapping + def _cmd_mkvdiskhostmap(self, **kwargs): + mapping_info = {} + mapping_info['id'] = self._find_unused_id(self._mappings_list) + if 'host' not in kwargs: + return self._errors['CMMVC5707E'] + mapping_info['host'] = kwargs['host'].strip('\'\"') + + if 'scsi' in kwargs: + mapping_info['lun'] = kwargs['scsi'].strip('\'\"') + else: + mapping_info['lun'] = mapping_info['id'] + + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + mapping_info['vol'] = kwargs['obj'].strip('\'\"') + + if mapping_info['vol'] not in self._volumes_list: + return self._errors['CMMVC5753E'] + + if mapping_info['host'] not in self._hosts_list: + return self._errors['CMMVC5754E'] + + if mapping_info['vol'] in self._mappings_list: + return self._errors['CMMVC6071E'] + + for v in self._mappings_list.values(): + if ((v['host'] == mapping_info['host']) and + (v['lun'] == mapping_info['lun'])): + return self._errors['CMMVC5879E'] + + for v in self._mappings_list.values(): + if (v['vol'] == mapping_info['vol']) and ('force' not in kwargs): + return self._errors['CMMVC6071E'] + + self._mappings_list[mapping_info['id']] = mapping_info + return ('Virtual Disk to Host map, id [%s], successfully created' + % (mapping_info['id']), '') + + # Delete a vdisk-host mapping + def _cmd_rmvdiskhostmap(self, **kwargs): + if 'host' not in kwargs: + return self._errors['CMMVC5707E'] + host = kwargs['host'].strip('\'\"') + + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol = kwargs['obj'].strip('\'\"') + + mapping_ids = [] + for v in self._mappings_list.values(): + if v['vol'] == vol: + mapping_ids.append(v['id']) + if not mapping_ids: + return self._errors['CMMVC5753E'] + + this_mapping = None + for mapping_id in mapping_ids: + if self._mappings_list[mapping_id]['host'] == host: + this_mapping = mapping_id + if this_mapping is None: + return self._errors['CMMVC5753E'] + + del self._mappings_list[this_mapping] + return ('', '') + + # Create a LocalCopy mapping + def _cmd_mklcmap(self, **kwargs): + source = '' + target = '' + copyrate = kwargs['copyrate'] if 'copyrate' in kwargs else '50' + + if 'source' not in kwargs: + return self._errors['CMMVC5707E'] + source = kwargs['source'].strip('\'\"') + if source not in self._volumes_list: + return self._errors['CMMVC5754E'] + + if 'target' not in kwargs: + return self._errors['CMMVC5707E'] + target = kwargs['target'].strip('\'\"') + if target not in self._volumes_list: + return self._errors['CMMVC5754E'] + + if source == target: + return self._errors['CMMVC6303E'] + + if (self._volumes_list[source]['capacity'] != + self._volumes_list[target]['capacity']): + return self._errors['CMMVC5754E'] + + lcmap_info = {} + lcmap_info['source'] = source + lcmap_info['target'] = target + lcmap_info['id'] = self._find_unused_id(self._lcmappings_list) + lcmap_info['name'] = 'lcmap' + lcmap_info['id'] + lcmap_info['copyrate'] = copyrate + lcmap_info['progress'] = '0' + lcmap_info['autodelete'] = True if 'autodelete' in kwargs else False + lcmap_info['status'] = 'idle_or_copied' + + # Add lcmap to consistency group + if 'consistgrp' in kwargs: + consistgrp = kwargs['consistgrp'] + + # if is digit, assume is cg id, else is cg name + cg_id = 0 + if not consistgrp.isdigit(): + for consistgrp_key in self._lcconsistgrp_list.keys(): + if (self._lcconsistgrp_list[consistgrp_key]['name'] == + consistgrp): + cg_id = consistgrp_key + lcmap_info['consistgrp'] = consistgrp_key + break + else: + if int(consistgrp) in self._lcconsistgrp_list.keys(): + cg_id = int(consistgrp) + + # If can't find exist consistgrp id, return not exist error + if not cg_id: + return self._errors['CMMVC5754E'] + + lcmap_info['consistgrp'] = cg_id + # Add lcmap to consistgrp + self._lcconsistgrp_list[cg_id]['lcmaps'][lcmap_info['id']] = ( + lcmap_info['name']) + self._lc_cg_state_transition('add', + self._lcconsistgrp_list[cg_id]) + + self._lcmappings_list[lcmap_info['id']] = lcmap_info + + return('LocalCopy Mapping, id [' + lcmap_info['id'] + + '], successfully created', '') + + def _cmd_prestartlcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + if self._next_cmd_error['prestartlcmap'] == 'bad_id': + id_num = -1 + self._next_cmd_error['prestartlcmap'] = '' + + try: + lcmap = self._lcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._state_transition('prepare', lcmap) + + def _cmd_startlcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + if self._next_cmd_error['startlcmap'] == 'bad_id': + id_num = -1 + self._next_cmd_error['startlcmap'] = '' + + try: + lcmap = self._lcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._state_transition('start', lcmap) + + def _cmd_stoplcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + try: + lcmap = self._lcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._state_transition('stop', lcmap) + + def _cmd_rmlcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + force = True if 'force' in kwargs else False + + if self._next_cmd_error['rmlcmap'] == 'bad_id': + id_num = -1 + self._next_cmd_error['rmlcmap'] = '' + + try: + lcmap = self._lcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + function = 'delete_force' if force else 'delete' + ret = self._state_transition(function, lcmap) + if lcmap['status'] == 'end': + del self._lcmappings_list[id_num] + return ret + + def _cmd_chlcmap(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + id_num = kwargs['obj'] + + try: + lcmap = self._lcmappings_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + for key in ['name', 'copyrate', 'autodelete']: + if key in kwargs: + lcmap[key] = kwargs[key] + return ('', '') + + # Create a LocalCopy mapping + def _cmd_mklcconsistgrp(self, **kwargs): + lcconsistgrp_info = {} + lcconsistgrp_info['id'] = self._find_unused_id(self._lcconsistgrp_list) + + if 'name' in kwargs: + lcconsistgrp_info['name'] = kwargs['name'].strip('\'\"') + else: + lcconsistgrp_info['name'] = 'lccstgrp' + lcconsistgrp_info['id'] + + if 'autodelete' in kwargs: + lcconsistgrp_info['autodelete'] = True + else: + lcconsistgrp_info['autodelete'] = False + lcconsistgrp_info['status'] = 'empty' + lcconsistgrp_info['start_time'] = None + lcconsistgrp_info['lcmaps'] = {} + + self._lcconsistgrp_list[lcconsistgrp_info['id']] = lcconsistgrp_info + + return('LocalCopy Consistency Group, id [' + lcconsistgrp_info['id'] + + '], successfully created', '') + + def _cmd_prestartlcconsistgrp(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + cg_name = kwargs['obj'] + + cg_id = 0 + for cg_id in self._lcconsistgrp_list.keys(): + if cg_name == self._lcconsistgrp_list[cg_id]['name']: + break + + return self._lc_cg_state_transition('prepare', + self._lcconsistgrp_list[cg_id]) + + def _cmd_startlcconsistgrp(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + cg_name = kwargs['obj'] + + cg_id = 0 + for cg_id in self._lcconsistgrp_list.keys(): + if cg_name == self._lcconsistgrp_list[cg_id]['name']: + break + + return self._lc_cg_state_transition('start', + self._lcconsistgrp_list[cg_id]) + + def _cmd_stoplcconsistgrp(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + try: + lcconsistgrps = self._lcconsistgrp_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + return self._lc_cg_state_transition('stop', lcconsistgrps) + + def _cmd_rmlcconsistgrp(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + cg_name = kwargs['obj'] + force = True if 'force' in kwargs else False + + cg_id = 0 + for cg_id in self._lcconsistgrp_list.keys(): + if cg_name == self._lcconsistgrp_list[cg_id]['name']: + break + if not cg_id: + return self._errors['CMMVC5753E'] + lcconsistgrps = self._lcconsistgrp_list[cg_id] + + function = 'delete_force' if force else 'delete' + ret = self._lc_cg_state_transition(function, lcconsistgrps) + if lcconsistgrps['status'] == 'end': + del self._lcconsistgrp_list[cg_id] + return ret + + def _cmd_migratevdisk(self, **kwargs): + if 'mdiskgrp' not in kwargs or 'vdisk' not in kwargs: + return self._errors['CMMVC5707E'] + mdiskgrp = kwargs['mdiskgrp'].strip('\'\"') + vdisk = kwargs['vdisk'].strip('\'\"') + + if vdisk in self._volumes_list: + curr_mdiskgrp = self._volumes_list + else: + for pool in self._other_pools: + if vdisk in pool: + curr_mdiskgrp = pool + break + else: + return self._errors['CMMVC5754E'] + + if mdiskgrp == self._flags['instorage_mcs_volpool_name']: + tgt_mdiskgrp = self._volumes_list + elif mdiskgrp == 'openstack2': + tgt_mdiskgrp = self._other_pools['openstack2'] + elif mdiskgrp == 'openstack3': + tgt_mdiskgrp = self._other_pools['openstack3'] + else: + return self._errors['CMMVC5754E'] + + if curr_mdiskgrp == tgt_mdiskgrp: + return self._errors['CMMVC6430E'] + + vol = curr_mdiskgrp[vdisk] + tgt_mdiskgrp[vdisk] = vol + del curr_mdiskgrp[vdisk] + return ('', '') + + def _cmd_addvdiskcopy(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + if vol_name not in self._volumes_list: + return self._errors['CMMVC5753E'] + vol = self._volumes_list[vol_name] + if 'mdiskgrp' not in kwargs: + return self._errors['CMMVC5707E'] + mdiskgrp = kwargs['mdiskgrp'].strip('\'\"') + if mdiskgrp == kwargs['mdiskgrp']: + raise exception.InvalidInput( + reason='mdiskgrp missing quotes %s') % kwargs['mdiskgrp'] + + copy_info = {} + copy_info['id'] = self._find_unused_id(vol['copies']) + copy_info['status'] = 'online' + copy_info['sync'] = 'no' + copy_info['primary'] = 'no' + copy_info['mdisk_grp_name'] = mdiskgrp + copy_info['mdisk_grp_id'] = str(self._get_mdiskgrp_id(mdiskgrp)) + + if 'intier' in kwargs: + if kwargs['intier'] == 'on': + copy_info['in_tier'] = 'on' + else: + copy_info['in_tier'] = 'off' + if 'rsize' in kwargs: + if 'compressed' in kwargs: + copy_info['compressed_copy'] = 'yes' + else: + copy_info['compressed_copy'] = 'no' + vol['copies'][copy_info['id']] = copy_info + return ('Vdisk [%(vid)s] copy [%(cid)s] successfully created' % + {'vid': vol['id'], 'cid': copy_info['id']}, '') + + def _cmd_rmvdiskcopy(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + if 'copy' not in kwargs: + return self._errors['CMMVC5707E'] + copy_id = kwargs['copy'].strip('\'\"') + if vol_name not in self._volumes_list: + return self._errors['CMMVC5753E'] + vol = self._volumes_list[vol_name] + if copy_id not in vol['copies']: + return self._errors['CMMVC6353E'] + del vol['copies'][copy_id] + + return ('', '') + + def _cmd_chvdisk(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + vol = self._volumes_list[vol_name] + kwargs.pop('obj') + + params = ['name', 'warning', 'udid', + 'autoexpand', 'intier', 'primary'] + for key, value in kwargs.items(): + if key == 'intier': + vol['in_tier'] = value + continue + if key == 'warning': + vol['warning'] = value.rstrip('%') + continue + if key == 'name': + vol['name'] = value + del self._volumes_list[vol_name] + self._volumes_list[value] = vol + if key == 'primary': + copies = self._volumes_list[vol_name]['copies'] + if value == '0': + copies['0']['primary'] = 'yes' + copies['1']['primary'] = 'no' + elif value == '1': + copies['0']['primary'] = 'no' + copies['1']['primary'] = 'yes' + else: + err = self._errors['CMMVC6353E'][1] % {'VALUE': key} + return ('', err) + if key in params: + vol[key] = value + else: + err = self._errors['CMMVC5709E'][1] % {'VALUE': key} + return ('', err) + return ('', '') + + def _cmd_movevdisk(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + vol_name = kwargs['obj'].strip('\'\"') + vol = self._volumes_list[vol_name] + + if 'iogrp' not in kwargs: + return self._errors['CMMVC5707E'] + + iogrp = kwargs['iogrp'] + if iogrp.isdigit(): + vol['IO_group_id'] = iogrp + vol['IO_group_name'] = 'io_grp%s' % iogrp + else: + vol['IO_group_id'] = iogrp[6:] + vol['IO_group_name'] = iogrp + return ('', '') + + def _cmd_addvdiskaccess(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + return ('', '') + + def _cmd_rmvdiskaccess(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + return ('', '') + + def _add_host_to_list(self, connector): + host_info = {} + host_info['id'] = self._find_unused_id(self._hosts_list) + host_info['host_name'] = connector['host'] + host_info['iscsi_names'] = [] + host_info['wwpns'] = [] + if 'initiator' in connector: + host_info['iscsi_names'].append(connector['initiator']) + if 'wwpns' in connector: + host_info['wwpns'] = host_info['wwpns'] + connector['wwpns'] + self._hosts_list[connector['host']] = host_info + + def _host_in_list(self, host_name): + for k in self._hosts_list: + if k.startswith(host_name): + return k + return None + + # Replication related command + # Create a remote copy + def _cmd_mkrcrelationship(self, **kwargs): + master_vol = '' + aux_vol = '' + aux_cluster = '' + master_sys = self._system_list['instorage-mcs-sim'] + aux_sys = self._system_list['aux-mcs-sim'] + + if 'master' not in kwargs: + return self._errors['CMMVC5707E'] + master_vol = kwargs['master'].strip('\'\"') + if master_vol not in self._volumes_list: + return self._errors['CMMVC5754E'] + + if 'aux' not in kwargs: + return self._errors['CMMVC5707E'] + aux_vol = kwargs['aux'].strip('\'\"') + if aux_vol not in self._volumes_list: + return self._errors['CMMVC5754E'] + + if 'cluster' not in kwargs: + return self._errors['CMMVC5707E'] + aux_cluster = kwargs['cluster'].strip('\'\"') + if aux_cluster != aux_sys['name']: + return self._errors['CMMVC5754E'] + + if (self._volumes_list[master_vol]['capacity'] != + self._volumes_list[aux_vol]['capacity']): + return self._errors['CMMVC5754E'] + rcrel_info = {} + rcrel_info['id'] = self._find_unused_id(self._rcrelationship_list) + rcrel_info['name'] = 'rcrel' + rcrel_info['id'] + rcrel_info['master_cluster_id'] = master_sys['id'] + rcrel_info['master_cluster_name'] = master_sys['name'] + rcrel_info['master_vdisk_id'] = self._volumes_list[master_vol]['id'] + rcrel_info['master_vdisk_name'] = master_vol + rcrel_info['aux_cluster_id'] = aux_sys['id'] + rcrel_info['aux_cluster_name'] = aux_sys['name'] + rcrel_info['aux_vdisk_id'] = self._volumes_list[aux_vol]['id'] + rcrel_info['aux_vdisk_name'] = aux_vol + rcrel_info['primary'] = 'master' + rcrel_info['consistency_group_id'] = '' + rcrel_info['consistency_group_name'] = '' + rcrel_info['state'] = 'inconsistent_stopped' + rcrel_info['bg_copy_priority'] = '50' + rcrel_info['progress'] = '0' + rcrel_info['freeze_time'] = '' + rcrel_info['status'] = 'online' + rcrel_info['sync'] = '' + rcrel_info['copy_type'] = 'async' if 'async' in kwargs else 'sync' + rcrel_info['cycling_mode'] = '' + rcrel_info['cycle_period_seconds'] = '300' + rcrel_info['master_change_vdisk_id'] = '' + rcrel_info['master_change_vdisk_name'] = '' + rcrel_info['aux_change_vdisk_id'] = '' + rcrel_info['aux_change_vdisk_name'] = '' + + self._rcrelationship_list[rcrel_info['name']] = rcrel_info + self._volumes_list[master_vol]['RC_name'] = rcrel_info['name'] + self._volumes_list[master_vol]['RC_id'] = rcrel_info['id'] + self._volumes_list[aux_vol]['RC_name'] = rcrel_info['name'] + self._volumes_list[aux_vol]['RC_id'] = rcrel_info['id'] + return('RC Relationship, id [' + rcrel_info['id'] + + '], successfully created', '') + + def _cmd_startrcrelationship(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + + primary_vol = None + if 'primary' in kwargs: + primary_vol = kwargs['primary'].strip('\'\"') + + try: + rcrel = self._rcrelationship_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + if rcrel['state'] == 'idling' and not primary_vol: + return self._errors['CMMVC5963E'] + + self._rc_state_transition('start', rcrel) + if primary_vol: + self._rcrelationship_list[id_num]['primary'] = primary_vol + return ('', '') + + def _cmd_stoprcrelationship(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + force_access = True if 'access' in kwargs else False + + try: + rcrel = self._rcrelationship_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + function = 'stop_access' if force_access else 'stop' + self._rc_state_transition(function, rcrel) + if force_access: + self._rcrelationship_list[id_num]['primary'] = '' + return ('', '') + + def _cmd_switchrcrelationship(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5707E'] + id_num = kwargs['obj'] + + try: + rcrel = self._rcrelationship_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + if rcrel['state'] == instorage_const.REP_CONSIS_SYNC: + rcrel['primary'] = kwargs['primary'] + return ('', '') + else: + return self._errors['CMMVC5753E'] + + def _cmd_rmrcrelationship(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + id_num = kwargs['obj'] + force = True if 'force' in kwargs else False + + try: + rcrel = self._rcrelationship_list[id_num] + except KeyError: + return self._errors['CMMVC5753E'] + + function = 'delete_force' if force else 'delete' + self._rc_state_transition(function, rcrel) + if rcrel['state'] == 'end': + self._volumes_list[rcrel['master_vdisk_name']]['RC_name'] = '' + self._volumes_list[rcrel['master_vdisk_name']]['RC_id'] = '' + self._volumes_list[rcrel['aux_vdisk_name']]['RC_name'] = '' + self._volumes_list[rcrel['aux_vdisk_name']]['RC_id'] = '' + del self._rcrelationship_list[id_num] + + return ('', '') + + def _rc_state_transition(self, function, rcrel): + if (function == 'wait' and + 'wait' not in self._rc_transitions[rcrel['state']]): + return ('', '') + + if rcrel['state'] == 'inconsistent_copying' and function == 'wait': + if rcrel['progress'] == '0': + rcrel['progress'] = '50' + else: + rcrel['progress'] = '100' + rcrel['state'] = 'consistent_synchronized' + return ('', '') + else: + try: + curr_state = rcrel['state'] + rcrel['state'] = self._rc_transitions[curr_state][function] + return ('', '') + except Exception: + return self._errors['CMMVC5982E'] + + def _cmd_mkippartnership(self, **kwargs): + if 'clusterip' not in kwargs: + return self._errors['CMMVC5707E'] + clusterip = kwargs['master'].strip('\'\"') + + if 'linkbandwidthmbits' not in kwargs: + return self._errors['CMMVC5707E'] + bandwith = kwargs['linkbandwidthmbits'].strip('\'\"') + + if 'backgroundcopyrate' not in kwargs: + return self._errors['CMMVC5707E'] + copyrate = kwargs['backgroundcopyrate'].strip('\'\"') + + if clusterip == '192.168.10.21': + partner_info_id = self._system_list['instorage-mcs-sim']['id'] + partner_info_name = self._system_list['instorage-mcs-sim']['name'] + else: + partner_info_id = self._system_list['aux-mcs-sim']['id'] + partner_info_name = self._system_list['aux-mcs-sim']['name'] + + partner_info = {} + partner_info['id'] = partner_info_id + partner_info['name'] = partner_info_name + partner_info['location'] = 'remote' + partner_info['type'] = 'ipv4' + partner_info['cluster_ip'] = clusterip + partner_info['event_log_sequence'] = '' + partner_info['chap_secret'] = '' + partner_info['linkbandwidthmbits'] = bandwith + partner_info['backgroundcopyrate'] = copyrate + partner_info['partnership'] = 'fully_configured' + + self._partnership_list[partner_info['id']] = partner_info + return('', '') + + def _cmd_mkfcpartnership(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + peer_sys = kwargs['obj'] + + if 'linkbandwidthmbits' not in kwargs: + return self._errors['CMMVC5707E'] + bandwith = kwargs['linkbandwidthmbits'].strip('\'\"') + + if 'backgroundcopyrate' not in kwargs: + return self._errors['CMMVC5707E'] + copyrate = kwargs['backgroundcopyrate'].strip('\'\"') + + partner_info = {} + partner_info['id'] = self._system_list[peer_sys]['id'] + partner_info['name'] = peer_sys + partner_info['location'] = 'remote' + partner_info['type'] = 'fc' + partner_info['cluster_ip'] = '' + partner_info['event_log_sequence'] = '' + partner_info['chap_secret'] = '' + partner_info['linkbandwidthmbits'] = bandwith + partner_info['backgroundcopyrate'] = copyrate + partner_info['partnership'] = 'fully_configured' + self._partnership_list[partner_info['id']] = partner_info + return('', '') + + def _cmd_chpartnership(self, **kwargs): + if 'obj' not in kwargs: + return self._errors['CMMVC5701E'] + peer_sys = kwargs['obj'] + if peer_sys not in self._partnership_list: + return self._errors['CMMVC5753E'] + + partner_state = ('fully_configured' if 'start'in kwargs + else 'fully_configured_stopped') + self._partnership_list[peer_sys]['partnership'] = partner_state + return('', '') + + # The main function to run commands on the management simulator + def execute_command(self, cmd, check_exit_code=True): + try: + kwargs = self._cmd_to_dict(cmd) + except IndexError: + return self._errors['CMMVC5707E'] + + command = kwargs.pop('cmd') + func = getattr(self, '_cmd_' + command) + out, err = func(**kwargs) + + if (check_exit_code) and (len(err) != 0): + raise processutils.ProcessExecutionError(exit_code=1, + stdout=out, + stderr=err, + cmd=' '.join(cmd)) + + return (out, err) + + # After calling this function, the next call to the specified command will + # result in in the error specified + def error_injection(self, cmd, error): + self._next_cmd_error[cmd] = error + + def change_vdiskcopy_attr(self, vol_name, key, value, copy="primary"): + if copy == 'primary': + self._volumes_list[vol_name]['copies']['0'][key] = value + elif copy == 'secondary': + self._volumes_list[vol_name]['copies']['1'][key] = value + else: + msg = "The copy should be primary or secondary" + raise exception.InvalidInput(reason=msg) diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py b/cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py new file mode 100644 index 00000000000..dd5825a05f6 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/test_common.py @@ -0,0 +1,1775 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +import ddt +from eventlet import greenthread +import mock +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_utils import importutils +from oslo_utils import units +import paramiko + +from cinder import context +from cinder import exception +from cinder import objects +from cinder.objects import fields +from cinder import ssh_utils +from cinder import test +from cinder.tests.unit import utils as testutils +from cinder.volume import configuration as conf +from cinder.volume.drivers.inspur.instorage import ( + replication as instorage_rep) +from cinder.volume.drivers.inspur.instorage import instorage_common +from cinder.volume.drivers.inspur.instorage import instorage_iscsi +from cinder.volume import qos_specs +from cinder.volume import utils as volume_utils +from cinder.volume import volume_types + +from cinder.tests.unit.volume.drivers.inspur.instorage import fakes + +CONF = cfg.CONF + + +@ddt.ddt +class InStorageMCSCommonDriverTestCase(test.TestCase): + + @mock.patch.object(greenthread, 'sleep') + def setUp(self, mock_sleep): + super(InStorageMCSCommonDriverTestCase, self).setUp() + self._def_flags = {'san_ip': 'hostname', + 'instorage_san_secondary_ip': 'secondaryname', + 'san_login': 'user', + 'san_password': 'pass', + 'instorage_mcs_volpool_name': fakes.MCS_POOLS, + 'instorage_mcs_localcopy_timeout': 20, + 'instorage_mcs_localcopy_rate': 49, + 'instorage_mcs_allow_tenant_qos': True} + config = conf.Configuration(instorage_common.instorage_mcs_opts, + conf.SHARED_CONF_GROUP) + # Override any configs that may get set in __init__ + self._reset_flags(config) + self.driver = fakes.FakeInStorageMCSISCSIDriver(configuration=config) + self._driver = instorage_iscsi.InStorageMCSISCSIDriver( + configuration=config) + wwpns = ['1234567890123450', '6543210987654325'] + initiator = 'test.initiator.%s' % 123450 + self._connector = {'ip': '1.234.56.78', + 'host': 'instorage-mcs-test', + 'wwpns': wwpns, + 'initiator': initiator} + self.sim = fakes.FakeInStorage(fakes.MCS_POOLS) + + self.driver.set_fake_storage(self.sim) + self.ctxt = context.get_admin_context() + + self.ctxt = context.get_admin_context() + db_driver = CONF.db_driver + self.db = importutils.import_module(db_driver) + self.driver.db = self.db + self.driver.do_setup(None) + self.driver.check_for_setup_error() + self.driver._assistant.check_lcmapping_interval = 0 + self.mock_object(instorage_iscsi.InStorageMCSISCSIDriver, + 'DEFAULT_GR_SLEEP', 0) + + def _set_flag(self, flag, value, configuration=None): + if not configuration: + configuration = self.driver.configuration + group = configuration.config_group + self.override_config(flag, value, group) + + def _reset_flags(self, configuration=None): + if not configuration: + configuration = self.driver.configuration + CONF.reset() + for k, v in self._def_flags.items(): + self._set_flag(k, v, configuration) + + def _assert_vol_exists(self, name, exists): + is_vol_defined = self.driver._assistant.is_vdisk_defined(name) + self.assertEqual(exists, is_vol_defined) + + def test_instorage_mcs_connectivity(self): + # Make sure we detect if the pool doesn't exist + no_exist_pool = 'i-dont-exist-%s' % 56789 + self._set_flag('instorage_mcs_volpool_name', no_exist_pool) + self.assertRaises(exception.InvalidInput, + self.driver.do_setup, None) + self._reset_flags() + + # Check the case where the user didn't configure IP addresses + # as well as receiving unexpected results from the storage + self.sim.error_injection('lsnodecanister', 'header_mismatch') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) + self.sim.error_injection('lsnodecanister', 'remove_field') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) + self.sim.error_injection('lsportip', 'header_mismatch') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) + self.sim.error_injection('lsportip', 'remove_field') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.do_setup, None) + + # Check with bad parameters + self._set_flag('san_ip', '') + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + self._set_flag('san_password', None) + self._set_flag('san_private_key', None) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + self._set_flag('instorage_mcs_vol_grainsize', 42) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + self._set_flag('instorage_mcs_vol_compression', True) + self._set_flag('instorage_mcs_vol_rsize', -1) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + self._set_flag('instorage_mcs_vol_iogrp', 5) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + self.sim.error_injection('lslicense', 'no_compression') + self.sim.error_injection('lsguicapabilities', 'no_compression') + self._set_flag('instorage_mcs_vol_compression', True) + self.driver.do_setup(None) + self.assertRaises(exception.InvalidInput, + self.driver.check_for_setup_error) + self._reset_flags() + + # Finally, check with good parameters + self.driver.do_setup(None) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_set_up_with_san_ip(self, mock_ssh_execute, mock_ssh_pool): + ssh_cmd = ['mcsinq'] + self._driver._run_ssh(ssh_cmd) + + mock_ssh_pool.assert_called_once_with( + self._driver.configuration.san_ip, + self._driver.configuration.san_ssh_port, + self._driver.configuration.ssh_conn_timeout, + self._driver.configuration.san_login, + password=self._driver.configuration.san_password, + privatekey=self._driver.configuration.san_private_key, + min_size=self._driver.configuration.ssh_min_pool_conn, + max_size=self._driver.configuration.ssh_max_pool_conn) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_set_up_with_secondary_ip(self, mock_ssh_execute, + mock_ssh_pool): + mock_ssh_pool.side_effect = [paramiko.SSHException, mock.MagicMock()] + ssh_cmd = ['mcsinq'] + self._driver._run_ssh(ssh_cmd) + + mock_ssh_pool.assert_called_with( + self._driver.configuration.instorage_san_secondary_ip, + self._driver.configuration.san_ssh_port, + self._driver.configuration.ssh_conn_timeout, + self._driver.configuration.san_login, + password=self._driver.configuration.san_password, + privatekey=self._driver.configuration.san_private_key, + min_size=self._driver.configuration.ssh_min_pool_conn, + max_size=self._driver.configuration.ssh_max_pool_conn) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_fail_to_secondary_ip(self, mock_ssh_execute, + mock_ssh_pool): + mock_ssh_execute.side_effect = [processutils.ProcessExecutionError, + mock.MagicMock()] + ssh_cmd = ['mcsinq'] + self._driver._run_ssh(ssh_cmd) + + mock_ssh_pool.assert_called_with( + self._driver.configuration.instorage_san_secondary_ip, + self._driver.configuration.san_ssh_port, + self._driver.configuration.ssh_conn_timeout, + self._driver.configuration.san_login, + password=self._driver.configuration.san_password, + privatekey=self._driver.configuration.san_private_key, + min_size=self._driver.configuration.ssh_min_pool_conn, + max_size=self._driver.configuration.ssh_max_pool_conn) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_secondary_ip_ssh_fail_to_san_ip(self, mock_ssh_execute, + mock_ssh_pool): + mock_ssh_pool.side_effect = [ + paramiko.SSHException, + mock.MagicMock( + ip=self._driver.configuration.instorage_san_secondary_ip), + mock.MagicMock()] + mock_ssh_execute.side_effect = [processutils.ProcessExecutionError, + mock.MagicMock()] + ssh_cmd = ['mcsinq'] + self._driver._run_ssh(ssh_cmd) + + mock_ssh_pool.assert_called_with( + self._driver.configuration.san_ip, + self._driver.configuration.san_ssh_port, + self._driver.configuration.ssh_conn_timeout, + self._driver.configuration.san_login, + password=self._driver.configuration.san_password, + privatekey=self._driver.configuration.san_private_key, + min_size=self._driver.configuration.ssh_min_pool_conn, + max_size=self._driver.configuration.ssh_max_pool_conn) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_both_ip_set_failure(self, mock_ssh_execute, + mock_ssh_pool): + mock_ssh_pool.side_effect = [ + paramiko.SSHException, + mock.MagicMock(), + mock.MagicMock()] + mock_ssh_execute.side_effect = [processutils.ProcessExecutionError, + processutils.ProcessExecutionError] + ssh_cmd = ['mcsinq'] + self.assertRaises(processutils.ProcessExecutionError, + self._driver._run_ssh, ssh_cmd) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_second_ip_not_set_failure(self, mock_ssh_execute, + mock_ssh_pool): + mock_ssh_execute.side_effect = [processutils.ProcessExecutionError, + mock.MagicMock()] + self._set_flag('instorage_san_secondary_ip', None) + ssh_cmd = ['mcsinq'] + self.assertRaises(processutils.ProcessExecutionError, + self._driver._run_ssh, ssh_cmd) + + @mock.patch.object(ssh_utils, 'SSHPool') + @mock.patch.object(processutils, 'ssh_execute') + def test_run_ssh_consistent_active_ip(self, mock_ssh_execute, + mock_ssh_pool): + ssh_cmd = ['mcsinq'] + self._driver._run_ssh(ssh_cmd) + self._driver._run_ssh(ssh_cmd) + self._driver._run_ssh(ssh_cmd) + self.assertEqual(self._driver.configuration.san_ip, + self._driver.active_ip) + mock_ssh_execute.side_effect = [paramiko.SSHException, + mock.MagicMock(), mock.MagicMock()] + self._driver._run_ssh(ssh_cmd) + self._driver._run_ssh(ssh_cmd) + self.assertEqual(self._driver.configuration.instorage_san_secondary_ip, + self._driver.active_ip) + + def _generate_vol_info(self, vol_name, vol_id): + pool = fakes.get_test_pool() + prop = {'mdisk_grp_name': pool} + if vol_name: + prop.update(volume_name=vol_name, + volume_id=vol_id, + volume_size=10) + else: + prop.update(size=10, + volume_type_id=None, + mdisk_grp_name=pool, + host='openstack@mcs#%s' % pool) + vol = testutils.create_volume(self.ctxt, **prop) + return vol + + def _generate_snapshot_info(self, vol): + snap = testutils.create_snapshot(self.ctxt, vol.id) + return snap + + def _create_volume(self, **kwargs): + pool = fakes.get_test_pool() + prop = {'host': 'openstack@mcs#%s' % pool, + 'size': 1} + for p in prop.keys(): + if p not in kwargs: + kwargs[p] = prop[p] + vol = testutils.create_volume(self.ctxt, **kwargs) + self.driver.create_volume(vol) + return vol + + def _delete_volume(self, volume): + self.driver.delete_volume(volume) + self.db.volume_destroy(self.ctxt, volume['id']) + + def _create_group_in_db(self, **kwargs): + group = testutils.create_group(self.ctxt, **kwargs) + return group + + def _create_group(self, **kwargs): + group = self._create_group_in_db(**kwargs) + + model_update = self.driver.create_group(self.ctxt, group) + self.assertEqual(fields.GroupStatus.AVAILABLE, + model_update['status'], + "Group created failed") + return group + + def _create_group_snapshot_in_db(self, grp_id, **kwargs): + group_snapshot = testutils.create_group_snapshot(self.ctxt, + group_id=grp_id, + **kwargs) + snapshots = [] + grp_id = group_snapshot['group_id'] + volumes = self.db.volume_get_all_by_group(self.ctxt.elevated(), + grp_id) + + if not volumes: + msg = "Group is empty. No group snapshot will be created." + raise exception.InvalidGroup(reason=msg) + + for volume in volumes: + snapshots.append(testutils.create_snapshot( + self.ctxt, volume['id'], + group_snapshot.id, + group_snapshot.name, + group_snapshot.id, + fields.SnapshotStatus.CREATING)) + return group_snapshot, snapshots + + def _create_group_snapshot(self, grp_id, **kwargs): + group_snapshot, snapshots = self._create_group_snapshot_in_db( + grp_id, **kwargs) + + model_update, snapshots_model = ( + self.driver.create_group_snapshot( + self.ctxt, group_snapshot, snapshots)) + self.assertEqual('available', + model_update['status'], + "Group_Snapshot created failed") + + for snapshot in snapshots_model: + self.assertEqual(fields.SnapshotStatus.AVAILABLE, + snapshot['status']) + return group_snapshot, snapshots + + def _create_test_vol(self, opts): + ctxt = testutils.get_test_admin_context() + type_ref = volume_types.create(ctxt, 'testtype', opts) + volume = self._generate_vol_info(None, None) + volume.volume_type_id = type_ref['id'] + volume.volume_typ = objects.VolumeType.get_by_id(ctxt, + type_ref['id']) + self.driver.create_volume(volume) + + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + self.driver.delete_volume(volume) + volume_types.destroy(ctxt, type_ref['id']) + return attrs + + def _get_default_opts(self): + opt = {'rsize': 2, + 'warning': 0, + 'autoexpand': True, + 'grainsize': 256, + 'compression': False, + 'intier': True, + 'iogrp': '0', + 'qos': None, + 'replication': False} + return opt + + @mock.patch.object(instorage_common.InStorageAssistant, 'add_vdisk_qos') + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_get_vdisk_params') + def test_instorage_mcs_create_volume_with_qos(self, get_vdisk_params, + add_vdisk_qos): + vol = testutils.create_volume(self.ctxt) + fake_opts = self._get_default_opts() + # If the qos is empty, chvdisk should not be called + # for create_volume. + get_vdisk_params.return_value = fake_opts + self.driver.create_volume(vol) + self._assert_vol_exists(vol['name'], True) + self.assertFalse(add_vdisk_qos.called) + self.driver.delete_volume(vol) + + # If the qos is not empty, chvdisk should be called + # for create_volume. + fake_opts['qos'] = {'IOThrottling': 5000} + get_vdisk_params.return_value = fake_opts + self.driver.create_volume(vol) + self._assert_vol_exists(vol['name'], True) + add_vdisk_qos.assert_called_once_with(vol['name'], fake_opts['qos']) + + self.driver.delete_volume(vol) + self._assert_vol_exists(vol['name'], False) + + def test_instorage_mcs_snapshots(self): + vol1 = self._create_volume() + snap1 = self._generate_snapshot_info(vol1) + + # Test timeout and volume cleanup + self._set_flag('instorage_mcs_localcopy_timeout', 1) + self.assertRaises(exception.VolumeDriverException, + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) + self._reset_flags() + + # Test prestartlcmap failing + with mock.patch.object( + instorage_common.InStorageSSH, 'prestartlcmap') as prestart: + prestart.side_effect = exception.VolumeBackendAPIException + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, snap1) + + self.sim.error_injection('lslcmap', 'speed_up') + self.sim.error_injection('startlcmap', 'bad_id') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) + self.sim.error_injection('prestartlcmap', 'bad_id') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, snap1) + self._assert_vol_exists(snap1['name'], False) + + # Test successful snapshot + self.driver.create_snapshot(snap1) + self._assert_vol_exists(snap1['name'], True) + + # Try to create a snapshot from an non-existing volume - should fail + snap_vol_src = self._generate_vol_info(None, None) + snap_novol = self._generate_snapshot_info(snap_vol_src) + self.assertRaises(exception.VolumeDriverException, + self.driver.create_snapshot, + snap_novol) + + # We support deleting a volume that has snapshots, so delete the volume + # first + self.driver.delete_volume(vol1) + self.driver.delete_snapshot(snap1) + + def test_instorage_mcs_create_cloned_volume(self): + vol1 = self._create_volume() + vol2 = testutils.create_volume(self.ctxt) + vol3 = testutils.create_volume(self.ctxt) + + # Try to clone where source size > target size + vol1['size'] = vol2['size'] + 1 + self.assertRaises(exception.InvalidInput, + self.driver.create_cloned_volume, + vol2, vol1) + self._assert_vol_exists(vol2['name'], False) + + # Try to clone where source size = target size + vol1['size'] = vol2['size'] + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_cloned_volume(vol2, vol1) + # validate copyrate was set on the local copy + for i, lcmap in self.sim._lcmappings_list.items(): + if lcmap['target'] == vol1['name']: + self.assertEqual('49', lcmap['copyrate']) + self._assert_vol_exists(vol2['name'], True) + + # Try to clone where source size < target size + vol3['size'] = vol1['size'] + 1 + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_cloned_volume(vol3, vol1) + # Validate copyrate was set on the local copy + for i, lcmap in self.sim._lcmappings_list.items(): + if lcmap['target'] == vol1['name']: + self.assertEqual('49', lcmap['copyrate']) + self._assert_vol_exists(vol3['name'], True) + + # Delete in the 'opposite' order to make sure it works + self.driver.delete_volume(vol3) + self._assert_vol_exists(vol3['name'], False) + self.driver.delete_volume(vol2) + self._assert_vol_exists(vol2['name'], False) + self.driver.delete_volume(vol1) + self._assert_vol_exists(vol1['name'], False) + + def test_instorage_mcs_create_volume_from_snapshot(self): + vol1 = self._create_volume(size=10) + snap1 = self._generate_snapshot_info(vol1) + self.driver.create_snapshot(snap1) + vol2 = self._generate_vol_info(None, None) + vol3 = self._generate_vol_info(None, None) + + # Try to create a volume from a non-existing snapshot + snap_vol_src = self._generate_vol_info(None, None) + snap_novol = self._generate_snapshot_info(snap_vol_src) + vol_novol = self._generate_vol_info(None, None) + self.assertRaises(exception.VolumeDriverException, + self.driver.create_volume_from_snapshot, + vol_novol, + snap_novol) + + # Fail the snapshot + with mock.patch.object( + instorage_common.InStorageSSH, 'prestartlcmap') as prestart: + prestart.side_effect = exception.VolumeBackendAPIException + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + vol2, snap1) + self._assert_vol_exists(vol2['name'], False) + + # Try to create where volume size < snapshot size + snap1.volume_size += 1 + self.assertRaises(exception.InvalidInput, + self.driver.create_volume_from_snapshot, + vol2, snap1) + self._assert_vol_exists(vol2['name'], False) + snap1.volume_size -= 1 + + # Try to create where volume size > snapshot size + vol2['size'] += 1 + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_volume_from_snapshot(vol2, snap1) + self._assert_vol_exists(vol2['name'], True) + vol2['size'] -= 1 + + # Try to create where volume size = snapshot size + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_volume_from_snapshot(vol3, snap1) + self._assert_vol_exists(vol3['name'], True) + + # Delete in the 'opposite' order to make sure it works + self.driver.delete_volume(vol3) + self._assert_vol_exists(vol3['name'], False) + self.driver.delete_volume(vol2) + self._assert_vol_exists(vol2['name'], False) + self.driver.delete_snapshot(snap1) + self._assert_vol_exists(snap1['name'], False) + self.driver.delete_volume(vol1) + self._assert_vol_exists(vol1['name'], False) + + @mock.patch.object(instorage_common.InStorageAssistant, 'add_vdisk_qos') + def test_instorage_mcs_create_volfromsnap_clone_with_qos(self, + add_vdisk_qos): + vol1 = self._create_volume() + snap1 = self._generate_snapshot_info(vol1) + self.driver.create_snapshot(snap1) + vol2 = self._generate_vol_info(None, None) + vol3 = self._generate_vol_info(None, None) + fake_opts = self._get_default_opts() + + # Succeed + self.sim.error_injection('lslcmap', 'speed_up') + + # If the qos is empty, chvdisk should not be called + # for create_volume_from_snapshot. + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + get_vdisk_params.return_value = fake_opts + self.driver.create_volume_from_snapshot(vol2, snap1) + self._assert_vol_exists(vol2['name'], True) + self.assertFalse(add_vdisk_qos.called) + self.driver.delete_volume(vol2) + + # If the qos is not empty, chvdisk should be called + # for create_volume_from_snapshot. + fake_opts['qos'] = {'IOThrottling': 5000} + get_vdisk_params.return_value = fake_opts + self.driver.create_volume_from_snapshot(vol2, snap1) + self._assert_vol_exists(vol2['name'], True) + add_vdisk_qos.assert_called_once_with(vol2['name'], + fake_opts['qos']) + + self.sim.error_injection('lslcmap', 'speed_up') + + # If the qos is empty, chvdisk should not be called + # for create_volume_from_snapshot. + add_vdisk_qos.reset_mock() + fake_opts['qos'] = None + get_vdisk_params.return_value = fake_opts + self.driver.create_cloned_volume(vol3, vol2) + self._assert_vol_exists(vol3['name'], True) + self.assertFalse(add_vdisk_qos.called) + self.driver.delete_volume(vol3) + + # If the qos is not empty, chvdisk should be called + # for create_volume_from_snapshot. + fake_opts['qos'] = {'IOThrottling': 5000} + get_vdisk_params.return_value = fake_opts + self.driver.create_cloned_volume(vol3, vol2) + self._assert_vol_exists(vol3['name'], True) + add_vdisk_qos.assert_called_once_with(vol3['name'], + fake_opts['qos']) + + # Delete in the 'opposite' order to make sure it works + self.driver.delete_volume(vol3) + self._assert_vol_exists(vol3['name'], False) + self.driver.delete_volume(vol2) + self._assert_vol_exists(vol2['name'], False) + self.driver.delete_snapshot(snap1) + self._assert_vol_exists(snap1['name'], False) + self.driver.delete_volume(vol1) + self._assert_vol_exists(vol1['name'], False) + + def test_instorage_mcs_delete_vol_with_lcmap(self): + vol1 = self._create_volume() + # create two snapshots + snap1 = self._generate_snapshot_info(vol1) + snap2 = self._generate_snapshot_info(vol1) + self.driver.create_snapshot(snap1) + self.driver.create_snapshot(snap2) + vol2 = self._generate_vol_info(None, None) + vol3 = self._generate_vol_info(None, None) + + # Create vol from the second snapshot + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_volume_from_snapshot(vol2, snap2) + # validate copyrate was set on the local copy + for i, lcmap in self.sim._lcmappings_list.items(): + if lcmap['target'] == vol2['name']: + self.assertEqual('copying', lcmap['status']) + self._assert_vol_exists(vol2['name'], True) + + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_cloned_volume(vol3, vol2) + + # validate copyrate was set on the local copy + for i, lcmap in self.sim._lcmappings_list.items(): + if lcmap['target'] == vol3['name']: + self.assertEqual('copying', lcmap['status']) + self._assert_vol_exists(vol3['name'], True) + + # Delete in the 'opposite' order to make sure it works + self.driver.delete_volume(vol3) + self._assert_vol_exists(vol3['name'], False) + self.driver.delete_volume(vol2) + self._assert_vol_exists(vol2['name'], False) + self.driver.delete_snapshot(snap2) + self._assert_vol_exists(snap2['name'], False) + self.driver.delete_snapshot(snap1) + self._assert_vol_exists(snap1['name'], False) + self.driver.delete_volume(vol1) + self._assert_vol_exists(vol1['name'], False) + + def test_instorage_mcs_volumes(self): + # Create a first volume + volume = self._generate_vol_info(None, None) + self.driver.create_volume(volume) + + self.driver.ensure_export(None, volume) + + # Do nothing + self.driver.create_export(None, volume, {}) + self.driver.remove_export(None, volume) + + # Make sure volume attributes are as they should be + attributes = self.driver._assistant.get_vdisk_attributes(volume[ + 'name']) + attr_size = float(attributes['capacity']) / units.Gi # bytes to GB + self.assertEqual(float(volume['size']), attr_size) + pool = fakes.get_test_pool() + self.assertEqual(pool, attributes['mdisk_grp_name']) + + # Try to create the volume again (should fail) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, + volume) + + # Try to delete a volume that doesn't exist (should not fail) + vol_no_exist = self._generate_vol_info('i_dont_exist', '111111') + self.driver.delete_volume(vol_no_exist) + # Ensure export for volume that doesn't exist (should not fail) + self.driver.ensure_export(None, vol_no_exist) + + # Delete the volume + self.driver.delete_volume(volume) + + def test_instorage_mcs_volume_name(self): + # Create a volume with space in name + volume = self._create_volume() + self.driver.ensure_export(None, volume) + + # Ensure lsvdisk can find the volume by name + attributes = self.driver._assistant.get_vdisk_attributes(volume.name) + self.assertIn('name', attributes) + self.assertEqual(volume.name, attributes['name']) + self.driver.delete_volume(volume) + + def test_instorage_mcs_volume_params(self): + # Option test matrix + # Option Value Covered by test # + # rsize -1 1 + # rsize 2 2,3 + # warning 0 2 + # warning 80 3 + # autoexpand True 2 + # autoexpand False 3 + # grainsize 32 2 + # grainsize 256 3 + # compression True 4 + # compression False 2,3 + # intier True 1,3 + # intier False 2 + # iogrp 0 1 + # iogrp 1 2 + + opts_list = [] + chck_list = [] + opts_list.append({'rsize': -1, 'intier': True, 'iogrp': '0'}) + chck_list.append({'free_capacity': '0', 'in_tier': 'on', + 'IO_group_id': '0'}) + + test_iogrp = '1' + opts_list.append({'rsize': 2, 'compression': False, 'warning': 0, + 'autoexpand': True, 'grainsize': 32, + 'intier': False, 'iogrp': test_iogrp}) + chck_list.append({'-free_capacity': '0', 'compressed_copy': 'no', + 'warning': '0', 'autoexpand': 'on', + 'grainsize': '32', 'in_tier': 'off', + 'IO_group_id': (test_iogrp)}) + opts_list.append({'rsize': 2, 'compression': False, 'warning': 80, + 'autoexpand': False, 'grainsize': 256, + 'intier': True}) + chck_list.append({'-free_capacity': '0', 'compressed_copy': 'no', + 'warning': '80', 'autoexpand': 'off', + 'grainsize': '256', 'in_tier': 'on'}) + opts_list.append({'rsize': 2, 'compression': True}) + chck_list.append({'-free_capacity': '0', + 'compressed_copy': 'yes'}) + + for idx in range(len(opts_list)): + attrs = self._create_test_vol(opts_list[idx]) + for k, v in chck_list[idx].items(): + try: + if k[0] == '-': + k = k[1:] + self.assertNotEqual(v, attrs[k]) + else: + self.assertEqual(v, attrs[k]) + except processutils.ProcessExecutionError as e: + if 'CMMVC7050E' not in e.stderr: + raise + + def test_instorage_mcs_unicode_host_and_volume_names(self): + # We'll check with iSCSI only - nothing protocol-dependent here + self.driver.do_setup(None) + + rand_id = 56789 + volume1 = self._generate_vol_info(None, None) + self.driver.create_volume(volume1) + self._assert_vol_exists(volume1['name'], True) + + self.assertRaises(exception.VolumeDriverException, + self.driver._assistant.create_host, + {'host': 12345}) + + # Add a host first to make life interesting (this host and + # conn['host'] should be translated to the same prefix, and the + # initiator should differentiate + tmpconn1 = {'initiator': u'unicode:initiator1.%s' % rand_id, + 'ip': '10.10.10.10', + 'host': u'unicode.foo}.bar{.baz-%s' % rand_id} + self.driver._assistant.create_host(tmpconn1) + + # Add a host with a different prefix + tmpconn2 = {'initiator': u'unicode:initiator2.%s' % rand_id, + 'ip': '10.10.10.11', + 'host': u'unicode.hello.world-%s' % rand_id} + self.driver._assistant.create_host(tmpconn2) + + conn = {'initiator': u'unicode:initiator3.%s' % rand_id, + 'ip': '10.10.10.12', + 'host': u'unicode.foo.bar.baz-%s' % rand_id} + self.driver.initialize_connection(volume1, conn) + host_name = self.driver._assistant.get_host_from_connector(conn) + self.assertIsNotNone(host_name) + self.driver.terminate_connection(volume1, conn) + host_name = self.driver._assistant.get_host_from_connector(conn) + self.assertIsNone(host_name) + self.driver.delete_volume(volume1) + + # Clean up temporary hosts + for tmpconn in [tmpconn1, tmpconn2]: + host_name = self.driver._assistant.get_host_from_connector(tmpconn) + self.assertIsNotNone(host_name) + self.driver._assistant.delete_host(host_name) + + def test_instorage_mcs_delete_volume_snapshots(self): + # Create a volume with two snapshots + master = self._create_volume() + + # Delete a snapshot + snap = self._generate_snapshot_info(master) + self.driver.create_snapshot(snap) + self._assert_vol_exists(snap['name'], True) + self.driver.delete_snapshot(snap) + self._assert_vol_exists(snap['name'], False) + + # Delete a volume with snapshots (regular) + snap = self._generate_snapshot_info(master) + self.driver.create_snapshot(snap) + self._assert_vol_exists(snap['name'], True) + self.driver.delete_volume(master) + self._assert_vol_exists(master['name'], False) + + # Fail create volume from snapshot - will force delete the volume + volfs = self._generate_vol_info(None, None) + self.sim.error_injection('startlcmap', 'bad_id') + self.sim.error_injection('lslcmap', 'speed_up') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + volfs, snap) + self._assert_vol_exists(volfs['name'], False) + + # Create volume from snapshot and delete it + volfs = self._generate_vol_info(None, None) + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_volume_from_snapshot(volfs, snap) + self._assert_vol_exists(volfs['name'], True) + self.driver.delete_volume(volfs) + self._assert_vol_exists(volfs['name'], False) + + # Create volume from snapshot and delete the snapshot + volfs = self._generate_vol_info(None, None) + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_volume_from_snapshot(volfs, snap) + self.driver.delete_snapshot(snap) + self._assert_vol_exists(snap['name'], False) + + # Fail create clone - will force delete the target volume + clone = self._generate_vol_info(None, None) + self.sim.error_injection('startlcmap', 'bad_id') + self.sim.error_injection('lslcmap', 'speed_up') + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, clone, volfs) + self._assert_vol_exists(clone['name'], False) + + # Create the clone, delete the source and target + clone = self._generate_vol_info(None, None) + self.sim.error_injection('lslcmap', 'speed_up') + self.driver.create_cloned_volume(clone, volfs) + self._assert_vol_exists(clone['name'], True) + self.driver.delete_volume(volfs) + self._assert_vol_exists(volfs['name'], False) + self.driver.delete_volume(clone) + self._assert_vol_exists(clone['name'], False) + + @ddt.data((True, None), (True, 5), (False, -1), (False, 100)) + @ddt.unpack + def test_instorage_mcs_get_volume_stats( + self, is_thin_provisioning_enabled, rsize): + self._set_flag('reserved_percentage', 25) + self._set_flag('instorage_mcs_vol_rsize', rsize) + stats = self.driver.get_volume_stats() + for each_pool in stats['pools']: + self.assertIn(each_pool['pool_name'], + self._def_flags['instorage_mcs_volpool_name']) + self.assertFalse(each_pool['multiattach']) + self.assertLessEqual(each_pool['free_capacity_gb'], + each_pool['total_capacity_gb']) + self.assertLessEqual(each_pool['allocated_capacity_gb'], + each_pool['total_capacity_gb']) + self.assertEqual(25, each_pool['reserved_percentage']) + self.assertEqual(is_thin_provisioning_enabled, + each_pool['thin_provisioning_support']) + self.assertEqual(not is_thin_provisioning_enabled, + each_pool['thick_provisioning_support']) + expected = 'instorage-mcs-sim' + self.assertEqual(expected, stats['volume_backend_name']) + for each_pool in stats['pools']: + self.assertIn(each_pool['pool_name'], + self._def_flags['instorage_mcs_volpool_name']) + self.assertAlmostEqual(3328.0, each_pool['total_capacity_gb']) + self.assertAlmostEqual(3287.5, each_pool['free_capacity_gb']) + self.assertAlmostEqual(25.0, + each_pool['allocated_capacity_gb']) + if is_thin_provisioning_enabled: + self.assertAlmostEqual( + 1576.96, each_pool['provisioned_capacity_gb']) + + def test_get_pool(self): + ctxt = testutils.get_test_admin_context() + type_ref = volume_types.create(ctxt, 'testtype', None) + volume = self._generate_vol_info(None, None) + volume.volume_type_id = type_ref['id'] + volume.volume_type = objects.VolumeType.get_by_id(ctxt, + type_ref['id']) + self.driver.create_volume(volume) + self.assertEqual(volume['mdisk_grp_name'], + self.driver.get_pool(volume)) + + self.driver.delete_volume(volume) + volume_types.destroy(ctxt, type_ref['id']) + + def test_instorage_mcs_extend_volume(self): + volume = self._create_volume() + self.driver.extend_volume(volume, '13') + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + vol_size = int(attrs['capacity']) / units.Gi + + self.assertAlmostEqual(vol_size, 13) + + snap = self._generate_snapshot_info(volume) + self.driver.create_snapshot(snap) + self._assert_vol_exists(snap['name'], True) + self.assertRaises(exception.VolumeDriverException, + self.driver.extend_volume, volume, '16') + + self.driver.delete_snapshot(snap) + self.driver.delete_volume(volume) + + @mock.patch.object(instorage_rep.InStorageMCSReplicationAsyncCopy, + 'create_relationship') + @mock.patch.object(instorage_rep.InStorageMCSReplicationAsyncCopy, + 'extend_target_volume') + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def _instorage_mcs_extend_volume_replication(self, + get_relationship, + delete_relationship, + extend_target_volume, + create_relationship): + fake_target = mock.Mock() + rep_type = 'async' + self.driver.replications[rep_type] = ( + self.driver.replication_factory(rep_type, fake_target)) + volume = self._create_volume() + volume['replication_status'] = 'enabled' + fake_target_vol = 'vol-target-id' + get_relationship.return_value = {'aux_vdisk_name': fake_target_vol} + with mock.patch.object( + self.driver, + '_get_volume_replicated_type_mirror') as mirror_type: + mirror_type.return_value = 'async' + self.driver.extend_volume(volume, '13') + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + vol_size = int(attrs['capacity']) / units.Gi + self.assertAlmostEqual(vol_size, 13) + delete_relationship.assert_called_once_with(volume['name']) + extend_target_volume.assert_called_once_with(fake_target_vol, + 12) + create_relationship.assert_called_once_with(volume, + fake_target_vol) + + self.driver.delete_volume(volume) + + def _instorage_mcs_extend_volume_replication_failover(self): + volume = self._create_volume() + volume['replication_status'] = 'failed-over' + with mock.patch.object( + self.driver, + '_get_volume_replicated_type_mirror') as mirror_type: + mirror_type.return_value = 'async' + self.driver.extend_volume(volume, '13') + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + vol_size = int(attrs['capacity']) / units.Gi + self.assertAlmostEqual(vol_size, 13) + + self.driver.delete_volume(volume) + + def _check_loc_info(self, capabilities, expected): + volume = self._create_volume() + host = {'host': 'foo', 'capabilities': capabilities} + ctxt = context.get_admin_context() + moved, model_update = self.driver.migrate_volume(ctxt, volume, host) + self.assertEqual(expected['moved'], moved) + self.assertEqual(expected['model_update'], model_update) + self.driver.delete_volume(volume) + + def test_instorage_mcs_migrate_bad_loc_info(self): + self._check_loc_info({}, {'moved': False, 'model_update': None}) + cap = {'location_info': 'foo'} + self._check_loc_info(cap, {'moved': False, 'model_update': None}) + cap = {'location_info': 'FooDriver:foo:bar'} + self._check_loc_info(cap, {'moved': False, 'model_update': None}) + cap = {'location_info': 'InStorageMCSDriver:foo:bar'} + self._check_loc_info(cap, {'moved': False, 'model_update': None}) + + def test_instorage_mcs_volume_migrate(self): + # Make sure we don't call migrate_volume_vdiskcopy + self.driver.do_setup(None) + loc = ('InStorageMCSDriver:' + self.driver._state['system_id'] + + ':openstack2') + cap = {'location_info': loc, 'extent_size': '256'} + host = {'host': 'openstack@mcs#openstack2', 'capabilities': cap} + ctxt = context.get_admin_context() + volume = self._create_volume() + volume['volume_type_id'] = None + self.driver.migrate_volume(ctxt, volume, host) + self._delete_volume(volume) + + def test_instorage_mcs_get_vdisk_params(self): + self.driver.do_setup(None) + fake_qos = {'qos:IOThrottling': '5000'} + expected_qos = {'IOThrottling': 5000} + fake_opts = self._get_default_opts() + # The parameters retured should be the same to the default options, + # if the QoS is empty. + vol_type_empty_qos = self._create_volume_type_qos(True, None) + type_id = vol_type_empty_qos['id'] + params = self.driver._get_vdisk_params(type_id, + volume_type=vol_type_empty_qos, + volume_metadata=None) + self.assertEqual(fake_opts, params) + volume_types.destroy(self.ctxt, type_id) + + # If the QoS is set via the qos association with the volume type, + # qos value should be set in the retured parameters. + vol_type_qos = self._create_volume_type_qos(False, fake_qos) + type_id = vol_type_qos['id'] + # If type_id is not none and volume_type is none, it should work fine. + params = self.driver._get_vdisk_params(type_id, volume_type=None, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If type_id is not none and volume_type is not none, it should + # work fine. + params = self.driver._get_vdisk_params(type_id, + volume_type=vol_type_qos, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If type_id is none and volume_type is not none, it should work fine. + params = self.driver._get_vdisk_params(None, volume_type=vol_type_qos, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If both type_id and volume_type are none, no qos will be returned + # in the parameter. + params = self.driver._get_vdisk_params(None, volume_type=None, + volume_metadata=None) + self.assertIsNone(params['qos']) + qos_spec = volume_types.get_volume_type_qos_specs(type_id) + volume_types.destroy(self.ctxt, type_id) + qos_specs.delete(self.ctxt, qos_spec['qos_specs']['id']) + + # If the QoS is set via the extra specs in the volume type, + # qos value should be set in the retured parameters. + vol_type_qos = self._create_volume_type_qos(True, fake_qos) + type_id = vol_type_qos['id'] + # If type_id is not none and volume_type is none, it should work fine. + params = self.driver._get_vdisk_params(type_id, volume_type=None, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If type_id is not none and volume_type is not none, + # it should work fine. + params = self.driver._get_vdisk_params(type_id, + volume_type=vol_type_qos, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If type_id is none and volume_type is not none, + # it should work fine. + params = self.driver._get_vdisk_params(None, + volume_type=vol_type_qos, + volume_metadata=None) + self.assertEqual(expected_qos, params['qos']) + # If both type_id and volume_type are none, no qos will be returned + # in the parameter. + params = self.driver._get_vdisk_params(None, volume_type=None, + volume_metadata=None) + self.assertIsNone(params['qos']) + volume_types.destroy(self.ctxt, type_id) + + # If the QoS is set in the volume metadata, + # qos value should be set in the retured parameters. + metadata = [{'key': 'qos:IOThrottling', 'value': 4000}] + expected_qos_metadata = {'IOThrottling': 4000} + params = self.driver._get_vdisk_params(None, volume_type=None, + volume_metadata=metadata) + self.assertEqual(expected_qos_metadata, params['qos']) + + # If the QoS is set both in the metadata and the volume type, the one + # in the volume type will take effect. + vol_type_qos = self._create_volume_type_qos(True, fake_qos) + type_id = vol_type_qos['id'] + params = self.driver._get_vdisk_params(type_id, volume_type=None, + volume_metadata=metadata) + self.assertEqual(expected_qos, params['qos']) + volume_types.destroy(self.ctxt, type_id) + + # If the QoS is set both via the qos association and the + # extra specs, the one from the qos association will take effect. + fake_qos_associate = {'qos:IOThrottling': '6000'} + expected_qos_associate = {'IOThrottling': 6000} + vol_type_qos = self._create_volume_type_qos_both(fake_qos, + fake_qos_associate) + type_id = vol_type_qos['id'] + params = self.driver._get_vdisk_params(type_id, volume_type=None, + volume_metadata=None) + self.assertEqual(expected_qos_associate, params['qos']) + qos_spec = volume_types.get_volume_type_qos_specs(type_id) + volume_types.destroy(self.ctxt, type_id) + qos_specs.delete(self.ctxt, qos_spec['qos_specs']['id']) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'disable_vdisk_qos') + @mock.patch.object(instorage_common.InStorageAssistant, + 'update_vdisk_qos') + def test_instorage_mcs_retype_no_copy(self, update_vdisk_qos, + disable_vdisk_qos): + self.driver.do_setup(None) + loc = ('InStorageMCSDriver:' + self.driver._state['system_id'] + + ':openstack') + cap = {'location_info': loc, 'extent_size': '128'} + self.driver._stats = {'location_info': loc} + host = {'host': 'openstack@mcs#openstack', 'capabilities': cap} + ctxt = context.get_admin_context() + + key_specs_old = {'intier': False, 'warning': 2, 'autoexpand': True} + key_specs_new = {'intier': True, 'warning': 5, 'autoexpand': False} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, _equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = self._generate_vol_info(None, None) + old_type = objects.VolumeType.get_by_id(ctxt, + old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host['host'] + new_type = objects.VolumeType.get_by_id(ctxt, + new_type_ref['id']) + + self.driver.create_volume(volume) + self.driver.retype(ctxt, volume, new_type, diff, host) + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + self.assertEqual('on', attrs['in_tier'], 'Volume retype failed') + self.assertEqual('5', attrs['warning'], 'Volume retype failed') + self.assertEqual('off', attrs['autoexpand'], 'Volume retype failed') + self.driver.delete_volume(volume) + + fake_opts = self._get_default_opts() + fake_opts_old = self._get_default_opts() + fake_opts_old['qos'] = {'IOThrottling': 4000} + fake_opts_qos = self._get_default_opts() + fake_opts_qos['qos'] = {'IOThrottling': 5000} + self.driver.create_volume(volume) + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for both the source and target volumes, + # add_vdisk_qos and disable_vdisk_qos will not be called for + # retype. + get_vdisk_params.side_effect = [fake_opts, fake_opts] + self.driver.retype(ctxt, volume, new_type, diff, host) + self.assertFalse(update_vdisk_qos.called) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is specified for both source and target volumes, + # add_vdisk_qos will be called for retype, and disable_vdisk_qos + # will not be called. + get_vdisk_params.side_effect = [fake_opts_old, fake_opts_qos] + self.driver.retype(ctxt, volume, new_type, diff, host) + update_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for source and speficied for target volume, + # add_vdisk_qos will be called for retype, and disable_vdisk_qos + # will not be called. + get_vdisk_params.side_effect = [fake_opts, fake_opts_qos] + self.driver.retype(ctxt, volume, new_type, diff, host) + update_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for target volume and specified for source + # volume, add_vdisk_qos will not be called for retype, and + # disable_vdisk_qos will be called. + get_vdisk_params.side_effect = [fake_opts_qos, fake_opts] + self.driver.retype(ctxt, volume, new_type, diff, host) + self.assertFalse(update_vdisk_qos.called) + disable_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.driver.delete_volume(volume) + + def test_instorage_mcs_retype_only_change_iogrp(self): + self.driver.do_setup(None) + loc = ('InStorageMCSDriver:' + self.driver._state['system_id'] + + ':openstack') + cap = {'location_info': loc, 'extent_size': '128'} + self.driver._stats = {'location_info': loc} + host = {'host': 'openstack@mcs#openstack', 'capabilities': cap} + ctxt = context.get_admin_context() + + key_specs_old = {'iogrp': 0} + key_specs_new = {'iogrp': 1} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, _equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = self._generate_vol_info(None, None) + old_type = objects.VolumeType.get_by_id(ctxt, + old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host['host'] + new_type = objects.VolumeType.get_by_id(ctxt, + new_type_ref['id']) + + self.driver.create_volume(volume) + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + self.assertEqual('0', attrs['IO_group_id'], 'Volume retype ' + 'failed') + self.driver.retype(ctxt, volume, new_type, diff, host) + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + self.assertEqual('1', attrs['IO_group_id'], 'Volume retype ' + 'failed') + self.driver.delete_volume(volume) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'disable_vdisk_qos') + @mock.patch.object(instorage_common.InStorageAssistant, + 'update_vdisk_qos') + def test_instorage_mcs_retype_need_copy(self, update_vdisk_qos, + disable_vdisk_qos): + self.driver.do_setup(None) + loc = ('InStorageMCSDriver:' + self.driver._state['system_id'] + + ':openstack') + cap = {'location_info': loc, 'extent_size': '128'} + self.driver._stats = {'location_info': loc} + host = {'host': 'openstack@mcs#openstack', 'capabilities': cap} + ctxt = context.get_admin_context() + + key_specs_old = {'compression': True, 'iogrp': 0} + key_specs_new = {'compression': False, 'iogrp': 1} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, _equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = self._generate_vol_info(None, None) + old_type = objects.VolumeType.get_by_id(ctxt, + old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host['host'] + new_type = objects.VolumeType.get_by_id(ctxt, + new_type_ref['id']) + + self.driver.create_volume(volume) + self.driver.retype(ctxt, volume, new_type, diff, host) + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + self.assertEqual('no', attrs['compressed_copy']) + self.assertEqual('1', attrs['IO_group_id'], 'Volume retype ' + 'failed') + self.driver.delete_volume(volume) + + fake_opts = self._get_default_opts() + fake_opts_old = self._get_default_opts() + fake_opts_old['qos'] = {'IOThrottling': 4000} + fake_opts_qos = self._get_default_opts() + fake_opts_qos['qos'] = {'IOThrottling': 5000} + self.driver.create_volume(volume) + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for both the source and target volumes, + # add_vdisk_qos and disable_vdisk_qos will not be called for + # retype. + get_vdisk_params.side_effect = [fake_opts, fake_opts] + self.driver.retype(ctxt, volume, new_type, diff, host) + self.assertFalse(update_vdisk_qos.called) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is specified for both source and target volumes, + # add_vdisk_qos will be called for retype, and disable_vdisk_qos + # will not be called. + get_vdisk_params.side_effect = [fake_opts_old, fake_opts_qos] + self.driver.retype(ctxt, volume, new_type, diff, host) + update_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for source and speficied for target volume, + # add_vdisk_qos will be called for retype, and disable_vdisk_qos + # will not be called. + get_vdisk_params.side_effect = [fake_opts, fake_opts_qos] + self.driver.retype(ctxt, volume, new_type, diff, host) + update_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.assertFalse(disable_vdisk_qos.called) + self.driver.delete_volume(volume) + + self.driver.create_volume(volume) + update_vdisk_qos.reset_mock() + with mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_get_vdisk_params') as get_vdisk_params: + # If qos is empty for target volume and specified for source + # volume, add_vdisk_qos will not be called for retype, and + # disable_vdisk_qos will be called. + get_vdisk_params.side_effect = [fake_opts_qos, fake_opts] + self.driver.retype(ctxt, volume, new_type, diff, host) + self.assertFalse(update_vdisk_qos.called) + disable_vdisk_qos.assert_called_with(volume['name'], + fake_opts_qos['qos']) + self.driver.delete_volume(volume) + + def test_set_storage_code_level_success(self): + res = self.driver._assistant.get_system_info() + self.assertEqual((3, 1, 1, 0), res['code_level'], + 'Get code level error') + + @mock.patch.object(instorage_common.InStorageAssistant, 'rename_vdisk') + def test_instorage_update_migrated_volume(self, rename_vdisk): + ctxt = testutils.get_test_admin_context() + backend_volume = self._create_volume() + volume = self._create_volume() + model_update = self.driver.update_migrated_volume(ctxt, volume, + backend_volume, + 'available') + rename_vdisk.assert_called_once_with(backend_volume.name, volume.name) + self.assertEqual({'_name_id': None}, model_update) + + rename_vdisk.reset_mock() + rename_vdisk.side_effect = exception.VolumeBackendAPIException + model_update = self.driver.update_migrated_volume(ctxt, volume, + backend_volume, + 'available') + self.assertEqual({'_name_id': backend_volume.id}, model_update) + + rename_vdisk.reset_mock() + rename_vdisk.side_effect = exception.VolumeBackendAPIException + model_update = self.driver.update_migrated_volume(ctxt, volume, + backend_volume, + 'attached') + self.assertEqual({'_name_id': backend_volume.id}, model_update) + + def test_instorage_vdisk_copy_ops(self): + ctxt = testutils.get_test_admin_context() + volume = self._create_volume() + driver = self.driver + dest_pool = volume_utils.extract_host(volume['host'], 'pool') + new_ops = driver._assistant.add_vdisk_copy(volume['name'], dest_pool, + None, self.driver._state, + self.driver.configuration) + self.driver._add_vdisk_copy_op(ctxt, volume, new_ops) + self.assertEqual([new_ops], + self.driver._vdiskcopyops[volume.id]['copyops'], + 'InStorage driver add vdisk copy error.') + self.driver._check_volume_copy_ops() + self.driver._rm_vdisk_copy_op(ctxt, volume.id, new_ops[0], new_ops[1]) + self.assertNotIn(volume.id, self.driver._vdiskcopyops, + 'InStorage driver delete vdisk copy error') + self._delete_volume(volume) + + def test_instorage_delete_with_vdisk_copy_ops(self): + volume = self._create_volume() + self.driver._vdiskcopyops = {volume['id']: {'name': volume.name, + 'copyops': [('0', '1')]}} + with mock.patch.object(self.driver, '_vdiskcopyops_loop'): + self.assertIn(volume['id'], self.driver._vdiskcopyops) + self.driver.delete_volume(volume) + self.assertNotIn(volume['id'], self.driver._vdiskcopyops) + + def _create_volume_type_qos(self, extra_specs, fake_qos): + # Generate a QoS volume type for volume. + if extra_specs: + spec = fake_qos + type_ref = volume_types.create(self.ctxt, "qos_extra_specs", spec) + else: + type_ref = volume_types.create(self.ctxt, "qos_associate", None) + if fake_qos: + qos_ref = qos_specs.create(self.ctxt, 'qos-specs', fake_qos) + qos_specs.associate_qos_with_type(self.ctxt, qos_ref['id'], + type_ref['id']) + + qos_type = volume_types.get_volume_type(self.ctxt, type_ref['id']) + return qos_type + + def _create_volume_type_qos_both(self, fake_qos, fake_qos_associate): + type_ref = volume_types.create(self.ctxt, "qos_extra_specs", fake_qos) + qos_ref = qos_specs.create(self.ctxt, 'qos-specs', fake_qos_associate) + qos_specs.associate_qos_with_type(self.ctxt, qos_ref['id'], + type_ref['id']) + qos_type = volume_types.get_volume_type(self.ctxt, type_ref['id']) + return qos_type + + def _create_replication_volume_type(self, enable): + # Generate a volume type for volume repliation. + if enable: + spec = {'capabilities:replication': ' True'} + type_ref = volume_types.create(self.ctxt, "replication_1", spec) + else: + spec = {'capabilities:replication': ' False'} + type_ref = volume_types.create(self.ctxt, "replication_2", spec) + + replication_type = objects.VolumeType.get_by_id(self.ctxt, + type_ref['id']) + return replication_type + + def _create_consistency_group_volume_type(self): + # Generate a volume type for volume consistencygroup. + spec = {'capabilities:consistencygroup_support': ' True'} + type_ref = volume_types.create(self.ctxt, "cg", spec) + + cg_type = volume_types.get_volume_type(self.ctxt, type_ref['id']) + + return cg_type + + def _create_group_volume_type(self): + # Generate a volume type for volume group. + spec = {'capabilities:group_support': ' True'} + type_ref = volume_types.create(self.ctxt, "group", spec) + + group_type = volume_types.get_volume_type(self.ctxt, type_ref['id']) + + return group_type + + def _get_vdisk_uid(self, vdisk_name): + """Return vdisk_UID for given vdisk. + + Given a vdisk by name, performs an lvdisk command that extracts + the vdisk_UID parameter and returns it. + Returns None if the specified vdisk does not exist. + """ + vdisk_properties, _err = self.sim._cmd_lsvdisk(obj=vdisk_name, + delim='!') + + # Iterate through each row until we find the vdisk_UID entry + for row in vdisk_properties.split('\n'): + words = row.split('!') + if words[0] == 'vdisk_UID': + return words[1] + return None + + def _create_volume_and_return_uid(self, volume_name): + """Creates a volume and returns its UID. + + Creates a volume with the specified name, and returns the UID that + the InStorage controller allocated for it. We do this by executing a + create_volume and then calling into the simulator to perform an + lsvdisk directly. + """ + volume = self._generate_vol_info(None, None) + self.driver.create_volume(volume) + + return (volume, self._get_vdisk_uid(volume['name'])) + + def test_manage_existing_get_size_bad_ref(self): + """Error on manage with bad reference. + + This test case attempts to manage an existing volume but passes in + a bad reference that the InStorage driver doesn't understand. We + expect an exception to be raised. + """ + volume = self._generate_vol_info(None, None) + ref = {} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, volume, ref) + + def test_manage_existing_get_size_bad_uid(self): + """Error when the specified UUID does not exist.""" + volume = self._generate_vol_info(None, None) + ref = {'source-id': 'bad_uid'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, volume, ref) + pass + + def test_manage_existing_get_size_bad_name(self): + """Error when the specified name does not exist.""" + volume = self._generate_vol_info(None, None) + ref = {'source-name': 'bad_name'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, volume, ref) + + def test_manage_existing_bad_ref(self): + """Error on manage with bad reference. + + This test case attempts to manage an existing volume but passes in + a bad reference that the InStorage driver doesn't understand. We + expect an exception to be raised. + """ + + # Error when neither UUID nor name are specified. + volume = self._generate_vol_info(None, None) + ref = {} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, volume, ref) + + # Error when the specified UUID does not exist. + volume = self._generate_vol_info(None, None) + ref = {'source-id': 'bad_uid'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, volume, ref) + + # Error when the specified name does not exist. + volume = self._generate_vol_info(None, None) + ref = {'source-name': 'bad_name'} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing, volume, ref) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_vdisk_copy_attrs') + def test_manage_existing_mismatch(self, + get_vdisk_copy_attrs): + ctxt = testutils.get_test_admin_context() + _volume, uid = self._create_volume_and_return_uid('manage_test') + + opts = {'rsize': -1} + type_thick_ref = volume_types.create(ctxt, 'testtype1', opts) + + opts = {'rsize': 2} + type_thin_ref = volume_types.create(ctxt, 'testtype2', opts) + + opts = {'rsize': 2, 'compression': True} + type_comp_ref = volume_types.create(ctxt, 'testtype3', opts) + + opts = {'rsize': -1, 'iogrp': 1} + type_iogrp_ref = volume_types.create(ctxt, 'testtype4', opts) + + new_volume = self._generate_vol_info(None, None) + ref = {'source-name': _volume['name']} + + fake_copy_thin = self._get_default_opts() + fake_copy_thin['autoexpand'] = 'on' + + fake_copy_comp = self._get_default_opts() + fake_copy_comp['autoexpand'] = 'on' + fake_copy_comp['compressed_copy'] = 'yes' + + fake_copy_thick = self._get_default_opts() + fake_copy_thick['autoexpand'] = '' + fake_copy_thick['compressed_copy'] = 'no' + + fake_copy_no_comp = self._get_default_opts() + fake_copy_no_comp['compressed_copy'] = 'no' + + valid_iogrp = self.driver._state['available_iogrps'] + self.driver._state['available_iogrps'] = [9999] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + self.driver._state['available_iogrps'] = valid_iogrp + + get_vdisk_copy_attrs.side_effect = [fake_copy_thin, + fake_copy_thick, + fake_copy_no_comp, + fake_copy_comp, + fake_copy_thick, + fake_copy_thick + ] + new_volume['volume_type_id'] = type_thick_ref['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + new_volume['volume_type_id'] = type_thin_ref['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + new_volume['volume_type_id'] = type_comp_ref['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + new_volume['volume_type_id'] = type_thin_ref['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + new_volume['volume_type_id'] = type_iogrp_ref['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + new_volume['volume_type_id'] = type_thick_ref['id'] + no_exist_pool = 'i-dont-exist-%s' % 56789 + new_volume['host'] = 'openstack@mcs#%s' % no_exist_pool + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + self._reset_flags() + volume_types.destroy(ctxt, type_thick_ref['id']) + volume_types.destroy(ctxt, type_comp_ref['id']) + volume_types.destroy(ctxt, type_iogrp_ref['id']) + + def test_manage_existing_good_uid_not_mapped(self): + """Tests managing a volume with no mappings. + + This test case attempts to manage an existing volume by UID, and + we expect it to succeed. We verify that the backend volume was + renamed to have the name of the Cinder volume that we asked for it to + be associated with. + """ + + # Create a volume as a way of getting a vdisk created, and find out the + # UID of that vdisk. + _volume, uid = self._create_volume_and_return_uid('manage_test') + + # Descriptor of the Cinder volume that we want to own the vdisk + # referenced by uid. + new_volume = self._generate_vol_info(None, None) + + # Submit the request to manage it. + ref = {'source-id': uid} + size = self.driver.manage_existing_get_size(new_volume, ref) + self.assertEqual(10, size) + self.driver.manage_existing(new_volume, ref) + + # Assert that there is a disk named after the new volume that has the + # ID that we passed in, indicating that the disk has been renamed. + uid_of_new_volume = self._get_vdisk_uid(new_volume['name']) + self.assertEqual(uid, uid_of_new_volume) + + def test_manage_existing_good_name_not_mapped(self): + """Tests managing a volume with no mappings. + + This test case attempts to manage an existing volume by name, and + we expect it to succeed. We verify that the backend volume was + renamed to have the name of the Cinder volume that we asked for it to + be associated with. + """ + + # Create a volume as a way of getting a vdisk created, and find out the + # UID of that vdisk. + _volume, uid = self._create_volume_and_return_uid('manage_test') + + # Descriptor of the Cinder volume that we want to own the vdisk + # referenced by uid. + new_volume = self._generate_vol_info(None, None) + + # Submit the request to manage it. + ref = {'source-name': _volume['name']} + size = self.driver.manage_existing_get_size(new_volume, ref) + self.assertEqual(10, size) + self.driver.manage_existing(new_volume, ref) + + # Assert that there is a disk named after the new volume that has the + # ID that we passed in, indicating that the disk has been renamed. + uid_of_new_volume = self._get_vdisk_uid(new_volume['name']) + self.assertEqual(uid, uid_of_new_volume) + + def test_manage_existing_mapped(self): + """Tests managing a mapped volume with no override. + + This test case attempts to manage an existing volume by UID, but + the volume is mapped to a host, so we expect to see an exception + raised. + """ + # Create a volume as a way of getting a vdisk created, and find out the + # UUID of that vdisk. + # Set replication target. + volume, uid = self._create_volume_and_return_uid('manage_test') + + # Map a host to the disk + conn = {'initiator': u'unicode:initiator3', + 'ip': '10.10.10.12', + 'host': u'unicode.foo.bar.baz'} + self.driver.initialize_connection(volume, conn) + + # Descriptor of the Cinder volume that we want to own the vdisk + # referenced by uid. + volume = self._generate_vol_info(None, None) + ref = {'source-id': uid} + + # Attempt to manage this disk, and except an exception beause the + # volume is already mapped. + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, volume, ref) + + ref = {'source-name': volume['name']} + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, volume, ref) + + def test_manage_existing_good_uid_mapped_with_override(self): + """Tests managing a mapped volume with override. + + This test case attempts to manage an existing volume by UID, when it + already mapped to a host, but the ref specifies that this is OK. + We verify that the backend volume was renamed to have the name of the + Cinder volume that we asked for it to be associated with. + """ + # Create a volume as a way of getting a vdisk created, and find out the + # UUID of that vdisk. + volume, uid = self._create_volume_and_return_uid('manage_test') + + # Map a host to the disk + conn = {'initiator': u'unicode:initiator3', + 'ip': '10.10.10.12', + 'host': u'unicode.foo.bar.baz'} + self.driver.initialize_connection(volume, conn) + + # Descriptor of the Cinder volume that we want to own the vdisk + # referenced by uid. + new_volume = self._generate_vol_info(None, None) + + # Submit the request to manage it, specifying that it is OK to + # manage a volume that is already attached. + ref = {'source-id': uid, 'manage_if_in_use': True} + size = self.driver.manage_existing_get_size(new_volume, ref) + self.assertEqual(10, size) + self.driver.manage_existing(new_volume, ref) + + # Assert that there is a disk named after the new volume that has the + # ID that we passed in, indicating that the disk has been renamed. + uid_of_new_volume = self._get_vdisk_uid(new_volume['name']) + self.assertEqual(uid, uid_of_new_volume) + + def test_manage_existing_good_name_mapped_with_override(self): + """Tests managing a mapped volume with override. + + This test case attempts to manage an existing volume by name, when it + already mapped to a host, but the ref specifies that this is OK. + We verify that the backend volume was renamed to have the name of the + Cinder volume that we asked for it to be associated with. + """ + # Create a volume as a way of getting a vdisk created, and find out the + # UUID of that vdisk. + volume, uid = self._create_volume_and_return_uid('manage_test') + + # Map a host to the disk + conn = {'initiator': u'unicode:initiator3', + 'ip': '10.10.10.12', + 'host': u'unicode.foo.bar.baz'} + self.driver.initialize_connection(volume, conn) + + # Descriptor of the Cinder volume that we want to own the vdisk + # referenced by uid. + new_volume = self._generate_vol_info(None, None) + + # Submit the request to manage it, specifying that it is OK to + # manage a volume that is already attached. + ref = {'source-name': volume['name'], 'manage_if_in_use': True} + size = self.driver.manage_existing_get_size(new_volume, ref) + self.assertEqual(10, size) + self.driver.manage_existing(new_volume, ref) + + # Assert that there is a disk named after the new volume that has the + # ID that we passed in, indicating that the disk has been renamed. + uid_of_new_volume = self._get_vdisk_uid(new_volume['name']) + self.assertEqual(uid, uid_of_new_volume) diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/test_helper_routines.py b/cinder/tests/unit/volume/drivers/inspur/instorage/test_helper_routines.py new file mode 100644 index 00000000000..0e7097ff85a --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/test_helper_routines.py @@ -0,0 +1,256 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +import ddt +import mock + +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.inspur.instorage import instorage_common + +from cinder.tests.unit.volume.drivers.inspur.instorage import fakes + + +class CLIParserTestCase(test.TestCase): + + def test_empty(self): + self.assertEqual(0, len( + instorage_common.CLIParser(''))) + self.assertEqual(0, len( + instorage_common.CLIParser(('', 'stderr')))) + + def test_header(self): + raw = r'''id!name +1!node1 +2!node2 +''' + resp = instorage_common.CLIParser(raw, with_header=True) + self.assertEqual(2, len(resp)) + self.assertEqual('1', resp[0]['id']) + self.assertEqual('2', resp[1]['id']) + + def test_select(self): + raw = r'''id!123 +name!Bill +name!Bill2 +age!30 +home address!s1 +home address!s2 + +id! 7 +name!John +name!John2 +age!40 +home address!s3 +home address!s4 +''' + resp = instorage_common.CLIParser(raw, with_header=False) + self.assertEqual([('s1', 'Bill', 's1'), ('s2', 'Bill2', 's2'), + ('s3', 'John', 's3'), ('s4', 'John2', 's4')], + list(resp.select('home address', 'name', + 'home address'))) + + def test_lsnode_all(self): + raw = r'''id!name!UPS_serial_number!WWNN!status +1!node1!!500507680200C744!online +2!node2!!500507680200C745!online +''' + resp = instorage_common.CLIParser(raw) + self.assertEqual(2, len(resp)) + self.assertEqual('1', resp[0]['id']) + self.assertEqual('500507680200C744', resp[0]['WWNN']) + self.assertEqual('2', resp[1]['id']) + self.assertEqual('500507680200C745', resp[1]['WWNN']) + + def test_lsnode_single(self): + raw = r'''id!1 +port_id!500507680210C744 +port_status!active +port_speed!8Gb +port_id!500507680240C744 +port_status!inactive +port_speed!8Gb +''' + resp = instorage_common.CLIParser(raw, with_header=False) + self.assertEqual(1, len(resp)) + self.assertEqual('1', resp[0]['id']) + self.assertEqual([('500507680210C744', 'active'), + ('500507680240C744', 'inactive')], + list(resp.select('port_id', 'port_status'))) + + +class InStorageAssistantTestCase(test.TestCase): + + def setUp(self): + super(InStorageAssistantTestCase, self).setUp() + self.instorage_mcs_common = instorage_common.InStorageAssistant(None) + self.mock_wait_time = mock.patch.object( + instorage_common.InStorageAssistant, "WAIT_TIME", 0) + + @mock.patch.object(instorage_common.InStorageSSH, 'lslicense') + @mock.patch.object(instorage_common.InStorageSSH, 'lsguicapabilities') + def test_compression_enabled(self, lsguicapabilities, lslicense): + fake_license_without_keys = {} + fake_license = { + 'license_compression_enclosures': '1', + 'license_compression_capacity': '1' + } + fake_license_scheme = { + 'compression': 'yes' + } + fake_license_invalid_scheme = { + 'compression': 'no' + } + + lslicense.side_effect = [fake_license_without_keys, + fake_license_without_keys, + fake_license, + fake_license_without_keys] + lsguicapabilities.side_effect = [fake_license_without_keys, + fake_license_invalid_scheme, + fake_license_scheme] + self.assertFalse(self.instorage_mcs_common.compression_enabled()) + + self.assertFalse(self.instorage_mcs_common.compression_enabled()) + + self.assertTrue(self.instorage_mcs_common.compression_enabled()) + + self.assertTrue(self.instorage_mcs_common.compression_enabled()) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_vdisk_count_by_io_group') + def test_select_io_group(self, get_vdisk_count_by_io_group): + # given io groups + opts = {} + # system io groups + state = {} + + fake_iog_vdc1 = {0: 100, 1: 50, 2: 50, 3: 300} + fake_iog_vdc2 = {0: 2, 1: 1, 2: 200} + fake_iog_vdc3 = {0: 2, 2: 200} + fake_iog_vdc4 = {0: 100, 1: 100, 2: 100, 3: 100} + fake_iog_vdc5 = {0: 10, 1: 1, 2: 200, 3: 300} + + get_vdisk_count_by_io_group.side_effect = [fake_iog_vdc1, + fake_iog_vdc2, + fake_iog_vdc3, + fake_iog_vdc4, + fake_iog_vdc5] + opts['iogrp'] = '0,2' + state['available_iogrps'] = [0, 1, 2, 3] + + iog = self.instorage_mcs_common.select_io_group(state, opts) + self.assertTrue(iog in state['available_iogrps']) + self.assertEqual(2, iog) + + opts['iogrp'] = '0' + state['available_iogrps'] = [0, 1, 2] + + iog = self.instorage_mcs_common.select_io_group(state, opts) + self.assertTrue(iog in state['available_iogrps']) + self.assertEqual(0, iog) + + opts['iogrp'] = '1,2' + state['available_iogrps'] = [0, 2] + + iog = self.instorage_mcs_common.select_io_group(state, opts) + self.assertTrue(iog in state['available_iogrps']) + self.assertEqual(2, iog) + + opts['iogrp'] = ' 0, 1, 2 ' + state['available_iogrps'] = [0, 1, 2, 3] + + iog = self.instorage_mcs_common.select_io_group(state, opts) + self.assertTrue(iog in state['available_iogrps']) + # since vdisk count in all iogroups is same, it will pick the first + self.assertEqual(0, iog) + + opts['iogrp'] = '0,1,2, 3' + state['available_iogrps'] = [0, 1, 2, 3] + + iog = self.instorage_mcs_common.select_io_group(state, opts) + self.assertTrue(iog in state['available_iogrps']) + self.assertEqual(1, iog) + + +@ddt.ddt +class InStorageSSHTestCase(test.TestCase): + + def setUp(self): + super(InStorageSSHTestCase, self).setUp() + self.fake_driver = fakes.FakeInStorageMCSISCSIDriver( + configuration=conf.Configuration(None)) + sim = fakes.FakeInStorage(['openstack']) + self.fake_driver.set_fake_storage(sim) + self.instorage_ssh = instorage_common.InStorageSSH( + self.fake_driver._run_ssh) + + def test_mkvdiskhostmap(self): + # mkvdiskhostmap should not be returning anything + self.fake_driver.fake_storage._volumes_list['9999'] = { + 'name': ' 9999', 'id': '0', 'uid': '0', + 'IO_group_id': '0', 'IO_group_name': 'fakepool'} + self.fake_driver.fake_storage._hosts_list['HOST1'] = { + 'name': 'HOST1', 'id': '0', 'host_name': 'HOST1'} + self.fake_driver.fake_storage._hosts_list['HOST2'] = { + 'name': 'HOST2', 'id': '1', 'host_name': 'HOST2'} + self.fake_driver.fake_storage._hosts_list['HOST3'] = { + 'name': 'HOST3', 'id': '2', 'host_name': 'HOST3'} + + ret = self.instorage_ssh.mkvdiskhostmap('HOST1', '9999', '511', False) + self.assertEqual('511', ret) + + ret = self.instorage_ssh.mkvdiskhostmap('HOST2', '9999', '512', True) + self.assertEqual('512', ret) + + ret = self.instorage_ssh.mkvdiskhostmap('HOST3', '9999', None, True) + self.assertIsNotNone(ret) + + with mock.patch.object( + instorage_common.InStorageSSH, + 'run_ssh_check_created') as run_ssh_check_created: + ex = exception.VolumeBackendAPIException(data='CMMVC6071E') + run_ssh_check_created.side_effect = ex + self.assertRaises(exception.VolumeBackendAPIException, + self.instorage_ssh.mkvdiskhostmap, + 'HOST3', '9999', 511, True) + + @ddt.data((exception.VolumeBackendAPIException(data='CMMVC6372W'), None), + (exception.VolumeBackendAPIException(data='CMMVC6372W'), + {'name': 'fakevol', 'id': '0', 'uid': '0', 'IO_group_id': '0', + 'IO_group_name': 'fakepool'}), + (exception.VolumeBackendAPIException(data='error'), None)) + @ddt.unpack + def test_mkvdisk_with_warning(self, run_ssh_check, lsvol): + opt = {'iogrp': 0} + with mock.patch.object(instorage_common.InStorageSSH, + 'run_ssh_check_created', + side_effect=run_ssh_check): + with mock.patch.object(instorage_common.InStorageSSH, 'lsvdisk', + return_value=lsvol): + if lsvol: + ret = self.instorage_ssh.mkvdisk('fakevol', '1', 'gb', + 'fakepool', opt, []) + self.assertEqual('0', ret) + else: + self.assertRaises(exception.VolumeBackendAPIException, + self.instorage_ssh.mkvdisk, + 'fakevol', '1', 'gb', 'fakepool', + opt, []) diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/test_iscsi_driver.py b/cinder/tests/unit/volume/drivers/inspur/instorage/test_iscsi_driver.py new file mode 100644 index 00000000000..c5784dd4752 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/test_iscsi_driver.py @@ -0,0 +1,430 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +from eventlet import greenthread +import mock +from oslo_utils import importutils +import six + +from cinder import context +from cinder import exception +from cinder import test +from cinder.tests.unit import utils as testutils +from cinder.volume import configuration as conf +from cinder.volume.drivers.inspur.instorage import instorage_iscsi +from cinder.volume import volume_types + +from cinder.tests.unit.volume.drivers.inspur.instorage import fakes + + +class InStorageMCSISCSIDriverTestCase(test.TestCase): + + @mock.patch.object(greenthread, 'sleep') + def setUp(self, mock_sleep): + super(InStorageMCSISCSIDriverTestCase, self).setUp() + self.iscsi_driver = fakes.FakeInStorageMCSISCSIDriver( + configuration=conf.Configuration(None)) + self._def_flags = {'san_ip': 'hostname', + 'san_login': 'user', + 'san_password': 'pass', + 'instorage_mcs_volpool_name': ['openstack'], + 'instorage_mcs_localcopy_timeout': 20, + 'instorage_mcs_localcopy_rate': 49, + 'instorage_mcs_allow_tenant_qos': True} + wwpns = ['1234567890123456', '6543210987654321'] + initiator = 'test.initiator.%s' % 123456 + self._connector = {'ip': '1.234.56.78', + 'host': 'instorage-mcs-test', + 'wwpns': wwpns, + 'initiator': initiator} + self.sim = fakes.FakeInStorage(['openstack']) + + self.iscsi_driver.set_fake_storage(self.sim) + self.ctxt = context.get_admin_context() + + self._reset_flags() + self.ctxt = context.get_admin_context() + db_driver = self.iscsi_driver.configuration.db_driver + self.db = importutils.import_module(db_driver) + self.iscsi_driver.db = self.db + self.iscsi_driver.do_setup(None) + self.iscsi_driver.check_for_setup_error() + self.iscsi_driver._assistant.check_lcmapping_interval = 0 + + def _set_flag(self, flag, value): + group = self.iscsi_driver.configuration.config_group + self.iscsi_driver.configuration.set_override(flag, value, group) + + def _reset_flags(self): + self.iscsi_driver.configuration.local_conf.reset() + for k, v in self._def_flags.items(): + self._set_flag(k, v) + + def _create_volume(self, **kwargs): + pool = fakes.get_test_pool() + prop = {'host': 'openstack@mcs#%s' % pool, + 'size': 1} + for p in prop.keys(): + if p not in kwargs: + kwargs[p] = prop[p] + vol = testutils.create_volume(self.ctxt, **kwargs) + self.iscsi_driver.create_volume(vol) + return vol + + def _delete_volume(self, volume): + self.iscsi_driver.delete_volume(volume) + self.db.volume_destroy(self.ctxt, volume['id']) + + def _generate_vol_info(self, vol_name, vol_id): + pool = fakes.get_test_pool() + prop = {'mdisk_grp_name': pool} + if vol_name: + prop.update(volume_name=vol_name, + volume_id=vol_id, + volume_size=10) + else: + prop.update(size=10, + volume_type_id=None, + mdisk_grp_name=pool, + host='openstack@mcs#%s' % pool) + vol = testutils.create_volume(self.ctxt, **prop) + return vol + + def _assert_vol_exists(self, name, exists): + is_vol_defined = self.iscsi_driver._assistant.is_vdisk_defined(name) + self.assertEqual(exists, is_vol_defined) + + def test_instorage_mcs_iscsi_validate_connector(self): + conn_neither = {'host': 'host'} + conn_iscsi = {'host': 'host', 'initiator': 'foo'} + conn_fc = {'host': 'host', 'wwpns': 'bar'} + conn_both = {'host': 'host', 'initiator': 'foo', 'wwpns': 'bar'} + + self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI']) + self.iscsi_driver.validate_connector(conn_iscsi) + self.iscsi_driver.validate_connector(conn_both) + self.assertRaises(exception.InvalidConnectorException, + self.iscsi_driver.validate_connector, conn_fc) + self.assertRaises(exception.InvalidConnectorException, + self.iscsi_driver.validate_connector, conn_neither) + + self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI', 'FC']) + self.iscsi_driver.validate_connector(conn_iscsi) + self.iscsi_driver.validate_connector(conn_both) + self.assertRaises(exception.InvalidConnectorException, + self.iscsi_driver.validate_connector, conn_neither) + + def test_instorage_terminate_iscsi_connection(self): + # create a iSCSI volume + volume_iSCSI = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' iSCSI'} + vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec) + volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id'] + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + + self.iscsi_driver.initialize_connection(volume_iSCSI, connector) + self.iscsi_driver.terminate_connection(volume_iSCSI, connector) + + @mock.patch.object(instorage_iscsi.InStorageMCSISCSIDriver, + '_do_terminate_connection') + def test_instorage_initialize_iscsi_connection_failure(self, term_conn): + # create a iSCSI volume + volume_iSCSI = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' iSCSI'} + vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec) + volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id'] + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + + self.iscsi_driver._state['storage_nodes'] = {} + self.assertRaises(exception.VolumeBackendAPIException, + self.iscsi_driver.initialize_connection, + volume_iSCSI, connector) + term_conn.assert_called_once_with(volume_iSCSI, connector) + + def test_instorage_initialize_iscsi_connection_single_path(self): + # Test the return value for _get_iscsi_properties + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa'} + # Expected single path host-volume map return value + exp_s_path = {'driver_volume_type': 'iscsi', + 'data': {'target_discovered': False, + 'target_iqn': + 'iqn.1982-01.com.inspur:1234.sim.node1', + 'target_portal': '1.234.56.78:3260', + 'target_lun': 0, + 'auth_method': 'CHAP', + 'discovery_auth_method': 'CHAP'}} + + volume_iSCSI = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' iSCSI'} + vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec) + volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id'] + + # Make sure that the volumes have been created + self._assert_vol_exists(volume_iSCSI['name'], True) + + # Check case where no hosts exist + ret = self.iscsi_driver._assistant.get_host_from_connector( + connector) + self.assertIsNone(ret) + + # Initialize connection to map volume to a host + ret = self.iscsi_driver.initialize_connection( + volume_iSCSI, connector) + self.assertEqual(exp_s_path['driver_volume_type'], + ret['driver_volume_type']) + + # Check the single path host-volume map return value + for k, v in exp_s_path['data'].items(): + self.assertEqual(v, ret['data'][k]) + + ret = self.iscsi_driver._assistant.get_host_from_connector( + connector) + self.assertIsNotNone(ret) + + def test_instorage_initialize_iscsi_connection_multipath(self): + # Test the return value for _get_iscsi_properties + + connector = {'host': 'instorage-mcs-host', + 'wwnns': ['20000090fa17311e', '20000090fa17311f'], + 'wwpns': ['ff00000000000000', 'ff00000000000001'], + 'initiator': 'iqn.1993-08.org.debian:01:eac5ccc1aaa', + 'multipath': True} + + # Expected multipath host-volume map return value + exp_m_path = {'driver_volume_type': 'iscsi', + 'data': {'target_discovered': False, + 'target_iqn': + 'iqn.1982-01.com.inspur:1234.sim.node1', + 'target_portal': '1.234.56.78:3260', + 'target_lun': 0, + 'target_iqns': [ + 'iqn.1982-01.com.inspur:1234.sim.node1', + 'iqn.1982-01.com.inspur:1234.sim.node1', + 'iqn.1982-01.com.inspur:1234.sim.node2'], + 'target_portals': + ['1.234.56.78:3260', + '1.234.56.80:3260', + '1.234.56.79:3260'], + 'target_luns': [0, 0, 0], + 'auth_method': 'CHAP', + 'discovery_auth_method': 'CHAP'}} + + volume_iSCSI = self._create_volume() + extra_spec = {'capabilities:storage_protocol': ' iSCSI'} + vol_type_iSCSI = volume_types.create(self.ctxt, 'iSCSI', extra_spec) + volume_iSCSI['volume_type_id'] = vol_type_iSCSI['id'] + + # Check case where no hosts exist + ret = self.iscsi_driver._assistant.get_host_from_connector( + connector) + self.assertIsNone(ret) + + # Initialize connection to map volume to a host + ret = self.iscsi_driver.initialize_connection( + volume_iSCSI, connector) + self.assertEqual(exp_m_path['driver_volume_type'], + ret['driver_volume_type']) + + # Check the multipath host-volume map return value + for k, v in exp_m_path['data'].items(): + self.assertEqual(v, ret['data'][k]) + + ret = self.iscsi_driver._assistant.get_host_from_connector( + connector) + self.assertIsNotNone(ret) + + def test_instorage_mcs_iscsi_host_maps(self): + # Create two volumes to be used in mappings + + ctxt = context.get_admin_context() + volume1 = self._generate_vol_info(None, None) + self.iscsi_driver.create_volume(volume1) + volume2 = self._generate_vol_info(None, None) + self.iscsi_driver.create_volume(volume2) + + # Create volume types that we created + types = {} + for protocol in ['iSCSI']: + opts = {'storage_protocol': ' ' + protocol} + types[protocol] = volume_types.create(ctxt, protocol, opts) + + expected = {'iSCSI': {'driver_volume_type': 'iscsi', + 'data': {'target_discovered': False, + 'target_iqn': + 'iqn.1982-01.com.inspur:1234.sim.node1', + 'target_portal': '1.234.56.78:3260', + 'target_lun': 0, + 'auth_method': 'CHAP', + 'discovery_auth_method': 'CHAP'}}} + + volume1['volume_type_id'] = types[protocol]['id'] + volume2['volume_type_id'] = types[protocol]['id'] + + # Check case where no hosts exist + ret = self.iscsi_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(ret) + + # Make sure that the volumes have been created + self._assert_vol_exists(volume1['name'], True) + self._assert_vol_exists(volume2['name'], True) + + # Initialize connection from the first volume to a host + ret = self.iscsi_driver.initialize_connection( + volume1, self._connector) + self.assertEqual(expected[protocol]['driver_volume_type'], + ret['driver_volume_type']) + for k, v in expected[protocol]['data'].items(): + self.assertEqual(v, ret['data'][k]) + + # Initialize again, should notice it and do nothing + ret = self.iscsi_driver.initialize_connection( + volume1, self._connector) + self.assertEqual(expected[protocol]['driver_volume_type'], + ret['driver_volume_type']) + for k, v in expected[protocol]['data'].items(): + self.assertEqual(v, ret['data'][k]) + + # Try to delete the 1st volume (should fail because it is mapped) + self.assertRaises(exception.VolumeBackendAPIException, + self.iscsi_driver.delete_volume, + volume1) + + ret = self.iscsi_driver.terminate_connection(volume1, + self._connector) + ret = self.iscsi_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(ret) + + # Check cases with no auth set for host + for auth_enabled in [True, False]: + for host_exists in ['yes-auth', 'yes-noauth', 'no']: + self._set_flag('instorage_mcs_iscsi_chap_enabled', + auth_enabled) + case = 'en' + six.text_type( + auth_enabled) + 'ex' + six.text_type(host_exists) + conn_na = {'initiator': 'test:init:%s' % 56789, + 'ip': '11.11.11.11', + 'host': 'host-%s' % case} + if host_exists.startswith('yes'): + self.sim._add_host_to_list(conn_na) + if host_exists == 'yes-auth': + kwargs = {'chapsecret': 'foo', + 'obj': conn_na['host']} + self.sim._cmd_chhost(**kwargs) + volume1['volume_type_id'] = types['iSCSI']['id'] + + init_ret = self.iscsi_driver.initialize_connection(volume1, + conn_na) + host_name = self.sim._host_in_list(conn_na['host']) + chap_ret = ( + self.iscsi_driver._assistant.get_chap_secret_for_host( + host_name)) + if auth_enabled or host_exists == 'yes-auth': + self.assertIn('auth_password', init_ret['data']) + self.assertIsNotNone(chap_ret) + else: + self.assertNotIn('auth_password', init_ret['data']) + self.assertIsNone(chap_ret) + self.iscsi_driver.terminate_connection(volume1, conn_na) + self._set_flag('instorage_mcs_iscsi_chap_enabled', True) + + # Test no preferred node + self.sim.error_injection('lsvdisk', 'no_pref_node') + self.assertRaises(exception.VolumeBackendAPIException, + self.iscsi_driver.initialize_connection, + volume1, self._connector) + + # Initialize connection from the second volume to the host with no + # preferred node set if in simulation mode, otherwise, just + # another initialize connection. + self.sim.error_injection('lsvdisk', 'blank_pref_node') + self.iscsi_driver.initialize_connection(volume2, self._connector) + + # Try to remove connection from host that doesn't exist (should fail) + conn_no_exist = self._connector.copy() + conn_no_exist['initiator'] = 'i_dont_exist' + conn_no_exist['wwpns'] = ['0000000000000000'] + self.assertRaises(exception.VolumeDriverException, + self.iscsi_driver.terminate_connection, + volume1, + conn_no_exist) + + # Try to remove connection from volume that isn't mapped (should print + # message but NOT fail) + unmapped_vol = self._generate_vol_info(None, None) + self.iscsi_driver.create_volume(unmapped_vol) + self.iscsi_driver.terminate_connection(unmapped_vol, self._connector) + self.iscsi_driver.delete_volume(unmapped_vol) + + # Remove the mapping from the 1st volume and delete it + self.iscsi_driver.terminate_connection(volume1, self._connector) + self.iscsi_driver.delete_volume(volume1) + self._assert_vol_exists(volume1['name'], False) + + # Make sure our host still exists + host_name = self.iscsi_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNotNone(host_name) + + # Remove the mapping from the 2nd volume. The host should + # be automatically removed because there are no more mappings. + self.iscsi_driver.terminate_connection(volume2, self._connector) + + # Check if we successfully terminate connections when the host is not + # specified + fake_conn = {'ip': '127.0.0.1', 'initiator': 'iqn.fake'} + self.iscsi_driver.initialize_connection(volume2, self._connector) + host_name = self.iscsi_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNotNone(host_name) + self.iscsi_driver.terminate_connection(volume2, fake_conn) + host_name = self.iscsi_driver._assistant.get_host_from_connector( + self._connector) + self.assertIsNone(host_name) + self.iscsi_driver.delete_volume(volume2) + self._assert_vol_exists(volume2['name'], False) + + # Delete volume types that we created + for protocol in ['iSCSI']: + volume_types.destroy(ctxt, types[protocol]['id']) + + # Check if our host still exists (it should not) + ret = (self.iscsi_driver._assistant.get_host_from_connector( + self._connector)) + self.assertIsNone(ret) + + def test_add_vdisk_copy_iscsi(self): + # Ensure only iSCSI is available + self.iscsi_driver._state['enabled_protocols'] = set(['iSCSI']) + volume = self._generate_vol_info(None, None) + self.iscsi_driver.create_volume(volume) + self.iscsi_driver.add_vdisk_copy(volume['name'], 'fake-pool', None) diff --git a/cinder/tests/unit/volume/drivers/inspur/instorage/test_replication.py b/cinder/tests/unit/volume/drivers/inspur/instorage/test_replication.py new file mode 100644 index 00000000000..75c27efa378 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/inspur/instorage/test_replication.py @@ -0,0 +1,1002 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +Tests for the Inspur InStorage volume driver. +""" + +import json + +from eventlet import greenthread +import mock +from oslo_utils import importutils +from oslo_utils import units + +from cinder import context +from cinder import exception +from cinder.objects import fields +from cinder import test +from cinder.tests.unit import fake_constants as fake +from cinder.tests.unit import utils as testutils +from cinder import utils +from cinder.volume import configuration as conf +from cinder.volume.drivers.inspur.instorage import ( + replication as instorage_rep) +from cinder.volume.drivers.inspur.instorage import instorage_common +from cinder.volume.drivers.inspur.instorage import instorage_const +from cinder.volume import volume_types + +from cinder.tests.unit.volume.drivers.inspur.instorage import fakes + + +class InStorageMCSReplicationTestCase(test.TestCase): + + @mock.patch.object(greenthread, 'sleep') + def setUp(self, mock_sleep): + super(InStorageMCSReplicationTestCase, self).setUp() + + def _run_ssh_aux(cmd, check_exit_code=True, attempts=1): + utils.check_ssh_injection(cmd) + if len(cmd) > 2 and cmd[1] == 'lssystem': + cmd[1] = 'lssystem_aux' + ret = self.sim.execute_command(cmd, check_exit_code) + return ret + aux_connect_patcher = mock.patch( + 'cinder.volume.drivers.inspur.instorage.' + 'replication.InStorageMCSReplicationManager._run_ssh') + self.aux_ssh_mock = aux_connect_patcher.start() + self.addCleanup(aux_connect_patcher.stop) + self.aux_ssh_mock.side_effect = _run_ssh_aux + + self.driver = fakes.FakeInStorageMCSISCSIDriver( + configuration=conf.Configuration(None)) + self.rep_target = {"backend_id": "mcs_aux_target_1", + "san_ip": "192.168.10.22", + "san_login": "admin", + "san_password": "admin", + "pool_name": fakes.get_test_pool()} + self.fake_target = {"backend_id": "mcs_id_target", + "san_ip": "192.168.10.23", + "san_login": "admin", + "san_password": "admin", + "pool_name": fakes.get_test_pool()} + self._def_flags = {'san_ip': '192.168.10.21', + 'san_login': 'user', + 'san_password': 'pass', + 'instorage_mcs_volpool_name': fakes.MCS_POOLS, + 'replication_device': [self.rep_target]} + wwpns = ['1234567890123451', '6543210987654326'] + initiator = 'test.initiator.%s' % 123451 + self._connector = {'ip': '1.234.56.78', + 'host': 'instorage-mcs-test', + 'wwpns': wwpns, + 'initiator': initiator} + self.sim = fakes.FakeInStorage(fakes.MCS_POOLS) + + self.driver.set_fake_storage(self.sim) + self.ctxt = context.get_admin_context() + + self._reset_flags() + self.ctxt = context.get_admin_context() + db_driver = self.driver.configuration.db_driver + self.db = importutils.import_module(db_driver) + self.driver.db = self.db + + self.driver.do_setup(None) + self.driver.check_for_setup_error() + self._create_test_volume_types() + + def _set_flag(self, flag, value): + group = self.driver.configuration.config_group + self.driver.configuration.set_override(flag, value, group) + + def _reset_flags(self): + self.driver.configuration.local_conf.reset() + for k, v in self._def_flags.items(): + self._set_flag(k, v) + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + + def _assert_vol_exists(self, name, exists): + is_vol_defined = self.driver._assistant.is_vdisk_defined(name) + self.assertEqual(exists, is_vol_defined) + + def _generate_vol_info(self, vol_name, vol_id, vol_type=None): + pool = fakes.get_test_pool() + volume_type = self.non_replica_type + if vol_type: + volume_type = vol_type + if vol_name: + prop = {'volume_name': vol_name, + 'volume_id': vol_id, + 'volume_size': 10, + 'mdisk_grp_name': pool} + else: + prop = {'size': 10, + 'mdisk_grp_name': pool, + 'host': 'openstack@mcs#%s' % pool, + 'volume_type_id': volume_type['id']} + vol = testutils.create_volume(self.ctxt, **prop) + return vol + + def _generate_snapshot_info(self, vol): + snap = testutils.create_snapshot(self.ctxt, vol.id) + return snap + + def _create_replica_volume_type(self, enable, + rep_type=instorage_const.SYNC): + # Generate a volume type for volume repliation. + if enable: + if rep_type == instorage_const.SYNC: + spec = {'replication_enabled': ' True', + 'replication_type': ' sync'} + type_name = 'rep_sync' + else: + spec = {'replication_enabled': ' True', + 'replication_type': ' async'} + type_name = 'rep_async' + else: + spec = {'replication_enabled': ' False'} + type_name = "non_rep" + + db_rep_type = testutils.create_volume_type(self.ctxt, + name=type_name, + extra_specs=spec) + rep_type = volume_types.get_volume_type(self.ctxt, db_rep_type.id) + + return rep_type + + def _create_test_volume_types(self): + self.mm_type = self._create_replica_volume_type( + True, rep_type=instorage_const.SYNC) + self.gm_type = self._create_replica_volume_type( + True, rep_type=instorage_const.ASYNC) + self.non_replica_type = self._create_replica_volume_type(False) + + def _create_test_volume(self, rep_type): + volume = self._generate_vol_info(None, None, rep_type) + model_update = self.driver.create_volume(volume) + return volume, model_update + + def _get_vdisk_uid(self, vdisk_name): + vdisk_properties, _err = self.sim._cmd_lsvdisk(obj=vdisk_name, + delim='!') + for row in vdisk_properties.split('\n'): + words = row.split('!') + if words[0] == 'vdisk_UID': + return words[1] + return None + + def test_instorage_do_replication_setup_error(self): + fake_targets = [self.rep_target, self.rep_target] + self.driver.configuration.set_override('replication_device', + [{"backend_id": + "mcs_id_target"}]) + self.assertRaises(exception.InvalidInput, + self.driver._do_replication_setup) + + self.driver.configuration.set_override('replication_device', + fake_targets) + self.assertRaises(exception.InvalidInput, + self.driver._do_replication_setup) + + self.driver._active_backend_id = 'fake_id' + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.assertRaises(exception.InvalidInput, + self.driver._do_replication_setup) + + self.driver._active_backend_id = None + + self.driver._do_replication_setup() + self.assertEqual(self.rep_target, self.driver._replica_target) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'replication_licensed') + def test_instorage_setup_replication(self, + replication_licensed): + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver._active_backend_id = None + replication_licensed.side_effect = [False, True, True, True] + + self.driver._get_instorage_config() + self.assertEqual(self.driver._assistant, + self.driver._local_backend_assistant) + self.assertFalse(self.driver._replica_enabled) + + self.driver._get_instorage_config() + self.assertEqual(self.rep_target, self.driver._replica_target) + self.assertTrue(self.driver._replica_enabled) + + self.driver._active_backend_id = self.rep_target['backend_id'] + self.driver._get_instorage_config() + self.assertEqual(self.driver._assistant, + self.driver._aux_backend_assistant) + self.assertTrue(self.driver._replica_enabled) + + self.driver._active_backend_id = None + self.driver._get_instorage_config() + + def test_instorage_create_volume_with_mirror_replication(self): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + self._validate_replic_vol_creation(volume) + self.driver.delete_volume(volume) + self._validate_replic_vol_deletion(volume) + + # Create async copy replication. + volume, model_update = self._create_test_volume(self.gm_type) + self.assertEqual('enabled', model_update['replication_status']) + self._validate_replic_vol_creation(volume) + self.driver.delete_volume(volume) + self._validate_replic_vol_deletion(volume) + + def _validate_replic_vol_creation(self, volume): + # Create sync copy volume + self._assert_vol_exists(volume['name'], True) + self._assert_vol_exists( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume['name'], True) + + rel_info = self.driver._assistant.get_relationship_info(volume['name']) + self.assertIsNotNone(rel_info) + vol_rep_type = rel_info['copy_type'] + rep_type = self.driver._get_volume_replicated_type(self.ctxt, volume) + self.assertEqual(vol_rep_type, rep_type) + + self.assertEqual('master', rel_info['primary']) + self.assertEqual(volume['name'], rel_info['master_vdisk_name']) + self.assertEqual( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume['name'], + rel_info['aux_vdisk_name']) + self.assertEqual('inconsistent_copying', rel_info['state']) + + self.sim._rc_state_transition('wait', rel_info) + self.assertEqual('consistent_synchronized', rel_info['state']) + + def _validate_replic_vol_deletion(self, volume): + self._assert_vol_exists(volume['name'], False) + self._assert_vol_exists( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume['name'], False) + rel_info = self.driver._assistant.get_relationship_info(volume['name']) + self.assertIsNone(rel_info) + + def test_instorage_create_snapshot_volume_with_mirror_replica(self): + # Set replication target + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication volume. + vol1, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + snap = self._generate_snapshot_info(vol1) + self.driver.create_snapshot(snap) + + vol2 = self._generate_vol_info(None, None, self.mm_type) + model_update = self.driver.create_volume_from_snapshot(vol2, snap) + self.assertEqual('enabled', model_update['replication_status']) + self._validate_replic_vol_creation(vol2) + + self.driver.delete_snapshot(snap) + self.driver.delete_volume(vol1) + self.driver.delete_volume(vol2) + + def test_instorage_create_cloned_volume_with_mirror_replica(self): + # Set replication target + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create a source sync copy replication volume. + src_volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + volume = self._generate_vol_info(None, None, self.mm_type) + + # Create a cloned volume from source volume. + model_update = self.driver.create_cloned_volume(volume, src_volume) + self.assertEqual('enabled', model_update['replication_status']) + self._validate_replic_vol_creation(volume) + + self.driver.delete_volume(src_volume) + self.driver.delete_volume(volume) + + def test_instorage_retype_from_mirror_to_none_replication(self): + # Set replication target + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + host = {'host': 'openstack@mcs#openstack'} + + volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + diff, _equal = volume_types.volume_types_diff( + self.ctxt, self.mm_type['id'], self.gm_type['id']) + # Change the mirror type + self.assertRaises(exception.VolumeDriverException, + self.driver.retype, self.ctxt, + volume, self.gm_type, diff, host) + + diff, _equal = volume_types.volume_types_diff( + self.ctxt, self.non_replica_type['id'], self.mm_type['id']) + # Disable replica + retyped, model_update = self.driver.retype( + self.ctxt, volume, self.non_replica_type, diff, host) + self.assertEqual('disabled', model_update['replication_status']) + self._assert_vol_exists( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume['name'], False) + + self.driver.delete_volume(volume) + self._assert_vol_exists(volume['name'], False) + rel_info = self.driver._assistant.get_relationship_info(volume['name']) + self.assertIsNone(rel_info) + + def test_instorage_retype_from_none_to_mirror_replication(self): + # Set replication target + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + host = {'host': 'openstack@mcs#openstack'} + + diff, _equal = volume_types.volume_types_diff( + self.ctxt, self.non_replica_type['id'], self.mm_type['id']) + + volume, model_update = self._create_test_volume(self.non_replica_type) + self.assertIsNone(model_update) + + # Enable replica + retyped, model_update = self.driver.retype( + self.ctxt, volume, self.mm_type, diff, host) + volume['volume_type_id'] = self.mm_type['id'] + self.assertEqual(fields.ReplicationStatus.ENABLED, + model_update['replication_status']) + self._validate_replic_vol_creation(volume) + + self.driver.delete_volume(volume) + + def test_instorage_extend_volume_replication(self): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + self.driver.extend_volume(volume, '13') + attrs = self.driver._assistant.get_vdisk_attributes(volume['name']) + vol_size = int(attrs['capacity']) / units.Gi + self.assertAlmostEqual(vol_size, 13) + + attrs = self.driver._aux_backend_assistant.get_vdisk_attributes( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume['name']) + vol_size = int(attrs['capacity']) / units.Gi + self.assertAlmostEqual(vol_size, 13) + + self.driver.delete_volume(volume) + self._validate_replic_vol_deletion(volume) + + def test_instorage_manage_existing_mismatch_with_volume_replication(self): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create replication volume. + rep_volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual(fields.ReplicationStatus.ENABLED, + model_update['replication_status']) + + # Create non-replication volume. + non_rep_volume, model_update = self._create_test_volume( + self.non_replica_type) + + new_volume = self._generate_vol_info(None, None) + + ref = {'source-name': rep_volume['name']} + new_volume['volume_type_id'] = self.non_replica_type['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + ref = {'source-name': non_rep_volume['name']} + new_volume['volume_type_id'] = self.mm_type['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + + ref = {'source-name': rep_volume['name']} + new_volume['volume_type_id'] = self.gm_type['id'] + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, new_volume, ref) + self.driver.delete_volume(rep_volume) + self.driver.delete_volume(new_volume) + + def test_instorage_manage_existing_with_volume_replication(self): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create replication volume. + rep_volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + uid_of_master = self._get_vdisk_uid(rep_volume['name']) + uid_of_aux = self._get_vdisk_uid( + instorage_const.REPLICA_AUX_VOL_PREFIX + rep_volume['name']) + + new_volume = self._generate_vol_info(None, None, self.mm_type) + ref = {'source-name': rep_volume['name']} + self.driver.manage_existing(new_volume, ref) + + # Check the uid of the volume which has been renamed. + uid_of_master_volume = self._get_vdisk_uid(new_volume['name']) + uid_of_aux_volume = self._get_vdisk_uid( + instorage_const.REPLICA_AUX_VOL_PREFIX + new_volume['name']) + self.assertEqual(uid_of_master, uid_of_master_volume) + self.assertEqual(uid_of_aux, uid_of_aux_volume) + + self.driver.delete_volume(rep_volume) + + def test_instorage_delete_volume_with_mirror_replication(self): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + volume, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + self._validate_replic_vol_creation(volume) + + # Delete volume in non-failover state + self.driver.delete_volume(volume) + self._validate_replic_vol_deletion(volume) + + non_replica_vol, model_update = self._create_test_volume( + self.non_replica_type) + self.assertIsNone(model_update) + + volumes = [volume, non_replica_vol] + # Delete volume in failover state + self.driver.failover_host( + self.ctxt, volumes, self.rep_target['backend_id']) + # Delete non-replicate volume in a failover state + self.assertRaises(exception.VolumeDriverException, + self.driver.delete_volume, + non_replica_vol) + + # Delete replicate volume in failover state + self.driver.delete_volume(volume) + self._validate_replic_vol_deletion(volume) + self.driver.failover_host( + self.ctxt, volumes, 'default') + self.driver.delete_volume(non_replica_vol) + self._assert_vol_exists(non_replica_vol['name'], False) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_vdisk') + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def test_delete_target_volume(self, get_relationship_info, + delete_relationship, + delete_vdisk): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + fake_name = 'volume-%s' % fake.VOLUME_ID + get_relationship_info.return_value = {'aux_vdisk_name': + fake_name} + self.driver._assistant.delete_rc_volume(fake_name) + get_relationship_info.assert_called_once_with(fake_name) + delete_relationship.assert_called_once_with(fake_name) + delete_vdisk.assert_called_once_with(fake_name, False) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_vdisk') + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def test_delete_target_volume_no_relationship(self, get_relationship_info, + delete_relationship, + delete_vdisk): + # Set replication target. + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + fake_name = 'volume-%s' % fake.VOLUME_ID + get_relationship_info.return_value = None + self.driver._assistant.delete_rc_volume(fake_name) + get_relationship_info.assert_called_once_with(fake_name) + self.assertFalse(delete_relationship.called) + self.assertTrue(delete_vdisk.called) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_vdisk') + @mock.patch.object(instorage_common.InStorageAssistant, + 'delete_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def test_delete_target_volume_fail(self, get_relationship_info, + delete_relationship, + delete_vdisk): + fake_id = fake.VOLUME_ID + fake_name = 'volume-%s' % fake_id + get_relationship_info.return_value = {'aux_vdisk_name': + fake_name} + delete_vdisk.side_effect = Exception + self.assertRaises(exception.VolumeDriverException, + self.driver._assistant.delete_rc_volume, + fake_name) + get_relationship_info.assert_called_once_with(fake_name) + delete_relationship.assert_called_once_with(fake_name) + + def test_instorage_failover_host_backend_error(self): + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + mm_vol, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + volumes = [mm_vol] + + self.driver._replica_enabled = False + self.assertRaises(exception.UnableToFailOver, + self.driver.failover_host, + self.ctxt, volumes, self.rep_target['backend_id']) + self.driver._replica_enabled = True + self.assertRaises(exception.InvalidReplicationTarget, + self.driver.failover_host, + self.ctxt, volumes, self.fake_target['backend_id']) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_system_info') as get_sys_info: + get_sys_info.side_effect = [ + exception.VolumeBackendAPIException(data='CMMVC6071E'), + exception.VolumeBackendAPIException(data='CMMVC6071E')] + self.assertRaises(exception.UnableToFailOver, + self.driver.failover_host, + self.ctxt, volumes, + self.rep_target['backend_id']) + + self.driver._active_backend_id = self.rep_target['backend_id'] + self.assertRaises(exception.UnableToFailOver, + self.driver.failover_host, + self.ctxt, volumes, 'default') + self.driver.delete_volume(mm_vol) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def test_failover_volume_relationship_error(self, get_relationship_info): + # Create async copy replication. + gm_vol, model_update = self._create_test_volume(self.gm_type) + self.assertEqual('enabled', model_update['replication_status']) + + get_relationship_info.side_effect = [None, + exception.VolumeDriverException] + expected_list = [{'updates': {'replication_status': + fields.ReplicationStatus.FAILOVER_ERROR, + 'status': 'error'}, + 'volume_id': gm_vol.id} + ] + volumes_update = self.driver._failover_replica_volumes(self.ctxt, + [gm_vol]) + self.assertEqual(expected_list, volumes_update) + + volumes_update = self.driver._failover_replica_volumes(self.ctxt, + [gm_vol]) + self.assertEqual(expected_list, volumes_update) + + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_volume_stats') + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_instorage_state') + def test_instorage_failover_host_replica_volumes(self, + update_instorage_state, + update_volume_stats): + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + mm_vol, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + # Create async replication volume. + gm_vol, model_update = self._create_test_volume(self.gm_type) + self.assertEqual('enabled', model_update['replication_status']) + + volumes = [mm_vol, gm_vol] + expected_list = [{'updates': {'replication_status': 'failed-over'}, + 'volume_id': mm_vol['id']}, + {'updates': {'replication_status': 'failed-over'}, + 'volume_id': gm_vol['id']} + ] + + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, self.rep_target['backend_id']) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual(expected_list, volume_list) + + self.assertEqual(self.driver._active_backend_id, target_id) + self.assertEqual(self.driver._aux_backend_assistant, + self.driver._assistant) + self.assertEqual([self.driver._replica_target['pool_name']], + self.driver._get_backend_pools()) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + + self.driver.delete_volume(mm_vol) + self.driver.delete_volume(gm_vol) + + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, None) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual([], volume_list) + + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_volume_stats') + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_instorage_state') + def test_instorage_failover_host_normal_volumes(self, + update_instorage_state, + update_volume_stats): + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + mm_vol, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + mm_vol['status'] = 'in-use' + + # Create non-replication volume. + non_replica_vol, model_update = self._create_test_volume( + self.non_replica_type) + self.assertIsNone(model_update) + non_replica_vol['status'] = 'error' + + volumes = [mm_vol, non_replica_vol] + + rep_data1 = json.dumps({'previous_status': mm_vol['status']}) + rep_data2 = json.dumps({'previous_status': non_replica_vol['status']}) + expected_list = [{'updates': {'status': 'error', + 'replication_driver_data': rep_data1}, + 'volume_id': mm_vol['id']}, + {'updates': {'status': 'error', + 'replication_driver_data': rep_data2}, + 'volume_id': non_replica_vol['id']}, + ] + + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, self.rep_target['backend_id']) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual(expected_list, volume_list) + + self.assertEqual(self.driver._active_backend_id, target_id) + self.assertEqual(self.driver._aux_backend_assistant, + self.driver._assistant) + self.assertEqual([self.driver._replica_target['pool_name']], + self.driver._get_backend_pools()) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, None) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual([], volume_list) + # Delete non-replicate volume in a failover state + self.assertRaises(exception.VolumeDriverException, + self.driver.delete_volume, + non_replica_vol) + self.driver.failover_host(self.ctxt, volumes, 'default') + self.driver.delete_volume(mm_vol) + self.driver.delete_volume(non_replica_vol) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'switch_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'stop_relationship') + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + def test_failover_host_by_force_access(self, get_relationship_info, + stop_relationship, + switch_relationship): + replica_obj = self.driver._get_replica_obj(instorage_const.SYNC) + fake_vol_info = {'vol_id': '21345678-1234-5678-1234-567812345683', + 'vol_name': 'fake-volume'} + fake_vol = self._generate_vol_info(**fake_vol_info) + target_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + fake_vol['name'] + context = mock.Mock + get_relationship_info.side_effect = [{ + 'aux_vdisk_name': 'replica-12345678-1234-5678-1234-567812345678', + 'name': 'RC_name'}] + switch_relationship.side_effect = exception.VolumeDriverException + replica_obj.failover_volume_host(context, fake_vol) + get_relationship_info.assert_called_once_with(target_vol) + switch_relationship.assert_called_once_with('RC_name') + stop_relationship.assert_called_once_with(target_vol, access=True) + + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_volume_stats') + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_instorage_state') + def test_instorage_failback_replica_volumes(self, + update_instorage_state, + update_volume_stats): + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + mm_vol, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + + # Create async copy replication. + gm_vol, model_update = self._create_test_volume(self.gm_type) + self.assertEqual('enabled', model_update['replication_status']) + + volumes = [gm_vol, mm_vol] + failover_expect = [{'updates': {'replication_status': 'failed-over'}, + 'volume_id': gm_vol['id']}, + {'updates': {'replication_status': 'failed-over'}, + 'volume_id': mm_vol['id']} + ] + + failback_expect = [{'updates': {'replication_status': 'enabled', + 'status': 'available'}, + 'volume_id': gm_vol['id']}, + {'updates': {'replication_status': 'enabled', + 'status': 'available'}, + 'volume_id': mm_vol['id']}, + ] + # Already failback + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, 'default') + self.assertIsNone(target_id) + self.assertEqual([], volume_list) + + # fail over operation + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, self.rep_target['backend_id']) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual(failover_expect, volume_list) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + + # fail back operation + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, 'default') + self.assertEqual('default', target_id) + self.assertEqual(failback_expect, volume_list) + self.assertIsNone(self.driver._active_backend_id) + self.assertEqual(fakes.MCS_POOLS, self.driver._get_backend_pools()) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + self.driver.delete_volume(mm_vol) + self.driver.delete_volume(gm_vol) + + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_volume_stats') + @mock.patch.object(instorage_common.InStorageMCSCommonDriver, + '_update_instorage_state') + def test_instorage_failback_normal_volumes(self, + update_instorage_state, + update_volume_stats): + + self.driver.configuration.set_override('replication_device', + [self.rep_target]) + self.driver.do_setup(self.ctxt) + + # Create sync copy replication. + mm_vol, model_update = self._create_test_volume(self.mm_type) + self.assertEqual('enabled', model_update['replication_status']) + mm_vol['status'] = 'in-use' + + # Create non-replication volume. + non_replica_vol1, model_update = self._create_test_volume( + self.non_replica_type) + self.assertIsNone(model_update) + non_replica_vol2, model_update = self._create_test_volume( + self.non_replica_type) + self.assertIsNone(model_update) + non_replica_vol1['status'] = 'error' + non_replica_vol2['status'] = 'available' + + volumes = [mm_vol, non_replica_vol1, non_replica_vol2] + + rep_data0 = json.dumps({'previous_status': mm_vol['status']}) + rep_data1 = json.dumps({'previous_status': non_replica_vol1['status']}) + rep_data2 = json.dumps({'previous_status': non_replica_vol2['status']}) + failover_expect = [{'updates': {'status': 'error', + 'replication_driver_data': rep_data0}, + 'volume_id': mm_vol['id']}, + {'updates': {'status': 'error', + 'replication_driver_data': rep_data1}, + 'volume_id': non_replica_vol1['id']}, + {'updates': {'status': 'error', + 'replication_driver_data': rep_data2}, + 'volume_id': non_replica_vol2['id']}] + failback_expect = [{'updates': {'status': 'in-use', + 'replication_driver_data': ''}, + 'volume_id': mm_vol['id']}, + {'updates': {'status': 'error', + 'replication_driver_data': ''}, + 'volume_id': non_replica_vol1['id']}, + {'updates': {'status': 'available', + 'replication_driver_data': ''}, + 'volume_id': non_replica_vol2['id']}] + # Already failback + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, 'default') + self.assertIsNone(target_id) + self.assertEqual([], volume_list) + + # fail over operation + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, self.rep_target['backend_id']) + self.assertEqual(self.rep_target['backend_id'], target_id) + self.assertEqual(failover_expect, volume_list) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + + # fail back operation + mm_vol['replication_driver_data'] = json.dumps( + {'previous_status': 'in-use'}) + non_replica_vol1['replication_driver_data'] = json.dumps( + {'previous_status': 'error'}) + non_replica_vol2['replication_driver_data'] = json.dumps( + {'previous_status': 'available'}) + target_id, volume_list = self.driver.failover_host( + self.ctxt, volumes, 'default') + self.assertEqual('default', target_id) + self.assertEqual(failback_expect, volume_list) + self.assertIsNone(self.driver._active_backend_id) + self.assertEqual(fakes.MCS_POOLS, self.driver._get_backend_pools()) + self.assertTrue(update_instorage_state.called) + self.assertTrue(update_volume_stats.called) + self.driver.delete_volume(mm_vol) + self.driver.delete_volume(non_replica_vol1) + self.driver.delete_volume(non_replica_vol2) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_system_info') + @mock.patch.object(instorage_rep.InStorageMCSReplicationManager, + '_partnership_validate_create') + def test_establish_partnership_with_local_sys(self, partnership_create, + get_system_info): + get_system_info.side_effect = [{'system_name': 'instorage-mcs-sim'}, + {'system_name': 'instorage-mcs-sim'}] + + rep_mgr = self.driver._get_replica_mgr() + rep_mgr.establish_target_partnership() + self.assertFalse(partnership_create.called) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_system_info') + def test_establish_target_partnership(self, get_system_info): + source_system_name = 'instorage-mcs-sim' + target_system_name = 'aux-mcs-sim' + + get_system_info.side_effect = [{'system_name': source_system_name}, + {'system_name': target_system_name}] + + rep_mgr = self.driver._get_replica_mgr() + rep_mgr.establish_target_partnership() + partner_info = self.driver._assistant.get_partnership_info( + source_system_name) + self.assertIsNotNone(partner_info) + self.assertEqual(source_system_name, partner_info['name']) + + partner_info = self.driver._assistant.get_partnership_info( + source_system_name) + self.assertIsNotNone(partner_info) + self.assertEqual(source_system_name, partner_info['name']) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'start_relationship') + def test_sync_replica_volumes_with_aux(self, start_relationship): + # Create sync copy replication. + mm_vol = self._generate_vol_info(None, None, self.mm_type) + tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + mm_vol['name'] + + volumes = [mm_vol] + fake_info = {'volume': 'fake', + 'master_vdisk_name': 'fake', + 'aux_vdisk_name': 'fake'} + sync_state = {'state': instorage_const.REP_CONSIS_SYNC, + 'primary': 'fake'} + sync_state.update(fake_info) + disconn_state = {'state': instorage_const.REP_IDL_DISC, + 'primary': 'master'} + disconn_state.update(fake_info) + stop_state = {'state': instorage_const.REP_CONSIS_STOP, + 'primary': 'aux'} + stop_state.update(fake_info) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=None)): + self.driver._sync_with_aux(self.ctxt, volumes) + self.assertFalse(start_relationship.called) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=sync_state)): + self.driver._sync_with_aux(self.ctxt, volumes) + self.assertFalse(start_relationship.called) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=disconn_state)): + self.driver._sync_with_aux(self.ctxt, volumes) + start_relationship.assert_called_once_with(tgt_volume) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=stop_state)): + self.driver._sync_with_aux(self.ctxt, volumes) + start_relationship.assert_called_with(tgt_volume, + primary='aux') + self.driver.delete_volume(mm_vol) + + @mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info') + @mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall', + new=testutils.ZeroIntervalLoopingCall) + def test_wait_replica_vol_ready(self, get_relationship_info): + # Create sync copy replication. + mm_vol = self._generate_vol_info(None, None, self.mm_type) + fake_info = {'volume': 'fake', + 'master_vdisk_name': 'fake', + 'aux_vdisk_name': 'fake', + 'primary': 'fake'} + sync_state = {'state': instorage_const.REP_CONSIS_SYNC} + sync_state.update(fake_info) + disconn_state = {'state': instorage_const.REP_IDL_DISC} + disconn_state.update(fake_info) + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=None)): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver._wait_replica_vol_ready, + self.ctxt, mm_vol) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=sync_state)): + self.driver._wait_replica_vol_ready(self.ctxt, mm_vol) + + with mock.patch.object(instorage_common.InStorageAssistant, + 'get_relationship_info', + mock.Mock(return_value=disconn_state)): + self.assertRaises(exception.VolumeBackendAPIException, + self.driver._wait_replica_vol_ready, + self.ctxt, mm_vol) diff --git a/cinder/volume/drivers/inspur/__init__.py b/cinder/volume/drivers/inspur/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/inspur/instorage/__init__.py b/cinder/volume/drivers/inspur/instorage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/inspur/instorage/instorage_common.py b/cinder/volume/drivers/inspur/instorage/instorage_common.py new file mode 100644 index 00000000000..a02013fa364 --- /dev/null +++ b/cinder/volume/drivers/inspur/instorage/instorage_common.py @@ -0,0 +1,3629 @@ +# Copyright 2017 Inspur Corp. +# 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. +# + +import math +import random +import re +import time +import unicodedata + +from eventlet import greenthread +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_serialization import jsonutils as json +from oslo_service import loopingcall +from oslo_utils import excutils +from oslo_utils import strutils +from oslo_utils import units +import paramiko +import six + +from cinder import context +from cinder import exception +from cinder.i18n import _ +from cinder.objects import fields +from cinder import ssh_utils +from cinder import utils as cinder_utils +from cinder.volume import driver +from cinder.volume.drivers.inspur.instorage import ( + replication as instorage_rep) +from cinder.volume.drivers.inspur.instorage import instorage_const +from cinder.volume.drivers.san import san +from cinder.volume import qos_specs +from cinder.volume import utils +from cinder.volume import volume_types + +INTERVAL_1_SEC = 1 +DEFAULT_TIMEOUT = 20 +LOG = logging.getLogger(__name__) + +instorage_mcs_opts = [ + cfg.BoolOpt('instorage_mcs_vol_autoexpand', + default=True, + help='Storage system autoexpand parameter for volumes ' + '(True/False)'), + cfg.BoolOpt('instorage_mcs_vol_compression', + default=False, + help='Storage system compression option for volumes'), + cfg.BoolOpt('instorage_mcs_vol_intier', + default=True, + help='Enable InTier for volumes'), + cfg.BoolOpt('instorage_mcs_allow_tenant_qos', + default=False, + help='Allow tenants to specify QOS on create'), + cfg.IntOpt('instorage_mcs_vol_grainsize', + default=256, + min=32, max=256, + help='Storage system grain size parameter for volumes ' + '(32/64/128/256)'), + cfg.IntOpt('instorage_mcs_vol_rsize', + default=2, + min=-1, max=100, + help='Storage system space-efficiency parameter for volumes ' + '(percentage)'), + cfg.IntOpt('instorage_mcs_vol_warning', + default=0, + min=-1, max=100, + help='Storage system threshold for volume capacity warnings ' + '(percentage)'), + cfg.IntOpt('instorage_mcs_localcopy_timeout', + default=120, + min=1, max=600, + help='Maximum number of seconds to wait for LocalCopy to be ' + 'prepared.'), + cfg.IntOpt('instorage_mcs_localcopy_rate', + default=50, + min=1, max=100, + help='Specifies the InStorage LocalCopy copy rate to be used ' + 'when creating a full volume copy. The default is rate ' + 'is 50, and the valid rates are 1-100.'), + cfg.StrOpt('instorage_mcs_vol_iogrp', + default='0', + help='The I/O group in which to allocate volumes. It can be a ' + 'comma-separated list in which case the driver will select an ' + 'io_group based on least number of volumes associated with the ' + 'io_group.'), + cfg.StrOpt('instorage_san_secondary_ip', + default=None, + help='Specifies secondary management IP or hostname to be ' + 'used if san_ip is invalid or becomes inaccessible.'), + cfg.ListOpt('instorage_mcs_volpool_name', + default=['volpool'], + help='Comma separated list of storage system storage ' + 'pools for volumes.'), +] + +CONF = cfg.CONF +CONF.register_opts(instorage_mcs_opts) + + +class InStorageMCSCommonDriver(driver.VolumeDriver, san.SanDriver): + """Inspur InStorage MCS abstract base class for iSCSI/FC volume drivers. + + Version history: + + .. code-block:: none + + 1.0 - Initial driver + """ + + VERSION = "1.0.0" + VDISKCOPYOPS_INTERVAL = 600 + DEFAULT_GR_SLEEP = random.randint(20, 500) / 100.0 + + def __init__(self, *args, **kwargs): + super(InStorageMCSCommonDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(instorage_mcs_opts) + self._backend_name = self.configuration.safe_get('volume_backend_name') + self.active_ip = self.configuration.san_ip + self.inactive_ip = self.configuration.instorage_san_secondary_ip + self._local_backend_assistant = InStorageAssistant(self._run_ssh) + self._aux_backend_assistant = None + self._assistant = self._local_backend_assistant + self._vdiskcopyops = {} + self._vdiskcopyops_loop = None + self.protocol = None + self.replication = None + self._state = {'storage_nodes': {}, + 'enabled_protocols': set(), + 'compression_enabled': False, + 'available_iogrps': [], + 'system_name': None, + 'system_id': None, + 'code_level': None, + } + self._active_backend_id = kwargs.get('active_backend_id') + + # This dictionary is used to map each replication target to certain + # replication manager object. + self.replica_manager = {} + + # One driver can be configured with only one replication target + # to failover. + self._replica_target = {} + + # This boolean is used to indicate whether replication is supported + # by this storage. + self._replica_enabled = False + + # This list is used to save the supported replication modes. + self._supported_replica_types = [] + + # This is used to save the available pools in failed-over status + self._secondary_pools = None + + @cinder_utils.trace + def do_setup(self, ctxt): + """Check that we have all configuration details from the storage.""" + # InStorage has the limitation that can not burst more than 3 new ssh + # connections within 1 second. So slow down the initialization. + # however, this maybe removed later. + greenthread.sleep(1) + + # Update the instorage state + self._update_instorage_state() + + # v2.1 replication setup + self._get_instorage_config() + + # Validate that the pool exists + self._validate_pools_exist() + + def _update_instorage_state(self): + # Get storage system name, id, and code level + self._state.update(self._assistant.get_system_info()) + + # Check if compression is supported + self._state['compression_enabled'] = (self._assistant. + compression_enabled()) + + # Get the available I/O groups + self._state['available_iogrps'] = (self._assistant. + get_available_io_groups()) + + # Get the iSCSI and FC names of the InStorage/MCS nodes + self._state['storage_nodes'] = self._assistant.get_node_info() + + # Add the iSCSI IP addresses and WWPNs to the storage node info + self._assistant.add_iscsi_ip_addrs(self._state['storage_nodes']) + self._assistant.add_fc_wwpns(self._state['storage_nodes']) + + # For each node, check what connection modes it supports. Delete any + # nodes that do not support any types (may be partially configured). + to_delete = [] + for k, node in self._state['storage_nodes'].items(): + if ((len(node['ipv4']) or len(node['ipv6'])) and + len(node['iscsi_name'])): + node['enabled_protocols'].append('iSCSI') + self._state['enabled_protocols'].add('iSCSI') + if len(node['WWPN']): + node['enabled_protocols'].append('FC') + self._state['enabled_protocols'].add('FC') + if not len(node['enabled_protocols']): + to_delete.append(k) + for delkey in to_delete: + del self._state['storage_nodes'][delkey] + + def _get_backend_pools(self): + if not self._active_backend_id: + return self.configuration.instorage_mcs_volpool_name + elif not self._secondary_pools: + self._secondary_pools = [self._replica_target.get('pool_name')] + return self._secondary_pools + + def _validate_pools_exist(self): + # Validate that the pool exists + pools = self._get_backend_pools() + for pool in pools: + try: + self._assistant.get_pool_attrs(pool) + except exception.VolumeBackendAPIException: + msg = _('Failed getting details for pool %s.') % pool + raise exception.InvalidInput(reason=msg) + + @cinder_utils.trace + def check_for_setup_error(self): + """Ensure that the flags are set properly.""" + + # Check that we have the system ID information + if self._state['system_name'] is None: + exception_msg = _('Unable to determine system name.') + raise exception.VolumeBackendAPIException(data=exception_msg) + if self._state['system_id'] is None: + exception_msg = _('Unable to determine system id.') + raise exception.VolumeBackendAPIException(data=exception_msg) + + # Make sure we have at least one node configured + if not len(self._state['storage_nodes']): + msg = _('do_setup: No configured nodes.') + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + if self.protocol not in self._state['enabled_protocols']: + raise exception.InvalidInput( + reason=_('The storage device does not support %(prot)s. ' + 'Please configure the device to support %(prot)s or ' + 'switch to a driver using a different protocol.') + % {'prot': self.protocol}) + + required_flags = ['san_ip', 'san_ssh_port', 'san_login', + 'instorage_mcs_volpool_name'] + for flag in required_flags: + if not self.configuration.safe_get(flag): + raise exception.InvalidInput(reason=_('%s is not set.') % flag) + + # Ensure that either password or keyfile were set + if not (self.configuration.san_password or + self.configuration.san_private_key): + raise exception.InvalidInput( + reason=_('Password or SSH private key is required for ' + 'authentication: set either san_password or ' + 'san_private_key option.')) + + opts = self._assistant.build_default_opts(self.configuration) + self._assistant.check_vdisk_opts(self._state, opts) + + def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1): + """SSH tool""" + cinder_utils.check_ssh_injection(cmd_list) + command = ' '.join(cmd_list) + if not self.sshpool: + try: + self.sshpool = self._set_up_sshpool(self.active_ip) + except paramiko.SSHException: + LOG.warning('Unable to use san_ip to create SSHPool. Now ' + 'attempting to use instorage_san_secondary_ip ' + 'to create SSHPool.') + if self._switch_ip(): + self.sshpool = self._set_up_sshpool(self.active_ip) + else: + LOG.error('Unable to create SSHPool using san_ip ' + 'and not able to use ' + 'instorage_san_secondary_ip since it is ' + 'not configured.') + raise + try: + return self._ssh_execute(self.sshpool, command, + check_exit_code, attempts) + + except Exception: + # Need to check if creating an SSHPool instorage_san_secondary_ip + # before raising an error. + try: + if self._switch_ip(): + LOG.warning("Unable to execute SSH command with " + "%(inactive)s. Attempting to execute SSH " + "command with %(active)s.", + {'inactive': self.inactive_ip, + 'active': self.active_ip}) + self.sshpool = self._set_up_sshpool(self.active_ip) + return self._ssh_execute(self.sshpool, command, + check_exit_code, attempts) + else: + LOG.warning('Not able to use ' + 'instorage_san_secondary_ip since it is ' + 'not configured.') + raise + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Error running SSH command: %s", + command) + + def _set_up_sshpool(self, ip): + password = self.configuration.san_password + privatekey = self.configuration.san_private_key + min_size = self.configuration.ssh_min_pool_conn + max_size = self.configuration.ssh_max_pool_conn + sshpool = ssh_utils.SSHPool( + ip, + self.configuration.san_ssh_port, + self.configuration.ssh_conn_timeout, + self.configuration.san_login, + password=password, + privatekey=privatekey, + min_size=min_size, + max_size=max_size) + + return sshpool + + def _ssh_execute(self, sshpool, command, + check_exit_code=True, attempts=1): + try: + with sshpool.item() as ssh: + while attempts > 0: + attempts -= 1 + try: + return processutils.ssh_execute( + ssh, + command, + check_exit_code=check_exit_code) + except Exception as e: + LOG.exception('Error has occurred') + last_exception = e + greenthread.sleep(self.DEFAULT_GR_SLEEP) + try: + raise processutils.ProcessExecutionError( + exit_code=last_exception.exit_code, + stdout=last_exception.stdout, + stderr=last_exception.stderr, + cmd=last_exception.cmd) + except AttributeError: + raise processutils.ProcessExecutionError( + exit_code=-1, + stdout="", + stderr="Error running SSH command", + cmd=command) + + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Error running SSH command: %s", command) + + def _switch_ip(self): + # Change active_ip if instorage_san_secondary_ip is set. + if self.configuration.instorage_san_secondary_ip is None: + return False + + self.inactive_ip, self.active_ip = self.active_ip, self.inactive_ip + LOG.info('Switch active_ip from %(old)s to %(new)s.', + {'old': self.inactive_ip, + 'new': self.active_ip}) + return True + + def ensure_export(self, ctxt, volume): + """Check that the volume exists on the storage.""" + vol_name = self._get_target_vol(volume) + volume_defined = self._assistant.is_vdisk_defined(vol_name) + + if not volume_defined: + LOG.error('ensure_export: Volume %s not found on storage.', + volume['name']) + + def create_export(self, ctxt, volume, connector): + pass + + def remove_export(self, ctxt, volume): + pass + + def _get_vdisk_params(self, type_id, volume_type=None, + volume_metadata=None): + return self._assistant.get_vdisk_params( + self.configuration, + self._state, + type_id, + volume_type=volume_type, + volume_metadata=volume_metadata) + + @cinder_utils.trace + def create_volume(self, volume): + opts = self._get_vdisk_params( + volume.volume_type_id, + volume_metadata=volume.get('volume_metadata')) + pool = utils.extract_host(volume.host, 'pool') + + opts['iogrp'] = self._assistant.select_io_group(self._state, opts) + self._assistant.create_vdisk(volume.name, six.text_type(volume.size), + 'gb', pool, opts) + if opts['qos']: + self._assistant.add_vdisk_qos(volume.name, opts['qos']) + + model_update = None + ctxt = context.get_admin_context() + rep_type = self._get_volume_replicated_type(ctxt, volume) + + if rep_type: + replica_obj = self._get_replica_obj(rep_type) + replica_obj.volume_replication_setup(ctxt, volume) + model_update = { + 'replication_status': fields.ReplicationStatus.ENABLED} + + return model_update + + def create_volume_from_snapshot(self, volume, snapshot): + if snapshot.volume_size > volume.size: + msg = (_("create_volume_from_snapshot: snapshot %(snapshot_name)s " + "size is %(snapshot_size)dGB and doesn't fit in target " + "volume %(volume_name)s of size %(volume_size)dGB.") % + {'snapshot_name': snapshot.name, + 'snapshot_size': snapshot.volume_size, + 'volume_name': volume.name, + 'volume_size': volume.size}) + LOG.error(msg) + raise exception.InvalidInput(message=msg) + + opts = self._get_vdisk_params( + volume.volume_type_id, + volume_metadata=volume.get('volume_metadata')) + pool = utils.extract_host(volume.host, 'pool') + self._assistant.create_copy(snapshot.name, volume.name, + snapshot.id, self.configuration, + opts, True, pool=pool) + # The volume size is equal to the snapshot size in most + # of the cases. But in some scenario, the volume size + # may be bigger than the source volume size. + # InStorage does not support localcopy between two volumes + # with two different size. So InStorage will copy volume + # from snapshot first and then extend the volume to + # the target size. + if volume.size > snapshot.volume_size: + # extend the new created target volume to expected size. + self._extend_volume_op(volume, volume.size, + snapshot.volume_size) + if opts['qos']: + self._assistant.add_vdisk_qos(volume.name, opts['qos']) + + ctxt = context.get_admin_context() + rep_type = self._get_volume_replicated_type(ctxt, volume) + + if rep_type: + self._validate_replication_enabled() + replica_obj = self._get_replica_obj(rep_type) + replica_obj.volume_replication_setup(ctxt, volume) + return {'replication_status': fields.ReplicationStatus.ENABLED} + + def create_cloned_volume(self, tgt_volume, src_volume): + """Creates a clone of the specified volume.""" + + if src_volume.size > tgt_volume.size: + msg = (_("create_cloned_volume: source volume %(src_vol)s " + "size is %(src_size)dGB and doesn't fit in target " + "volume %(tgt_vol)s of size %(tgt_size)dGB.") % + {'src_vol': src_volume.name, + 'src_size': src_volume.size, + 'tgt_vol': tgt_volume.name, + 'tgt_size': tgt_volume.size}) + LOG.error(msg) + raise exception.InvalidInput(message=msg) + + opts = self._get_vdisk_params( + tgt_volume.volume_type_id, + volume_metadata=tgt_volume.get('volume_metadata')) + pool = utils.extract_host(tgt_volume.host, 'pool') + self._assistant.create_copy(src_volume.name, tgt_volume.name, + src_volume.id, self.configuration, + opts, True, pool=pool) + + # The source volume size is equal to target volume size + # in most of the cases. But in some scenarios, the target + # volume size may be bigger than the source volume size. + # InStorage does not support localcopy between two volumes + # with two different sizes. So InStorage will copy volume + # from source volume first and then extend target + # volume to original size. + if tgt_volume.size > src_volume.size: + # extend the new created target volume to expected size. + self._extend_volume_op(tgt_volume, tgt_volume.size, + src_volume.size) + + if opts['qos']: + self._assistant.add_vdisk_qos(tgt_volume.name, opts['qos']) + + ctxt = context.get_admin_context() + rep_type = self._get_volume_replicated_type(ctxt, tgt_volume) + + if rep_type: + self._validate_replication_enabled() + replica_obj = self._get_replica_obj(rep_type) + replica_obj.volume_replication_setup(ctxt, tgt_volume) + return {'replication_status': fields.ReplicationStatus.ENABLED} + + def extend_volume(self, volume, new_size): + self._extend_volume_op(volume, new_size) + + @cinder_utils.trace + def _extend_volume_op(self, volume, new_size, old_size=None): + volume_name = self._get_target_vol(volume) + ret = self._assistant.ensure_vdisk_no_lc_mappings(volume_name, + allow_snaps=False) + if not ret: + msg = (_('_extend_volume_op: Extending a volume with snapshots is ' + 'not supported.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + if old_size is None: + old_size = volume.size + extend_amt = int(new_size) - old_size + + rel_info = self._assistant.get_relationship_info(volume_name) + if rel_info: + LOG.warning('_extend_volume_op: Extending a volume with ' + 'remote copy is not recommended.') + try: + tgt_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + rep_type = rel_info['copy_type'] + self._local_backend_assistant.delete_relationship( + volume.name) + self._local_backend_assistant.extend_vdisk(volume.name, + extend_amt) + self._aux_backend_assistant.extend_vdisk(tgt_vol, extend_amt) + tgt_sys = self._aux_backend_assistant.get_system_info() + self._local_backend_assistant.create_relationship( + volume.name, tgt_vol, tgt_sys.get('system_name'), + True if instorage_const.ASYNC == rep_type else False) + except Exception as e: + msg = (_('Failed to extend a volume with remote copy ' + '%(volume)s. Exception: ' + '%(err)s.') % {'volume': volume.id, + 'err': six.text_type(e)}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + else: + self._assistant.extend_vdisk(volume_name, extend_amt) + + @cinder_utils.trace + def delete_volume(self, volume): + ctxt = context.get_admin_context() + + rep_type = self._get_volume_replicated_type(ctxt, volume) + if rep_type: + self._aux_backend_assistant.delete_rc_volume(volume.name, + target_vol=True) + if not self._active_backend_id: + self._local_backend_assistant.delete_rc_volume(volume.name) + else: + # If it's in fail over state, also try to delete the volume + # in master backend + try: + self._local_backend_assistant.delete_rc_volume( + volume.name) + except Exception as ex: + LOG.error('Failed to get delete volume %(volume)s in ' + 'master backend. Exception: %(err)s.', + {'volume': volume.name, 'err': ex}) + else: + if self._active_backend_id: + msg = (_('Error: delete non-replicate volume in failover mode' + ' is not allowed.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + else: + self._assistant.delete_vdisk(volume.name, False) + + if volume.id in self._vdiskcopyops: + del self._vdiskcopyops[volume.id] + + if not self._vdiskcopyops: + self._vdiskcopyops_loop.stop() + self._vdiskcopyops_loop = None + + def create_snapshot(self, snapshot): + source_vol = snapshot.volume + pool = utils.extract_host(source_vol.host, 'pool') + opts = self._get_vdisk_params(source_vol.volume_type_id) + self._assistant.create_copy(snapshot.volume_name, snapshot.name, + snapshot.volume_id, self.configuration, + opts, False, pool=pool) + + def delete_snapshot(self, snapshot): + self._assistant.delete_vdisk(snapshot.name, False) + + def add_vdisk_copy(self, volume, dest_pool, vol_type): + return self._assistant.add_vdisk_copy(volume, dest_pool, + vol_type, self._state, + self.configuration) + + def _add_vdisk_copy_op(self, ctxt, volume, new_op): + if volume.id in self._vdiskcopyops: + self._vdiskcopyops[volume.id]['copyops'].append(new_op) + else: + self._vdiskcopyops[volume.id] = {'name': volume.name, + 'copyops': [new_op]} + + # We added the first copy operation, so start the looping call + if len(self._vdiskcopyops) == 1: + self._vdiskcopyops_loop = loopingcall.FixedIntervalLoopingCall( + self._check_volume_copy_ops) + self._vdiskcopyops_loop.start(interval=self.VDISKCOPYOPS_INTERVAL) + + def _rm_vdisk_copy_op(self, ctxt, vol_id, orig_copy_id, new_copy_id): + try: + self._vdiskcopyops[vol_id]['copyops'].remove((orig_copy_id, + new_copy_id)) + if not self._vdiskcopyops[vol_id]['copyops']: + del self._vdiskcopyops[vol_id] + if not self._vdiskcopyops: + self._vdiskcopyops_loop.stop() + self._vdiskcopyops_loop = None + except KeyError: + LOG.error('_rm_vdisk_copy_op: Volume %s does not have any ' + 'registered vdisk copy operations.', vol_id) + return + except ValueError: + LOG.error('_rm_vdisk_copy_op: Volume %(vol)s does not have ' + 'the specified vdisk copy operation: orig=%(orig)s ' + 'new=%(new)s.', + {'vol': vol_id, 'orig': orig_copy_id, + 'new': new_copy_id}) + return + + def _check_volume_copy_ops(self): + LOG.debug("Enter: update volume copy status.") + ctxt = context.get_admin_context() + copy_items = list(self._vdiskcopyops.items()) + for vol_id, copy_ops_data in copy_items: + vol_name = copy_ops_data['name'] + copy_ops = copy_ops_data['copyops'] + + if not self._assistant.is_vdisk_defined(vol_name): + LOG.warning('Volume %s does not exist.', vol_id) + del self._vdiskcopyops[vol_id] + if not self._vdiskcopyops: + self._vdiskcopyops_loop.stop() + self._vdiskcopyops_loop = None + continue + + for copy_op in copy_ops: + try: + synced = self._assistant.check_vdisk_copy_synced( + vol_name, copy_op[1]) + except Exception: + LOG.info('_check_volume_copy_ops: Volume %(vol)s does ' + 'not have the specified vdisk copy ' + 'operation: orig=%(orig)s new=%(new)s.', + {'vol': vol_id, 'orig': copy_op[0], + 'new': copy_op[1]}) + else: + if synced: + self._assistant.rm_vdisk_copy( + vol_name, copy_op[0]) + self._rm_vdisk_copy_op(ctxt, vol_id, copy_op[0], + copy_op[1]) + LOG.debug("Exit: update volume copy status.") + + @cinder_utils.trace + def migrate_volume(self, ctxt, volume, host): + """Migrate directly if source and dest are managed by same storage. + + We create a new vdisk copy in the desired pool, and add the original + vdisk copy to the admin_metadata of the volume to be deleted. The + deletion will occur using a periodic task once the new copy is synced. + + :param ctxt: Context + :param volume: A dictionary describing the volume to migrate + :param host: A dictionary describing the host to migrate to, where + host['host'] is its name, and host['capabilities'] is a + dictionary of its reported capabilities. + """ + false_ret = (False, None) + dest_pool = self._assistant.can_migrate_to_host(host, self._state) + if dest_pool is None: + return false_ret + + ctxt = context.get_admin_context() + volume_type_id = volume.volume_type_id + if volume_type_id is not None: + vol_type = volume_types.get_volume_type(ctxt, volume_type_id) + else: + vol_type = None + + self._check_volume_copy_ops() + new_op = self.add_vdisk_copy(volume.name, dest_pool, vol_type) + self._add_vdisk_copy_op(ctxt, volume, new_op) + return (True, None) + + @cinder_utils.trace + def retype(self, ctxt, volume, new_type, diff, host): + """Convert the volume to be of the new type. + + Returns a boolean indicating whether the retype occurred. + + :param ctxt: Context + :param volume: A volume object describing the volume to migrate + :param new_type: A dictionary describing the volume type to convert to + :param diff: A dictionary with the difference between the two types + :param host: A dictionary describing the host to migrate to, where + host['host'] is its name, and host['capabilities'] is a + dictionary of its reported capabilities. + """ + def retype_iogrp_property(volume, new, old): + if new != old: + self._assistant.change_vdisk_iogrp(volume.name, + self._state, (new, old)) + + no_copy_keys = ['warning', 'autoexpand', 'intier'] + copy_keys = ['rsize', 'grainsize', 'compression'] + all_keys = no_copy_keys + copy_keys + old_opts = self._get_vdisk_params( + volume.volume_type_id, + volume_metadata=volume.get('volume_matadata')) + new_opts = self._get_vdisk_params(new_type['id'], + volume_type=new_type) + + vdisk_changes = [] + need_copy = False + for key in all_keys: + if old_opts[key] != new_opts[key]: + if key in copy_keys: + need_copy = True + break + elif key in no_copy_keys: + vdisk_changes.append(key) + + if (utils.extract_host(volume.host, 'pool') != + utils.extract_host(host['host'], 'pool')): + need_copy = True + + # Check if retype affects volume replication + model_update = None + new_rep_type = self._get_specs_replicated_type(new_type) + old_rep_type = self._get_volume_replicated_type(ctxt, volume) + old_io_grp = self._assistant.get_volume_io_group(volume.name) + + # There are three options for rep_type: None, sync, async + if new_rep_type != old_rep_type: + if (old_io_grp not in + InStorageAssistant._get_valid_requested_io_groups( + self._state, new_opts)): + msg = (_('Unable to retype: it is not allowed to change ' + 'replication type and io group at the same time.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + if new_rep_type and old_rep_type: + msg = (_('Unable to retype: it is not allowed to change ' + '%(old_rep_type)s volume to %(new_rep_type)s ' + 'volume.') % + {'old_rep_type': old_rep_type, + 'new_rep_type': new_rep_type}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + # If volume is replicated, can't copy + if need_copy: + msg = (_('Unable to retype: Current action needs volume-copy,' + ' it is not allowed when new type is replication.' + ' Volume = %s') % volume.id) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + new_io_grp = self._assistant.select_io_group(self._state, new_opts) + + if need_copy: + self._check_volume_copy_ops() + dest_pool = self._assistant.can_migrate_to_host(host, self._state) + if dest_pool is None: + return False + + retype_iogrp_property(volume, + new_io_grp, old_io_grp) + try: + new_op = self.add_vdisk_copy(volume.name, + dest_pool, + new_type) + self._add_vdisk_copy_op(ctxt, volume, new_op) + except exception.VolumeDriverException: + # roll back changing iogrp property + retype_iogrp_property(volume, old_io_grp, new_io_grp) + msg = (_('Unable to retype: A copy of volume %s exists. ' + 'Retyping would exceed the limit of 2 copies.'), + volume.id) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + else: + retype_iogrp_property(volume, new_io_grp, old_io_grp) + + self._assistant.change_vdisk_options(volume.name, vdisk_changes, + new_opts, self._state) + + if new_opts['qos']: + # Add the new QoS setting to the volume. If the volume has an + # old QoS setting, it will be overwritten. + self._assistant.update_vdisk_qos(volume.name, new_opts['qos']) + elif old_opts['qos']: + # If the old_opts contain QoS keys, disable them. + self._assistant.disable_vdisk_qos(volume.name, old_opts['qos']) + + # Delete replica if needed + if old_rep_type and not new_rep_type: + self._aux_backend_assistant.delete_rc_volume(volume.name, + target_vol=True) + model_update = { + 'replication_status': fields.ReplicationStatus.DISABLED, + 'replication_driver_data': None, + 'replication_extended_status': None} + # Add replica if needed + if not old_rep_type and new_rep_type: + replica_obj = self._get_replica_obj(new_rep_type) + replica_obj.volume_replication_setup(ctxt, volume) + model_update = { + 'replication_status': fields.ReplicationStatus.ENABLED} + + return True, model_update + + def update_migrated_volume(self, ctxt, volume, new_volume, + original_volume_status): + """Return model update from InStorage for migrated volume. + + This method should rename the back-end volume name(id) on the + destination host back to its original name(id) on the source host. + + :param ctxt: The context used to run the method update_migrated_volume + :param volume: The original volume that was migrated to this backend + :param new_volume: The migration volume object that was created on + this backend as part of the migration process + :param original_volume_status: The status of the original volume + :returns: model_update to update DB with any needed changes + """ + current_name = CONF.volume_name_template % new_volume.id + original_volume_name = CONF.volume_name_template % volume.id + try: + self._assistant.rename_vdisk(current_name, original_volume_name) + except exception.VolumeBackendAPIException: + LOG.error('Unable to rename the logical volume ' + 'for volume: %s', volume.id) + return {'_name_id': new_volume._name_id or new_volume.id} + # If the back-end name(id) for the volume has been renamed, + # it is OK for the volume to keep the original name(id) and there is + # no need to use the column "_name_id" to establish the mapping + # relationship between the volume id and the back-end volume + # name(id). + # Set the key "_name_id" to None for a successful rename. + model_update = {'_name_id': None} + return model_update + + def manage_existing(self, volume, ref): + """Manages an existing vdisk. + + Renames the vdisk to match the expected name for the volume. + Error checking done by manage_existing_get_size is not repeated - + if we got here then we have a vdisk that isn't in use (or we don't + care if it is in use. + """ + # Check that the reference is valid + vdisk = self._manage_input_check(ref) + vdisk_io_grp = self._assistant.get_volume_io_group(vdisk['name']) + if vdisk_io_grp not in self._state['available_iogrps']: + msg = (_("Failed to manage existing volume due to " + "the volume to be managed is not in a valid " + "I/O group.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + # Add replication check + ctxt = context.get_admin_context() + rep_type = self._get_volume_replicated_type(ctxt, volume) + vol_rep_type = None + rel_info = self._assistant.get_relationship_info(vdisk['name']) + if rel_info: + vol_rep_type = rel_info['copy_type'] + aux_info = self._aux_backend_assistant.get_system_info() + if rel_info['aux_cluster_id'] != aux_info['system_id']: + msg = (_("Failed to manage existing volume due to the aux " + "cluster for volume %(volume)s is %(aux_id)s. The " + "configured cluster id is %(cfg_id)s") % + {'volume': vdisk['name'], + 'aux_id': rel_info['aux_cluster_id'], + 'cfg_id': aux_info['system_id']}) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if vol_rep_type != rep_type: + msg = (_("Failed to manage existing volume due to " + "the replication type of the volume to be managed is " + "mismatch with the provided replication type.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if volume.volume_type_id: + opts = self._get_vdisk_params( + volume.volume_type_id, + volume_metadata=volume.get('volume_metadata')) + vdisk_copy = self._assistant.get_vdisk_copy_attrs( + vdisk['name'], '0') + + if vdisk_copy['autoexpand'] == 'on' and opts['rsize'] == -1: + msg = (_("Failed to manage existing volume due to " + "the volume to be managed is thin, but " + "the volume type chosen is thick.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if not vdisk_copy['autoexpand'] and opts['rsize'] != -1: + msg = (_("Failed to manage existing volume due to " + "the volume to be managed is thick, but " + "the volume type chosen is thin.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if (vdisk_copy['compressed_copy'] == 'no' and + opts['compression']): + msg = (_("Failed to manage existing volume due to the " + "volume to be managed is not compress, but " + "the volume type chosen is compress.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if (vdisk_copy['compressed_copy'] == 'yes' and + not opts['compression']): + msg = (_("Failed to manage existing volume due to the " + "volume to be managed is compress, but " + "the volume type chosen is not compress.")) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + if (vdisk_io_grp not in + InStorageAssistant._get_valid_requested_io_groups( + self._state, opts)): + msg = (_("Failed to manage existing volume due to " + "I/O group mismatch. The I/O group of the " + "volume to be managed is %(vdisk_iogrp)s. I/O group" + "of the chosen type is %(opt_iogrp)s.") % + {'vdisk_iogrp': vdisk['IO_group_name'], + 'opt_iogrp': opts['iogrp']}) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + pool = utils.extract_host(volume.host, 'pool') + if vdisk['mdisk_grp_name'] != pool: + msg = (_("Failed to manage existing volume due to the " + "pool of the volume to be managed does not " + "match the backend pool. Pool of the " + "volume to be managed is %(vdisk_pool)s. Pool " + "of the backend is %(backend_pool)s.") % + {'vdisk_pool': vdisk['mdisk_grp_name'], + 'backend_pool': + self._get_backend_pools()}) + raise exception.ManageExistingVolumeTypeMismatch(reason=msg) + + model_update = {} + self._assistant.rename_vdisk(vdisk['name'], volume.name) + if vol_rep_type: + aux_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + self._aux_backend_assistant.rename_vdisk( + rel_info['aux_vdisk_name'], aux_vol) + model_update = { + 'replication_status': fields.ReplicationStatus.ENABLED} + return model_update + + def manage_existing_get_size(self, volume, ref): + """Return size of an existing Vdisk for manage_existing. + + existing_ref is a dictionary of the form: + {'source-id': } or + {'source-name': } + + Optional elements are: + 'manage_if_in_use': True/False (default is False) + If set to True, a volume will be managed even if it is currently + attached to a host system. + """ + + # Check that the reference is valid + vdisk = self._manage_input_check(ref) + + # Check if the disk is in use, if we need to. + manage_if_in_use = ref.get('manage_if_in_use', False) + if (not manage_if_in_use and + self._assistant.is_vdisk_in_use(vdisk['name'])): + reason = _('The specified vdisk is mapped to a host.') + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + + return int(math.ceil(float(vdisk['capacity']) / units.Gi)) + + def unmanage(self, volume): + """Remove the specified volume from Cinder management.""" + pass + + def get_volume_stats(self, refresh=False): + """Get volume stats. + + If we haven't gotten stats yet or 'refresh' is True, + run update the stats first. + """ + if not self._stats or refresh: + self._update_volume_stats() + + return self._stats + + # ## Group method ## # + def create_group(self, context, group): + """Create a group. + + Inspur InStorage will create group until group-snapshot creation, + db will maintain the volumes and group relationship. + """ + + # now we only support consistent group + if not utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + + LOG.debug("Creating group.") + model_update = {'status': fields.GroupStatus.AVAILABLE} + return model_update + + def create_group_from_src(self, context, group, volumes, + group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + """Creates a group from source. + + :param context: the context of the caller. + :param group: the dictionary of the group to be created. + :param volumes: a list of volume dictionaries in the group. + :param group_snapshot: the dictionary of the group_snapshot as source. + :param snapshots: a list of snapshot dictionaries + in the group_snapshot. + :param source_group: the dictionary of a group as source. + :param source_vols: a list of volume dictionaries in the source_group. + :returns: model_update, volumes_model_update + """ + + # now we only support consistent group + if not utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + + LOG.debug('Enter: create_group_from_src.') + if group_snapshot and snapshots: + group_name = 'group-' + group_snapshot.id + sources = snapshots + + elif source_group and source_vols: + group_name = 'group-' + source_group.id + sources = source_vols + + else: + error_msg = _("create_group_from_src must be creating from" + " a group snapshot, or a source group.") + raise exception.InvalidInput(reason=error_msg) + + LOG.debug('create_group_from_src: group_name %(group_name)s' + ' %(sources)s', {'group_name': group_name, + 'sources': sources}) + self._assistant.create_lc_consistgrp(group_name) # create group + timeout = self.configuration.instorage_mcs_localcopy_timeout + model_update, snapshots_model = ( + self._assistant.create_group_from_source(group, group_name, + sources, volumes, + self._state, + self.configuration, + timeout)) + LOG.debug("Leave: create_group_from_src.") + return model_update, snapshots_model + + def delete_group(self, context, group, volumes): + """Deletes a group. + + Inspur InStorage will delete the volumes of the group. + """ + + # now we only support consistent group + if not utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + + LOG.debug("Deleting group.") + model_update = {'status': fields.ConsistencyGroupStatus.DELETED} + volumes_model_update = [] + + for volume in volumes: + try: + self._assistant.delete_vdisk(volume.name, True) + volumes_model_update.append( + {'id': volume.id, + 'status': fields.ConsistencyGroupStatus.DELETED}) + except exception.VolumeBackendAPIException as err: + model_update['status'] = ( + fields.ConsistencyGroupStatus.ERROR_DELETING) + LOG.error("Failed to delete the volume %(vol)s of group. " + "Exception: %(exception)s.", + {'vol': volume.name, 'exception': err}) + volumes_model_update.append( + {'id': volume.id, + 'status': fields.ConsistencyGroupStatus.ERROR_DELETING}) + + return model_update, volumes_model_update + + def update_group(self, ctxt, group, add_volumes=None, + remove_volumes=None): + """Adds or removes volume(s) to/from an existing group.""" + + if not utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + + LOG.debug("Updating group.") + # as we don't keep group info on device, nonthing need to be done + return None, None, None + + def create_group_snapshot(self, ctxt, group_snapshot, snapshots): + """Creates a cgsnapshot.""" + + # now we only support consistent group + if not utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() + + # Use cgsnapshot id as cg name + group_name = 'group_snap-' + group_snapshot.id + # Create new cg as cg_snapshot + self._assistant.create_lc_consistgrp(group_name) + + timeout = self.configuration.instorage_mcs_localcopy_timeout + model_update, snapshots_model = ( + self._assistant.run_group_snapshots(group_name, + snapshots, + self._state, + self.configuration, + timeout)) + + return model_update, snapshots_model + + def delete_group_snapshot(self, context, group_snapshot, snapshots): + """Deletes a cgsnapshot.""" + + # now we only support consistent group + if not utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() + + group_snapshot_id = group_snapshot.id + group_name = 'group_snap-' + group_snapshot_id + model_update, snapshots_model = ( + self._assistant.delete_group_snapshots(group_name, + snapshots)) + + return model_update, snapshots_model + + def get_pool(self, volume): + attr = self._assistant.get_vdisk_attributes(volume.name) + + if attr is None: + msg = (_('get_pool: Failed to get attributes for volume ' + '%s') % volume.id) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + return attr['mdisk_grp_name'] + + def _update_volume_stats(self): + """Retrieve stats info from volume group.""" + + LOG.debug("Updating volume stats.") + data = {} + + data['vendor_name'] = 'Inspur' + data['driver_version'] = self.VERSION + data['storage_protocol'] = self.protocol + data['pools'] = [] + + backend_name = self.configuration.safe_get('volume_backend_name') + data['volume_backend_name'] = (backend_name or + self._state['system_name']) + + data['pools'] = [self._build_pool_stats(pool) + for pool in + self._get_backend_pools()] + if self._replica_enabled: + data['replication'] = self._replica_enabled + data['replication_enabled'] = self._replica_enabled + data['replication_targets'] = self._get_replication_targets() + self._stats = data + + def _build_pool_stats(self, pool): + """Build pool status""" + QoS_support = True + pool_stats = {} + try: + pool_data = self._assistant.get_pool_attrs(pool) + if pool_data: + in_tier = pool_data['in_tier'] in ['on', 'auto'] + total_capacity_gb = float(pool_data['capacity']) / units.Gi + free_capacity_gb = float(pool_data['free_capacity']) / units.Gi + allocated_capacity_gb = (float(pool_data['used_capacity']) / + units.Gi) + provisioned_capacity_gb = float( + pool_data['virtual_capacity']) / units.Gi + + rsize = self.configuration.safe_get( + 'instorage_mcs_vol_rsize') + # rsize of -1 or 100 means fully allocate the mdisk + use_thick_provisioning = rsize == -1 or rsize == 100 + over_sub_ratio = self.configuration.safe_get( + 'max_over_subscription_ratio') + location_info = ('InStorageMCSDriver:%(sys_id)s:%(pool)s' % + {'sys_id': self._state['system_id'], + 'pool': pool_data['name']}) + pool_stats = { + 'pool_name': pool_data['name'], + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'allocated_capacity_gb': allocated_capacity_gb, + 'provisioned_capacity_gb': provisioned_capacity_gb, + 'compression_support': self._state['compression_enabled'], + 'reserved_percentage': + self.configuration.reserved_percentage, + 'QoS_support': QoS_support, + 'consistent_group_snapshot_enabled': True, + 'location_info': location_info, + 'intier_support': in_tier, + 'multiattach': False, + 'thin_provisioning_support': not use_thick_provisioning, + 'thick_provisioning_support': use_thick_provisioning, + 'max_over_subscription_ratio': over_sub_ratio, + } + if self._replica_enabled: + pool_stats.update({ + 'replication_enabled': self._replica_enabled, + 'replication_type': self._supported_replica_types, + 'replication_targets': self._get_replication_targets(), + 'replication_count': len(self._get_replication_targets()) + }) + + except exception.VolumeBackendAPIException: + msg = _('Failed getting details for pool %s.') % pool + raise exception.VolumeBackendAPIException(data=msg) + + return pool_stats + + def _get_replication_targets(self): + return [self._replica_target['backend_id']] + + def _manage_input_check(self, ref): + """Verify the input of manage function.""" + # Check that the reference is valid + if 'source-name' in ref: + manage_source = ref['source-name'] + vdisk = self._assistant.get_vdisk_attributes(manage_source) + elif 'source-id' in ref: + manage_source = ref['source-id'] + vdisk = self._assistant.vdisk_by_uid(manage_source) + else: + reason = _('Reference must contain source-id or ' + 'source-name element.') + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + + if vdisk is None: + reason = (_('No vdisk with the UID specified by ref %s.') + % manage_source) + raise exception.ManageExistingInvalidReference(existing_ref=ref, + reason=reason) + return vdisk + + # #### V2.1 replication methods #### # + @cinder_utils.trace + def failover_host(self, context, volumes, secondary_id=None): + if not self._replica_enabled: + msg = _("Replication is not properly enabled on backend.") + LOG.error(msg) + raise exception.UnableToFailOver(reason=msg) + + if instorage_const.FAILBACK_VALUE == secondary_id: + # In this case the administrator would like to fail back. + secondary_id, volumes_update = self._replication_failback(context, + volumes) + elif (secondary_id == self._replica_target['backend_id'] or + secondary_id is None): + # In this case the administrator would like to fail over. + secondary_id, volumes_update = self._replication_failover(context, + volumes) + else: + msg = (_("Invalid secondary id %s.") % secondary_id) + LOG.error(msg) + raise exception.InvalidReplicationTarget(reason=msg) + + return secondary_id, volumes_update + + def _replication_failback(self, ctxt, volumes): + """Fail back all the volume on the secondary backend.""" + volumes_update = [] + if not self._active_backend_id: + LOG.info("Host has been failed back. doesn't need " + "to fail back again") + return None, volumes_update + + try: + self._local_backend_assistant.get_system_info() + except Exception: + msg = (_("Unable to failback due to primary is not reachable.")) + LOG.error(msg) + raise exception.UnableToFailOver(reason=msg) + + normal_volumes, rep_volumes = self._classify_volume(ctxt, volumes) + + # start synchronize from aux volume to master volume + self._sync_with_aux(ctxt, rep_volumes) + self._wait_replica_ready(ctxt, rep_volumes) + + rep_volumes_update = self._failback_replica_volumes(ctxt, + rep_volumes) + volumes_update.extend(rep_volumes_update) + + normal_volumes_update = self._failback_normal_volumes(normal_volumes) + volumes_update.extend(normal_volumes_update) + + self._assistant = self._local_backend_assistant + self._active_backend_id = None + + # Update the instorage state + self._update_instorage_state() + self._update_volume_stats() + return instorage_const.FAILBACK_VALUE, volumes_update + + @cinder_utils.trace + def _failback_replica_volumes(self, ctxt, rep_volumes): + volumes_update = [] + + for volume in rep_volumes: + rep_type = self._get_volume_replicated_type(ctxt, volume) + replica_obj = self._get_replica_obj(rep_type) + tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + rep_info = self._assistant.get_relationship_info(tgt_volume) + if not rep_info: + replication_status = fields.ReplicationStatus.FAILOVER_ERROR + volumes_update.append( + {'volume_id': volume.id, + 'updates': { + 'replication_status': replication_status, + 'status': 'error'}}) + LOG.error('_failback_replica_volumes:no rc-releationship ' + 'is established between master: %(master)s and ' + 'aux %(aux)s. Please re-establish the ' + 'relationship and synchronize the volumes on ' + 'backend storage.', + {'master': volume.name, 'aux': tgt_volume}) + continue + LOG.debug('_failover_replica_volumes: vol=%(vol)s, master_vol=' + '%(master_vol)s, aux_vol=%(aux_vol)s, state=%(state)s' + 'primary=%(primary)s', + {'vol': volume.name, + 'master_vol': rep_info['master_vdisk_name'], + 'aux_vol': rep_info['aux_vdisk_name'], + 'state': rep_info['state'], + 'primary': rep_info['primary']}) + try: + model_updates = replica_obj.replication_failback(volume) + volumes_update.append( + {'volume_id': volume.id, + 'updates': model_updates}) + except exception.VolumeDriverException: + LOG.error('Unable to fail back volume %(volume_id)s', + {'volume_id': volume.id}) + replication_status = fields.ReplicationStatus.FAILOVER_ERROR + volumes_update.append( + {'volume_id': volume.id, + 'updates': {'replication_status': replication_status, + 'status': 'error'}}) + return volumes_update + + def _failback_normal_volumes(self, normal_volumes): + volumes_update = [] + for vol in normal_volumes: + pre_status = 'available' + if ('replication_driver_data' in vol and + vol.replication_driver_data): + rep_data = json.loads(vol.replication_driver_data) + pre_status = rep_data['previous_status'] + volumes_update.append( + {'volume_id': vol.id, + 'updates': {'status': pre_status, + 'replication_driver_data': ''}}) + return volumes_update + + @cinder_utils.trace + def _sync_with_aux(self, ctxt, volumes): + try: + rep_mgr = self._get_replica_mgr() + rep_mgr.establish_target_partnership() + except Exception as ex: + LOG.warning('Fail to establish partnership in backend. ' + 'error=%(ex)s', {'error': ex}) + for volume in volumes: + tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + rep_info = self._assistant.get_relationship_info(tgt_volume) + if not rep_info: + LOG.error('_sync_with_aux: no rc-releationship is ' + 'established between master: %(master)s and aux ' + '%(aux)s. Please re-establish the relationship ' + 'and synchronize the volumes on backend ' + 'storage.', {'master': volume.name, + 'aux': tgt_volume}) + continue + LOG.debug('_sync_with_aux: volume: %(volume)s rep_info:master_vol=' + '%(master_vol)s, aux_vol=%(aux_vol)s, state=%(state)s, ' + 'primary=%(primary)s', + {'volume': volume.name, + 'master_vol': rep_info['master_vdisk_name'], + 'aux_vol': rep_info['aux_vdisk_name'], + 'state': rep_info['state'], + 'primary': rep_info['primary']}) + try: + if rep_info['state'] != instorage_const.REP_CONSIS_SYNC: + if rep_info['primary'] == 'master': + self._assistant.start_relationship(tgt_volume) + else: + self._assistant.start_relationship(tgt_volume, + primary='aux') + except Exception as ex: + LOG.warning('Fail to copy data from aux to master. master:' + ' %(master)s and aux %(aux)s. Please ' + 're-establish the relationship and synchronize' + ' the volumes on backend storage. error=' + '%(ex)s', {'master': volume.name, + 'aux': tgt_volume, + 'error': ex}) + + def _wait_replica_ready(self, ctxt, volumes): + for volume in volumes: + tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + try: + self._wait_replica_vol_ready(ctxt, tgt_volume) + except Exception as ex: + LOG.error('_wait_replica_ready: wait for volume:%(volume)s' + ' remote copy synchronization failed due to ' + 'error:%(err)s.', {'volume': tgt_volume, + 'err': ex}) + + @cinder_utils.trace + def _wait_replica_vol_ready(self, ctxt, volume): + def _replica_vol_ready(): + rep_info = self._assistant.get_relationship_info(volume) + if not rep_info: + msg = (_('_wait_replica_vol_ready: no rc-releationship' + 'is established for volume:%(volume)s. Please ' + 're-establish the rc-relationship and ' + 'synchronize the volumes on backend storage.'), + {'volume': volume}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + LOG.debug('_replica_vol_ready:volume: %(volume)s rep_info: ' + 'master_vol=%(master_vol)s, aux_vol=%(aux_vol)s, ' + 'state=%(state)s, primary=%(primary)s', + {'volume': volume, + 'master_vol': rep_info['master_vdisk_name'], + 'aux_vol': rep_info['aux_vdisk_name'], + 'state': rep_info['state'], + 'primary': rep_info['primary']}) + if rep_info['state'] == instorage_const.REP_CONSIS_SYNC: + return True + if rep_info['state'] == instorage_const.REP_IDL_DISC: + msg = (_('Wait synchronize failed. volume: %(volume)s'), + {'volume': volume}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return False + + self._assistant._wait_for_a_condition( + _replica_vol_ready, timeout=instorage_const.DEFAULT_RC_TIMEOUT, + interval=instorage_const.DEFAULT_RC_INTERVAL, + raise_exception=True) + + def _replication_failover(self, ctxt, volumes): + volumes_update = [] + if self._active_backend_id: + LOG.info("Host has been failed over to %s", + self._active_backend_id) + return self._active_backend_id, volumes_update + + try: + self._aux_backend_assistant.get_system_info() + except Exception as ex: + msg = (_("Unable to failover due to replication target is not " + "reachable. error=%(ex)s"), {'error': ex}) + LOG.error(msg) + raise exception.UnableToFailOver(reason=msg) + + normal_volumes, rep_volumes = self._classify_volume(ctxt, volumes) + + rep_volumes_update = self._failover_replica_volumes(ctxt, rep_volumes) + volumes_update.extend(rep_volumes_update) + + normal_volumes_update = self._failover_normal_volumes(normal_volumes) + volumes_update.extend(normal_volumes_update) + + self._assistant = self._aux_backend_assistant + self._active_backend_id = self._replica_target['backend_id'] + self._secondary_pools = [self._replica_target['pool_name']] + + # Update the instorage state + self._update_instorage_state() + self._update_volume_stats() + return self._active_backend_id, volumes_update + + @cinder_utils.trace + def _failover_replica_volumes(self, ctxt, rep_volumes): + volumes_update = [] + + for volume in rep_volumes: + rep_type = self._get_volume_replicated_type(ctxt, volume) + replica_obj = self._get_replica_obj(rep_type) + # Try do the fail-over. + try: + rep_info = self._aux_backend_assistant.get_relationship_info( + instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name) + if not rep_info: + rep_status = fields.ReplicationStatus.FAILOVER_ERROR + volumes_update.append( + {'volume_id': volume.id, + 'updates': {'replication_status': rep_status, + 'status': 'error'}}) + LOG.error('_failover_replica_volumes: no rc-' + 'releationship is established for master:' + '%(master)s. Please re-establish the rc-' + 'relationship and synchronize the volumes on' + ' backend storage.', + {'master': volume.name}) + continue + LOG.debug('_failover_replica_volumes: vol=%(vol)s, ' + 'master_vol=%(master_vol)s, aux_vol=%(aux_vol)s, ' + 'state=%(state)s, primary=%(primary)s', + {'vol': volume.name, + 'master_vol': rep_info['master_vdisk_name'], + 'aux_vol': rep_info['aux_vdisk_name'], + 'state': rep_info['state'], + 'primary': rep_info['primary']}) + model_updates = replica_obj.failover_volume_host(ctxt, volume) + volumes_update.append( + {'volume_id': volume.id, + 'updates': model_updates}) + except exception.VolumeDriverException: + LOG.error('Unable to failover to aux volume. Please make ' + 'sure that the aux volume is ready.') + volumes_update.append( + {'volume_id': volume.id, + 'updates': {'status': 'error', + 'replication_status': + fields.ReplicationStatus.FAILOVER_ERROR}}) + return volumes_update + + def _failover_normal_volumes(self, normal_volumes): + volumes_update = [] + for volume in normal_volumes: + # If the volume is not of replicated type, we need to + # force the status into error state so a user knows they + # do not have access to the volume. + rep_data = json.dumps({'previous_status': volume.status}) + volumes_update.append( + {'volume_id': volume.id, + 'updates': {'status': 'error', + 'replication_driver_data': rep_data}}) + return volumes_update + + def _classify_volume(self, ctxt, volumes): + normal_volumes = [] + replica_volumes = [] + + for v in volumes: + volume_type = self._get_volume_replicated_type(ctxt, v) + if volume_type and v.status == 'available': + replica_volumes.append(v) + else: + normal_volumes.append(v) + + return normal_volumes, replica_volumes + + def _get_replica_obj(self, rep_type): + replica_manager = self.replica_manager[ + self._replica_target['backend_id']] + return replica_manager.get_replica_obj(rep_type) + + def _get_replica_mgr(self): + replica_manager = self.replica_manager[ + self._replica_target['backend_id']] + return replica_manager + + def _get_target_vol(self, volume): + tgt_vol = volume.name + if self._active_backend_id: + ctxt = context.get_admin_context() + rep_type = self._get_volume_replicated_type(ctxt, volume) + if rep_type: + tgt_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + return tgt_vol + + def _validate_replication_enabled(self): + if not self._replica_enabled: + msg = _("Replication is not properly configured on backend.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def _get_specs_replicated_type(self, volume_type): + replication_type = None + extra_specs = volume_type.get("extra_specs", {}) + rep_val = extra_specs.get('replication_enabled') + if rep_val == " True": + replication_type = extra_specs.get('replication_type', + instorage_const.ASYNC) + # The format for replication_type in extra spec is in + # " async". Otherwise, the code will + # not reach here. + if replication_type != instorage_const.ASYNC: + # Pick up the replication type specified in the + # extra spec from the format like " async". + replication_type = replication_type.split()[1] + if replication_type not in instorage_const.VALID_REP_TYPES: + msg = (_("Invalid replication type %s.") % replication_type) + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + return replication_type + + def _get_volume_replicated_type(self, ctxt, volume): + replication_type = None + if volume.get("volume_type_id"): + volume_type = volume_types.get_volume_type( + ctxt, volume.volume_type_id) + replication_type = self._get_specs_replicated_type(volume_type) + + return replication_type + + def _get_instorage_config(self): + self._do_replication_setup() + + if self._active_backend_id and self._replica_target: + self._assistant = self._aux_backend_assistant + + self._replica_enabled = (True if (self._assistant. + replication_licensed() and + self._replica_target) else False) + if self._replica_enabled: + self._supported_replica_types = instorage_const.VALID_REP_TYPES + + def _do_replication_setup(self): + rep_devs = self.configuration.safe_get('replication_device') + if not rep_devs: + return + + if len(rep_devs) > 1: + raise exception.InvalidInput( + reason=_('Multiple replication devices are configured. ' + 'Now only one replication_device is supported.')) + + required_flags = ['san_ip', 'backend_id', 'san_login', + 'san_password', 'pool_name'] + for flag in required_flags: + if flag not in rep_devs[0]: + raise exception.InvalidInput( + reason=_('%s is not set.') % flag) + + rep_target = {} + rep_target['san_ip'] = rep_devs[0].get('san_ip') + rep_target['backend_id'] = rep_devs[0].get('backend_id') + rep_target['san_login'] = rep_devs[0].get('san_login') + rep_target['san_password'] = rep_devs[0].get('san_password') + rep_target['pool_name'] = rep_devs[0].get('pool_name') + + # Each replication target will have a corresponding replication. + self._replication_initialize(rep_target) + + def _replication_initialize(self, target): + rep_manager = instorage_rep.InStorageMCSReplicationManager( + self, target, InStorageAssistant) + + if self._active_backend_id: + if self._active_backend_id != target['backend_id']: + msg = (_("Invalid secondary id %s.") % self._active_backend_id) + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + # Setup partnership only in non-failover state + else: + try: + rep_manager.establish_target_partnership() + except exception.VolumeDriverException: + LOG.error('The replication src %(src)s has not ' + 'successfully established partnership with the ' + 'replica target %(tgt)s.', + {'src': self.configuration.san_ip, + 'tgt': target['backend_id']}) + + self._aux_backend_assistant = rep_manager.get_target_assistant() + self.replica_manager[target['backend_id']] = rep_manager + self._replica_target = target + + +class InStorageAssistant(object): + + # All the supported QoS key are saved in this dict. When a new + # key is going to add, three values MUST be set: + # 'default': to indicate the value, when the parameter is disabled. + # 'param': to indicate the corresponding parameter in the command. + # 'type': to indicate the type of this value. + WAIT_TIME = 5 + mcs_qos_keys = {'IOThrottling': {'default': '0', + 'param': 'rate', + 'type': int}} + + def __init__(self, run_ssh): + self.ssh = InStorageSSH(run_ssh) + self.check_lcmapping_interval = 3 + + @staticmethod + def handle_keyerror(cmd, out): + msg = (_('Could not find key in output of command %(cmd)s: %(out)s.') + % {'out': out, 'cmd': cmd}) + raise exception.VolumeBackendAPIException(data=msg) + + def compression_enabled(self): + """Return whether or not compression is enabled for this system.""" + resp = self.ssh.lslicense() + keys = ['license_compression_enclosures', + 'license_compression_capacity'] + for key in keys: + if resp.get(key, '0') != '0': + return True + try: + resp = self.ssh.lsguicapabilities() + if resp.get('compression', '0') == 'yes': + return True + except exception.VolumeBackendAPIException: + LOG.exception("Failed to fetch licensing scheme.") + return False + + def replication_licensed(self): + """Return whether or not replication is enabled for this system.""" + return True + + def get_system_info(self): + """Return system's name, ID, and code level.""" + resp = self.ssh.lssystem() + level = resp['code_level'] + match_obj = re.search('([0-9].){3}[0-9]', level) + if match_obj is None: + msg = _('Failed to get code level (%s).') % level + raise exception.VolumeBackendAPIException(data=msg) + code_level = match_obj.group().split('.') + return {'code_level': tuple([int(x) for x in code_level]), + 'system_name': resp['name'], + 'system_id': resp['id']} + + def get_node_info(self): + """Return dictionary containing information on system's nodes.""" + nodes = {} + resp = self.ssh.lsnode() + for node_data in resp: + try: + if node_data['status'] != 'online': + continue + node = {} + node['id'] = node_data['id'] + node['name'] = node_data['name'] + node['IO_group'] = node_data['IO_group_id'] + node['iscsi_name'] = node_data['iscsi_name'] + node['WWNN'] = node_data['WWNN'] + node['status'] = node_data['status'] + node['WWPN'] = [] + node['ipv4'] = [] + node['ipv6'] = [] + node['enabled_protocols'] = [] + nodes[node['id']] = node + except KeyError: + self.handle_keyerror('lsnode', node_data) + return nodes + + def get_pool_attrs(self, pool): + """Return attributes for the specified pool.""" + return self.ssh.lsmdiskgrp(pool) + + def get_available_io_groups(self): + """Return list of available IO groups.""" + iogrps = [] + resp = self.ssh.lsiogrp() + for iogrp in resp: + try: + if int(iogrp['node_count']) > 0: + iogrps.append(int(iogrp['id'])) + except KeyError: + self.handle_keyerror('lsiogrp', iogrp) + except ValueError: + msg = (_('Expected integer for node_count, ' + 'mcsinq lsiogrp returned: %(node)s.') % + {'node': iogrp['node_count']}) + raise exception.VolumeBackendAPIException(data=msg) + return iogrps + + def get_vdisk_count_by_io_group(self): + res = {} + resp = self.ssh.lsiogrp() + for iogrp in resp: + try: + if int(iogrp['node_count']) > 0: + res[int(iogrp['id'])] = int(iogrp['vdisk_count']) + except KeyError: + self.handle_keyerror('lsiogrp', iogrp) + except ValueError: + msg = (_('Expected integer for node_count, ' + 'mcsinq lsiogrp returned: %(node)s') % + {'node': iogrp['node_count']}) + raise exception.VolumeBackendAPIException(data=msg) + return res + + def select_io_group(self, state, opts): + selected_iog = 0 + iog_list = InStorageAssistant._get_valid_requested_io_groups( + state, opts) + if len(iog_list) == 0: + raise exception.InvalidInput( + reason=_('Given I/O group(s) %(iogrp)s not valid; available ' + 'I/O groups are %(avail)s.') + % {'iogrp': opts['iogrp'], + 'avail': state['available_iogrps']}) + iog_vdc = self.get_vdisk_count_by_io_group() + LOG.debug("IO group current balance %s", iog_vdc) + min_vdisk_count = iog_vdc[iog_list[0]] + selected_iog = iog_list[0] + for iog in iog_list: + if iog_vdc[iog] < min_vdisk_count: + min_vdisk_count = iog_vdc[iog] + selected_iog = iog + LOG.debug("Selected io_group is %d", selected_iog) + return selected_iog + + def get_volume_io_group(self, vol_name): + vdisk = self.ssh.lsvdisk(vol_name) + if vdisk: + resp = self.ssh.lsiogrp() + for iogrp in resp: + if iogrp['name'] == vdisk['IO_group_name']: + return int(iogrp['id']) + return None + + def add_iscsi_ip_addrs(self, storage_nodes): + """Add iSCSI IP addresses to system node information.""" + resp = self.ssh.lsportip() + for ip_data in resp: + try: + state = ip_data['state'] + if ip_data['node_id'] in storage_nodes and ( + state == 'configured' or state == 'online'): + node = storage_nodes[ip_data['node_id']] + if len(ip_data['IP_address']): + node['ipv4'].append(ip_data['IP_address']) + if len(ip_data['IP_address_6']): + node['ipv6'].append(ip_data['IP_address_6']) + except KeyError: + self.handle_keyerror('lsportip', ip_data) + + def add_fc_wwpns(self, storage_nodes): + """Add FC WWPNs to system node information.""" + for key in storage_nodes: + node = storage_nodes[key] + wwpns = set(node['WWPN']) + resp = self.ssh.lsportfc(node_id=node['id']) + for port_info in resp: + if (port_info['type'] == 'fc' and + port_info['status'] == 'active'): + wwpns.add(port_info['WWPN']) + node['WWPN'] = list(wwpns) + LOG.info('WWPN on node %(node)s: %(wwpn)s.', + {'node': node['id'], 'wwpn': node['WWPN']}) + + def get_conn_fc_wwpns(self, host): + wwpns = set() + resp = self.ssh.lsfabric(host=host) + for wwpn in resp.select('local_wwpn'): + if wwpn is not None: + wwpns.add(wwpn) + return list(wwpns) + + def add_chap_secret_to_host(self, host_name): + """Generate and store a randomly-generated CHAP secret for the host.""" + chap_secret = utils.generate_password() + self.ssh.add_chap_secret(chap_secret, host_name) + return chap_secret + + def get_chap_secret_for_host(self, host_name): + """Generate and store a randomly-generated CHAP secret for the host.""" + resp = self.ssh.lsiscsiauth() + host_found = False + for host_data in resp: + try: + if host_data['name'] == host_name: + host_found = True + if host_data['iscsi_auth_method'] == 'chap': + return host_data['iscsi_chap_secret'] + except KeyError: + self.handle_keyerror('lsiscsiauth', host_data) + if not host_found: + msg = _('Failed to find host %s.') % host_name + raise exception.VolumeBackendAPIException(data=msg) + return None + + def get_host_from_connector(self, connector, volume_name=None): + """Return the InStorage host described by the connector.""" + LOG.debug('Enter: get_host_from_connector: %s.', connector) + + # If we have FC information, we have a faster lookup option + host_name = None + if 'wwpns' in connector: + for wwpn in connector['wwpns']: + resp = self.ssh.lsfabric(wwpn=wwpn) + for wwpn_info in resp: + try: + if (wwpn_info['remote_wwpn'] and + wwpn_info['name'] and + wwpn_info['remote_wwpn'].lower() == + wwpn.lower()): + host_name = wwpn_info['name'] + break + except KeyError: + self.handle_keyerror('lsfabric', wwpn_info) + if host_name: + break + if host_name: + LOG.debug('Leave: get_host_from_connector: host %s.', host_name) + return host_name + + def update_host_list(host, host_list): + idx = host_list.index(host) + del host_list[idx] + host_list.insert(0, host) + + # That didn't work, so try exhaustive search + hosts_info = self.ssh.lshost() + host_list = list(hosts_info.select('name')) + # If we have a "real" connector, we might be able to find the + # host entry with fewer queries if we move the host entries + # that contain the connector's host property value to the front + # of the list + if 'host' in connector: + # order host_list such that the host entries that + # contain the connector's host name are at the + # beginning of the list + for host in host_list: + if re.search(connector['host'], host): + update_host_list(host, host_list) + # If we have a volume name we have a potential fast path + # for finding the matching host for that volume. + # Add the host_names that have mappings for our volume to the + # head of the list of host names to search them first + if volume_name: + hosts_map_info = self.ssh.lsvdiskhostmap(volume_name) + hosts_map_info_list = list(hosts_map_info.select('host_name')) + # remove the fast path host names from the end of the list + # and move to the front so they are only searched for once. + for host in hosts_map_info_list: + update_host_list(host, host_list) + found = False + for name in host_list: + try: + resp = self.ssh.lshost(host=name) + except exception.VolumeBackendAPIException as ex: + LOG.debug("Exception message: %s", ex.msg) + if 'CMMVC5754E' in ex.msg: + LOG.debug("CMMVC5754E found in CLI exception.") + # CMMVC5754E: The specified object does not exist + # The host has been deleted while walking the list. + # This is a result of a host change on the MCS that + # is out of band to this request. + continue + # unexpected error so reraise it + with excutils.save_and_reraise_exception(): + pass + if 'initiator' in connector: + for iscsi in resp.select('iscsi_name'): + if iscsi == connector['initiator']: + host_name = name + found = True + break + elif 'wwpns' in connector and len(connector['wwpns']): + connector_wwpns = [str(x).lower() for x in connector['wwpns']] + for wwpn in resp.select('WWPN'): + if wwpn and wwpn.lower() in connector_wwpns: + host_name = name + found = True + break + if found: + break + + LOG.debug('Leave: get_host_from_connector: host %s.', host_name) + return host_name + + def create_host(self, connector): + """Create a new host on the storage system. + + We create a host name and associate it with the given connection + information. The host name will be a cleaned up version of the given + host name (at most 55 characters), plus a random 8-character suffix to + avoid collisions. The total length should be at most 63 characters. + """ + LOG.debug('Enter: create_host: host %s.', connector['host']) + + # Before we start, make sure host name is a string and that we have + # one port at least . + host_name = connector['host'] + if not isinstance(host_name, six.string_types): + msg = _('create_host: Host name is not unicode or string.') + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + ports = [] + if 'initiator' in connector: + ports.append(['initiator', '%s' % connector['initiator']]) + if 'wwpns' in connector: + for wwpn in connector['wwpns']: + ports.append(['wwpn', '%s' % wwpn]) + if not len(ports): + msg = _('create_host: No initiators or wwpns supplied.') + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + # Build a host name for the InStorage host - first clean up the name + if isinstance(host_name, six.text_type): + host_name = unicodedata.normalize('NFKD', host_name).encode( + 'ascii', 'replace').decode('ascii') + + for num in range(0, 128): + ch = str(chr(num)) + if not ch.isalnum() and ch not in [' ', '.', '-', '_']: + host_name = host_name.replace(ch, '-') + + # InStorage doesn't expect hostname that doesn't starts with letter or + # _. + if not re.match('^[A-Za-z]', host_name): + host_name = '_' + host_name + + # Add a random 8-character suffix to avoid collisions + rand_id = str(random.randint(0, 99999999)).zfill(8) + host_name = '%s-%s' % (host_name[:55], rand_id) + + # Create a host with one port + port = ports.pop(0) + self.ssh.mkhost(host_name, port[0], port[1]) + + # Add any additional ports to the host + for port in ports: + self.ssh.addhostport(host_name, port[0], port[1]) + + LOG.debug('Leave: create_host: host %(host)s - %(host_name)s.', + {'host': connector['host'], 'host_name': host_name}) + return host_name + + def delete_host(self, host_name): + self.ssh.rmhost(host_name) + + def check_host_mapped_vols(self, host_name): + return self.ssh.lshostvdiskmap(host_name) + + def map_vol_to_host(self, volume_name, host_name, multihostmap): + """Create a mapping between a volume to a host.""" + + LOG.debug('Enter: map_vol_to_host: volume %(volume_name)s to ' + 'host %(host_name)s.', + {'volume_name': volume_name, 'host_name': host_name}) + + # Check if this volume is already mapped to this host + result_lun = self.ssh.get_vdiskhostmapid(volume_name, host_name) + if result_lun is None: + result_lun = self.ssh.mkvdiskhostmap(host_name, volume_name, None, + multihostmap) + + LOG.debug('Leave: map_vol_to_host: LUN %(result_lun)s, volume ' + '%(volume_name)s, host %(host_name)s.', + {'result_lun': result_lun, + 'volume_name': volume_name, + 'host_name': host_name}) + return int(result_lun) + + def unmap_vol_from_host(self, volume_name, host_name): + """Unmap the volume and delete the host if it has no more mappings.""" + + LOG.debug('Enter: unmap_vol_from_host: volume %(volume_name)s from ' + 'host %(host_name)s.', + {'volume_name': volume_name, 'host_name': host_name}) + + # Check if the mapping exists + resp = self.ssh.lsvdiskhostmap(volume_name) + if not len(resp): + LOG.warning('unmap_vol_from_host: No mapping of volume ' + '%(vol_name)s to any host found.', + {'vol_name': volume_name}) + return host_name + if host_name is None: + if len(resp) > 1: + LOG.warning('unmap_vol_from_host: Multiple mappings of ' + 'volume %(vol_name)s found, no host ' + 'specified.', {'vol_name': volume_name}) + return + else: + host_name = resp[0]['host_name'] + else: + found = False + for h in resp.select('host_name'): + if h == host_name: + found = True + if not found: + LOG.warning('unmap_vol_from_host: No mapping of volume ' + '%(vol_name)s to host %(host)s found.', + {'vol_name': volume_name, 'host': host_name}) + return host_name + # We now know that the mapping exists + self.ssh.rmvdiskhostmap(host_name, volume_name) + + LOG.debug('Leave: unmap_vol_from_host: volume %(volume_name)s from ' + 'host %(host_name)s.', + {'volume_name': volume_name, 'host_name': host_name}) + return host_name + + @staticmethod + def build_default_opts(config): + # Ignore capitalization + + opt = {'rsize': config.instorage_mcs_vol_rsize, + 'warning': config.instorage_mcs_vol_warning, + 'autoexpand': config.instorage_mcs_vol_autoexpand, + 'grainsize': config.instorage_mcs_vol_grainsize, + 'compression': config.instorage_mcs_vol_compression, + 'intier': config.instorage_mcs_vol_intier, + 'iogrp': config.instorage_mcs_vol_iogrp, + 'qos': None, + 'replication': False} + return opt + + @staticmethod + def check_vdisk_opts(state, opts): + # Check that grainsize is 32/64/128/256 + if opts['grainsize'] not in [32, 64, 128, 256]: + raise exception.InvalidInput( + reason=_('Illegal value specified for ' + 'instorage_mcs_vol_grainsize: set to either ' + '32, 64, 128, or 256.')) + + # Check that compression is supported + if opts['compression'] and not state['compression_enabled']: + raise exception.InvalidInput( + reason=_('System does not support compression.')) + + # Check that rsize is set if compression is set + if opts['compression'] and opts['rsize'] == -1: + raise exception.InvalidInput( + reason=_('If compression is set to True, rsize must ' + 'also be set (not equal to -1).')) + + iogs = InStorageAssistant._get_valid_requested_io_groups(state, opts) + + if len(iogs) == 0: + raise exception.InvalidInput( + reason=_('Given I/O group(s) %(iogrp)s not valid; available ' + 'I/O groups are %(avail)s.') + % {'iogrp': opts['iogrp'], + 'avail': state['available_iogrps']}) + + @staticmethod + def _get_valid_requested_io_groups(state, opts): + given_iogs = str(opts['iogrp']) + iog_list = given_iogs.split(',') + # convert to int + iog_list = list(map(int, iog_list)) + LOG.debug("Requested iogroups %s", iog_list) + LOG.debug("Available iogroups %s", state['available_iogrps']) + filtiog = set(iog_list).intersection(state['available_iogrps']) + iog_list = list(filtiog) + LOG.debug("Filtered (valid) requested iogroups %s", iog_list) + return iog_list + + def _get_opts_from_specs(self, opts, specs): + qos = {} + for k, value in specs.items(): + # Get the scope, if using scope format + key_split = k.split(':') + if len(key_split) == 1: + scope = None + key = key_split[0] + else: + scope = key_split[0] + key = key_split[1] + + # We generally do not look at capabilities in the driver, but + # replication is a special case where the user asks for + # a volume to be replicated, and we want both the scheduler and + # the driver to act on the value. + if ((not scope or scope == 'capabilities') and + key == 'replication'): + scope = None + key = 'replication' + words = value.split() + if not (words and len(words) == 2 and words[0] == ''): + LOG.error("Replication must be specified as " + "' True' or ' False'.") + del words[0] + value = words[0] + + # Add the QoS. + if scope and scope == 'qos': + if key in self.mcs_qos_keys.keys(): + try: + type_fn = self.mcs_qos_keys[key]['type'] + value = type_fn(value) + qos[key] = value + except ValueError: + continue + + # Any keys that the driver should look at should have the + # 'drivers' scope. + if scope and scope != 'drivers': + continue + if key in opts: + this_type = type(opts[key]).__name__ + if this_type == 'int': + value = int(value) + elif this_type == 'bool': + value = strutils.bool_from_string(value) + opts[key] = value + if len(qos) != 0: + opts['qos'] = qos + return opts + + def _get_qos_from_volume_metadata(self, volume_metadata): + """Return the QoS information from the volume metadata.""" + qos = {} + for i in volume_metadata: + k = i.get('key', None) + value = i.get('value', None) + key_split = k.split(':') + if len(key_split) == 1: + scope = None + key = key_split[0] + else: + scope = key_split[0] + key = key_split[1] + # Add the QoS. + if scope and scope == 'qos': + if key in self.mcs_qos_keys.keys(): + try: + type_fn = self.mcs_qos_keys[key]['type'] + value = type_fn(value) + qos[key] = value + except ValueError: + continue + return qos + + def _wait_for_a_condition(self, testmethod, timeout=None, + interval=INTERVAL_1_SEC, + raise_exception=False): + start_time = time.time() + if timeout is None: + timeout = DEFAULT_TIMEOUT + + def _inner(): + try: + testValue = testmethod() + except Exception as ex: + if raise_exception: + LOG.exception("_wait_for_a_condition: %s" + " execution failed.", + testmethod.__name__) + raise exception.VolumeBackendAPIException(data=ex) + else: + testValue = False + LOG.debug('Assistant.' + '_wait_for_condition: %(method_name)s ' + 'execution failed for %(exception)s.', + {'method_name': testmethod.__name__, + 'exception': ex.message}) + if testValue: + raise loopingcall.LoopingCallDone() + + if int(time.time()) - start_time > timeout: + msg = ( + _('CommandLineAssistant._wait_for_condition: ' + '%s timeout.') % testmethod.__name__) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + timer = loopingcall.FixedIntervalLoopingCall(_inner) + timer.start(interval=interval).wait() + + def get_vdisk_params(self, config, state, type_id, + volume_type=None, volume_metadata=None): + """Return the parameters for creating the vdisk. + + Get volume type and defaults from config options + and take them into account. + """ + opts = self.build_default_opts(config) + ctxt = context.get_admin_context() + if volume_type is None and type_id is not None: + volume_type = volume_types.get_volume_type(ctxt, type_id) + if volume_type: + qos_specs_id = volume_type.get('qos_specs_id') + specs = dict(volume_type).get('extra_specs') + + # NOTE: We prefer the qos_specs association + # and over-ride any existing + # extra-specs settings if present + if qos_specs_id is not None: + kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs'] + # Merge the qos_specs into extra_specs and qos_specs has higher + # priority than extra_specs if they have different values for + # the same key. + specs.update(kvs) + opts = self._get_opts_from_specs(opts, specs) + if (opts['qos'] is None and config.instorage_mcs_allow_tenant_qos and + volume_metadata): + qos = self._get_qos_from_volume_metadata(volume_metadata) + if len(qos) != 0: + opts['qos'] = qos + + self.check_vdisk_opts(state, opts) + return opts + + @staticmethod + def _get_vdisk_create_params(opts): + intier = 'on' if opts['intier'] else 'off' + if opts['rsize'] == -1: + params = [] + else: + params = ['-rsize', '%s%%' % str(opts['rsize']), + '-autoexpand', '-warning', + '%s%%' % str(opts['warning'])] + if not opts['autoexpand']: + params.remove('-autoexpand') + + if opts['compression']: + params.append('-compressed') + else: + params.extend(['-grainsize', str(opts['grainsize'])]) + + params.extend(['-intier', intier]) + return params + + def create_vdisk(self, name, size, units, pool, opts): + name = '"%s"' % name + LOG.debug('Enter: create_vdisk: vdisk %s.', name) + params = self._get_vdisk_create_params(opts) + self.ssh.mkvdisk(name, size, units, pool, opts, params) + LOG.debug('Leave: _create_vdisk: volume %s.', name) + + def delete_vdisk(self, vdisk, force): + """Ensures that vdisk is not part of FC mapping and deletes it.""" + LOG.debug('Enter: delete_vdisk: vdisk %s.', vdisk) + if not self.is_vdisk_defined(vdisk): + LOG.info('Tried to delete non-existent vdisk %s.', vdisk) + return + self.ensure_vdisk_no_lc_mappings(vdisk, allow_snaps=True, + allow_lctgt=True) + self.ssh.rmvdisk(vdisk, force=force) + LOG.debug('Leave: delete_vdisk: vdisk %s.', vdisk) + + def is_vdisk_defined(self, vdisk_name): + """Check if vdisk is defined.""" + attrs = self.get_vdisk_attributes(vdisk_name) + return attrs is not None + + def get_vdisk_attributes(self, vdisk): + attrs = self.ssh.lsvdisk(vdisk) + return attrs + + def find_vdisk_copy_id(self, vdisk, pool): + resp = self.ssh.lsvdiskcopy(vdisk) + for copy_id, mdisk_grp in resp.select('copy_id', 'mdisk_grp_name'): + if mdisk_grp == pool: + return copy_id + msg = _('Failed to find a vdisk copy in the expected pool.') + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + def get_vdisk_copy_attrs(self, vdisk, copy_id): + return self.ssh.lsvdiskcopy(vdisk, copy_id=copy_id)[0] + + def get_vdisk_copy_ids(self, vdisk): + resp = self.ssh.lsvdiskcopy(vdisk) + if len(resp) == 2: + if resp[0]['primary'] == 'yes': + primary = resp[0]['copy_id'] + secondary = resp[1]['copy_id'] + else: + primary = resp[1]['copy_id'] + secondary = resp[0]['copy_id'] + + return primary, secondary + else: + msg = (_('list_vdisk_copy failed: No copy of volume %s exists.') + % vdisk) + raise exception.VolumeDriverException(message=msg) + + def get_vdisk_copies(self, vdisk): + copies = {'primary': None, + 'secondary': None} + + resp = self.ssh.lsvdiskcopy(vdisk) + for copy_id, status, sync, primary, mdisk_grp in ( + resp.select('copy_id', 'status', 'sync', + 'primary', 'mdisk_grp_name')): + copy = {'copy_id': copy_id, + 'status': status, + 'sync': sync, + 'primary': primary, + 'mdisk_grp_name': mdisk_grp, + 'sync_progress': None} + if copy['sync'] != 'yes': + progress_info = self.ssh.lsvdisksyncprogress(vdisk, copy_id) + copy['sync_progress'] = progress_info['progress'] + if copy['primary'] == 'yes': + copies['primary'] = copy + else: + copies['secondary'] = copy + return copies + + def create_copy(self, src, tgt, src_id, config, opts, + full_copy, pool=None): + """Create a new snapshot using LocalCopy.""" + LOG.debug('Enter: create_copy: snapshot %(src)s to %(tgt)s.', + {'tgt': tgt, 'src': src}) + + src_attrs = self.get_vdisk_attributes(src) + if src_attrs is None: + msg = (_('create_copy: Source vdisk %(src)s (%(src_id)s) ' + 'does not exist.') % {'src': src, 'src_id': src_id}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + src_size = src_attrs['capacity'] + # In case we need to use a specific pool + if not pool: + pool = src_attrs['mdisk_grp_name'] + + opts['iogrp'] = src_attrs['IO_group_id'] + self.create_vdisk(tgt, src_size, 'b', pool, opts) + timeout = config.instorage_mcs_localcopy_timeout + try: + self.run_localcopy(src, tgt, timeout, + config.instorage_mcs_localcopy_rate, + full_copy=full_copy) + except Exception: + with excutils.save_and_reraise_exception(): + self.delete_vdisk(tgt, True) + + LOG.debug('Leave: _create_copy: snapshot %(tgt)s from ' + 'vdisk %(src)s.', + {'tgt': tgt, 'src': src}) + + def extend_vdisk(self, vdisk, amount): + self.ssh.expandvdisksize(vdisk, amount) + + def add_vdisk_copy(self, vdisk, dest_pool, volume_type, state, config): + """Add a vdisk copy in the given pool.""" + resp = self.ssh.lsvdiskcopy(vdisk) + if len(resp) > 1: + msg = (_('add_vdisk_copy failed: A copy of volume %s exists. ' + 'Adding another copy would exceed the limit of ' + '2 copies.') % vdisk) + raise exception.VolumeDriverException(message=msg) + orig_copy_id = resp[0].get("copy_id", None) + + if orig_copy_id is None: + msg = (_('add_vdisk_copy started without a vdisk copy in the ' + 'expected pool.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + if volume_type is None: + opts = self.get_vdisk_params(config, state, None) + else: + opts = self.get_vdisk_params(config, state, volume_type['id'], + volume_type=volume_type) + params = self._get_vdisk_create_params(opts) + new_copy_id = self.ssh.addvdiskcopy(vdisk, dest_pool, params) + return (orig_copy_id, new_copy_id) + + def check_vdisk_copy_synced(self, vdisk, copy_id): + sync = self.ssh.lsvdiskcopy(vdisk, copy_id=copy_id)[0]['sync'] + if sync == 'yes': + return True + return False + + def rm_vdisk_copy(self, vdisk, copy_id): + self.ssh.rmvdiskcopy(vdisk, copy_id) + + def _prepare_lc_map(self, lc_map_id, timeout): + self.ssh.prestartlcmap(lc_map_id) + mapping_ready = False + max_retries = (timeout // self.WAIT_TIME) + 1 + for try_number in range(1, max_retries): + mapping_attrs = self._get_localcopy_mapping_attributes(lc_map_id) + if (mapping_attrs is None or + 'status' not in mapping_attrs): + break + if mapping_attrs['status'] == 'prepared': + mapping_ready = True + break + elif mapping_attrs['status'] == 'stopped': + self.ssh.prestartlcmap(lc_map_id) + elif mapping_attrs['status'] != 'preparing': + msg = (_('Unexecpted mapping status %(status)s for mapping ' + '%(id)s. Attributes: %(attr)s.') + % {'status': mapping_attrs['status'], + 'id': lc_map_id, + 'attr': mapping_attrs}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + greenthread.sleep(self.WAIT_TIME) + + if not mapping_ready: + msg = (_('Mapping %(id)s prepare failed to complete within the' + 'allotted %(to)d seconds timeout. Terminating.') + % {'id': lc_map_id, + 'to': timeout}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + # Consistency Group + def start_lc_consistgrp(self, lc_consistgrp): + self.ssh.startlcconsistgrp(lc_consistgrp) + + def create_lc_consistgrp(self, lc_consistgrp): + self.ssh.mklcconsistgrp(lc_consistgrp) + + def delete_lc_consistgrp(self, lc_consistgrp): + self.ssh.rmlcconsistgrp(lc_consistgrp) + + def stop_lc_consistgrp(self, lc_consistgrp): + self.ssh.stoplcconsistgrp(lc_consistgrp) + + def run_consistgrp_snapshots(self, lc_consistgrp, snapshots, state, + config, timeout): + model_update = {'status': fields.ConsistencyGroupStatus.AVAILABLE} + snapshots_model_update = [] + try: + for snapshot in snapshots: + opts = self.get_vdisk_params(config, state, + snapshot.volume_type_id) + + self.create_localcopy_to_consistgrp(snapshot.volume_name, + snapshot.name, + lc_consistgrp, + config, opts) + + self.prepare_lc_consistgrp(lc_consistgrp, timeout) + self.start_lc_consistgrp(lc_consistgrp) + # There is CG limitation that could not create more than 128 CGs. + # After start CG, we delete CG to avoid CG limitation. + # Cinder general will maintain the CG and snapshots relationship. + self.delete_lc_consistgrp(lc_consistgrp) + except exception.VolumeBackendAPIException as err: + model_update['status'] = fields.ConsistencyGroupStatus.ERROR + # Release cg + self.delete_lc_consistgrp(lc_consistgrp) + LOG.error("Failed to create CGSnapshot. " + "Exception: %s.", err) + + for snapshot in snapshots: + snapshots_model_update.append( + {'id': snapshot.id, + 'status': model_update['status']}) + + return model_update, snapshots_model_update + + def delete_consistgrp_snapshots(self, lc_consistgrp, snapshots): + """Delete localcopy maps and consistent group.""" + model_update = {'status': fields.ConsistencyGroupStatus.DELETED} + snapshots_model_update = [] + + try: + for snapshot in snapshots: + self.ssh.rmvdisk(snapshot.name, True) + except exception.VolumeBackendAPIException as err: + model_update['status'] = ( + fields.ConsistencyGroupStatus.ERROR_DELETING) + LOG.error("Failed to delete the snapshot %(snap)s of " + "CGSnapshot. Exception: %(exception)s.", + {'snap': snapshot.name, 'exception': err}) + + for snapshot in snapshots: + snapshots_model_update.append( + {'id': snapshot.id, + 'status': model_update['status']}) + + return model_update, snapshots_model_update + + def run_group_snapshots(self, lc_group, snapshots, state, + config, timeout): + model_update = {'status': fields.GroupStatus.AVAILABLE} + snapshots_model_update = [] + try: + for snapshot in snapshots: + opts = self.get_vdisk_params(config, state, + snapshot.volume_type_id) + + self.create_localcopy_to_consistgrp(snapshot.volume_name, + snapshot.name, + lc_group, + config, opts) + + self.prepare_lc_consistgrp(lc_group, timeout) + self.start_lc_consistgrp(lc_group) + # There is CG limitation that could not create more than 128 CGs. + # After start CG, we delete CG to avoid CG limitation. + # Cinder general will maintain the group and snapshots + # relationship. + self.delete_lc_consistgrp(lc_group) + except exception.VolumeBackendAPIException as err: + model_update['status'] = fields.GroupStatus.ERROR + # Release cg + self.delete_lc_consistgrp(lc_group) + LOG.error("Failed to create Group_Snapshot. " + "Exception: %s.", err) + + for snapshot in snapshots: + snapshots_model_update.append( + {'id': snapshot.id, + 'status': model_update['status']}) + + return model_update, snapshots_model_update + + def delete_group_snapshots(self, lc_group, snapshots): + """Delete localcopy maps and group.""" + model_update = {'status': fields.GroupStatus.DELETED} + snapshots_model_update = [] + + try: + for snapshot in snapshots: + self.ssh.rmvdisk(snapshot.name, True) + except exception.VolumeBackendAPIException as err: + model_update['status'] = ( + fields.GroupStatus.ERROR_DELETING) + LOG.error("Failed to delete the snapshot %(snap)s of " + "Group_Snapshot. Exception: %(exception)s.", + {'snap': snapshot.name, 'exception': err}) + + for snapshot in snapshots: + snapshots_model_update.append( + {'id': snapshot.id, + 'status': model_update['status']}) + + return model_update, snapshots_model_update + + def prepare_lc_consistgrp(self, lc_consistgrp, timeout): + """Prepare LC Consistency Group.""" + self.ssh.prestartlcconsistgrp(lc_consistgrp) + + def prepare_lc_consistgrp_success(): + mapping_ready = False + mapping_attrs = self._get_localcopy_consistgrp_attr(lc_consistgrp) + if (mapping_attrs is None or + 'status' not in mapping_attrs): + pass + if mapping_attrs['status'] == 'prepared': + mapping_ready = True + elif mapping_attrs['status'] == 'stopped': + self.ssh.prestartlcconsistgrp(lc_consistgrp) + elif mapping_attrs['status'] != 'preparing': + msg = (_('Unexpected mapping status %(status)s for mapping ' + '%(id)s. Attributes: %(attr)s.') % + {'status': mapping_attrs['status'], + 'id': lc_consistgrp, + 'attr': mapping_attrs}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return mapping_ready + self._wait_for_a_condition(prepare_lc_consistgrp_success, timeout) + + def create_group_from_source(self, group, lc_group, + sources, targets, state, + config, timeout): + """Create group from source""" + LOG.debug('Enter: create_group_from_source: group %(group)s' + ' source %(source)s, target %(target)s', + {'group': lc_group, 'source': sources, 'target': targets}) + model_update = {'status': fields.GroupStatus.AVAILABLE} + ctxt = context.get_admin_context() + try: + for source, target in zip(sources, targets): + opts = self.get_vdisk_params(config, state, + source.volume_type_id) + pool = utils.extract_host(target.host, 'pool') + self.create_localcopy_to_consistgrp(source.name, + target.name, + lc_group, + config, opts, + True, pool=pool) + self.prepare_lc_consistgrp(lc_group, timeout) + self.start_lc_consistgrp(lc_group) + self.delete_lc_consistgrp(lc_group) + volumes_model_update = self._get_volume_model_updates( + ctxt, targets, group.id, model_update['status']) + except exception.VolumeBackendAPIException as err: + model_update['status'] = fields.GroupStatus.ERROR + volumes_model_update = self._get_volume_model_updates( + ctxt, targets, group.id, model_update['status']) + with excutils.save_and_reraise_exception(): + self.delete_lc_consistgrp(lc_group) + LOG.error("Failed to create group from group_snapshot. " + "Exception: %s", err) + return model_update, volumes_model_update + + LOG.debug('Leave: create_cg_from_source.') + return model_update, volumes_model_update + + def _get_volume_model_updates(self, ctxt, volumes, cgId, + status='available'): + """Update the volume model's status and return it.""" + volume_model_updates = [] + LOG.info("Updating status for CG: %(id)s.", {'id': cgId}) + if volumes: + for volume in volumes: + volume_model_updates.append({'id': volume.id, + 'status': status}) + else: + LOG.info("No volume found for CG: %(cg)s.", {'cg': cgId}) + return volume_model_updates + + def run_localcopy(self, source, target, timeout, copy_rate, + full_copy=True): + """Create a LocalCopy mapping from the source to the target.""" + LOG.debug('Enter: run_localcopy: execute LocalCopy from source ' + '%(source)s to target %(target)s.', + {'source': source, 'target': target}) + + lc_map_id = self.ssh.mklcmap(source, target, full_copy, copy_rate) + self._prepare_lc_map(lc_map_id, timeout) + self.ssh.startlcmap(lc_map_id) + + LOG.debug('Leave: run_localcopy: LocalCopy started from ' + '%(source)s to %(target)s.', + {'source': source, 'target': target}) + + def create_localcopy_to_consistgrp(self, source, target, consistgrp, + config, opts, full_copy=False, + pool=None): + """Create a LocalCopy mapping and add to consistent group.""" + LOG.debug('Enter: create_localcopy_to_consistgrp: create LocalCopy' + ' from source %(source)s to target %(target)s' + 'Then add the localcopy to %(cg)s.', + {'source': source, 'target': target, 'cg': consistgrp}) + + src_attrs = self.get_vdisk_attributes(source) + if src_attrs is None: + msg = (_('create_copy: Source vdisk %(src)s ' + 'does not exist.') % {'src': source}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + src_size = src_attrs['capacity'] + # In case we need to use a specific pool + if not pool: + pool = src_attrs['mdisk_grp_name'] + opts['iogrp'] = src_attrs['IO_group_id'] + self.create_vdisk(target, src_size, 'b', pool, opts) + + self.ssh.mklcmap(source, target, full_copy, + config.instorage_mcs_localcopy_rate, + consistgrp=consistgrp) + + LOG.debug('Leave: create_localcopy_to_consistgrp: ' + 'LocalCopy started from %(source)s to %(target)s.', + {'source': source, 'target': target}) + + def _get_vdisk_lc_mappings(self, vdisk): + """Return LocalCopy mappings that this vdisk is associated with.""" + mapping_ids = [] + resp = self.ssh.lsvdisklcmappings(vdisk) + for id in resp.select('id'): + mapping_ids.append(id) + return mapping_ids + + def _get_localcopy_mapping_attributes(self, lc_map_id): + resp = self.ssh.lslcmap(lc_map_id) + if not len(resp): + return None + return resp[0] + + def _get_localcopy_consistgrp_attr(self, lc_map_id): + resp = self.ssh.lslcconsistgrp(lc_map_id) + if not len(resp): + return None + return resp[0] + + def _check_vdisk_lc_mappings(self, name, + allow_snaps=True, allow_lctgt=False): + """LocalCopy mapping check helper.""" + LOG.debug('Loopcall: _check_vdisk_lc_mappings(), vdisk %s.', name) + mapping_ids = self._get_vdisk_lc_mappings(name) + wait_for_copy = False + rmlcmap_failed_e = None + for map_id in mapping_ids: + attrs = self._get_localcopy_mapping_attributes(map_id) + if not attrs: + continue + source = attrs['source_vdisk_name'] + target = attrs['target_vdisk_name'] + copy_rate = attrs['copy_rate'] + status = attrs['status'] + + if allow_lctgt and target == name and status == 'copying': + self.ssh.stoplcmap(map_id) + attrs = self._get_localcopy_mapping_attributes(map_id) + if attrs: + status = attrs['status'] + + if copy_rate == '0': + if source == name: + # Vdisk with snapshots. Return False if snapshot + # not allowed. + if not allow_snaps: + raise loopingcall.LoopingCallDone(retvalue=False) + self.ssh.chlcmap(map_id, copyrate='50', autodel='on') + wait_for_copy = True + else: + # A snapshot + if target != name: + msg = (_('Vdisk %(name)s not involved in ' + 'mapping %(src)s -> %(tgt)s.') % + {'name': name, 'src': source, 'tgt': target}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + if status in ['copying', 'prepared']: + self.ssh.stoplcmap(map_id) + # Need to wait for the lcmap to change to + # stopped state before remove lcmap + wait_for_copy = True + elif status in ['stopping', 'preparing']: + wait_for_copy = True + else: + try: + self.ssh.rmlcmap(map_id) + except exception.VolumeBackendAPIException as e: + rmlcmap_failed_e = e + # Case 4: Copy in progress - wait and will autodelete + else: + if status == 'prepared': + self.ssh.stoplcmap(map_id) + self.ssh.rmlcmap(map_id) + elif status in ['idle_or_copied', 'stopped']: + # Prepare failed or stopped + self.ssh.rmlcmap(map_id) + else: + wait_for_copy = True + + if not wait_for_copy and rmlcmap_failed_e is not None: + raise rmlcmap_failed_e + + if not wait_for_copy or not len(mapping_ids): + raise loopingcall.LoopingCallDone(retvalue=True) + + def ensure_vdisk_no_lc_mappings(self, name, allow_snaps=True, + allow_lctgt=False): + """Ensure vdisk has no localcopy mappings.""" + timer = loopingcall.FixedIntervalLoopingCall( + self._check_vdisk_lc_mappings, name, + allow_snaps, allow_lctgt) + # Create a timer greenthread. The default volume service heart + # beat is every 10 seconds. The localcopy usually takes hours + # before it finishes. Don't set the sleep interval shorter + # than the heartbeat. Otherwise volume service heartbeat + # will not be serviced. + LOG.debug('Calling _ensure_vdisk_no_lc_mappings: vdisk %s.', + name) + ret = timer.start(interval=self.check_lcmapping_interval).wait() + timer.stop() + return ret + + def start_relationship(self, volume_name, primary=None): + vol_attrs = self.get_vdisk_attributes(volume_name) + if vol_attrs['RC_name']: + self.ssh.startrcrelationship(vol_attrs['RC_name'], primary) + + def stop_relationship(self, volume_name, access=False): + vol_attrs = self.get_vdisk_attributes(volume_name) + if vol_attrs['RC_name']: + self.ssh.stoprcrelationship(vol_attrs['RC_name'], access=access) + + def create_relationship(self, master, aux, system, asynccopy): + try: + rc_id = self.ssh.mkrcrelationship(master, aux, system, + asynccopy) + except exception.VolumeBackendAPIException as e: + # CMMVC5959E is the code in InStorage, meaning that + # there is a relationship that already has this name on the + # master cluster. + if 'CMMVC5959E' not in six.text_type(e): + # If there is no relation between the primary and the + # secondary back-end storage, the exception is raised. + raise + if rc_id: + self.start_relationship(master) + + def delete_relationship(self, volume_name): + vol_attrs = self.get_vdisk_attributes(volume_name) + if vol_attrs['RC_name']: + self.ssh.rmrcrelationship(vol_attrs['RC_name'], True) + + def get_relationship_info(self, volume_name): + vol_attrs = self.get_vdisk_attributes(volume_name) + if not vol_attrs or not vol_attrs['RC_name']: + LOG.info("Unable to get remote copy information for " + "volume %s", volume_name) + return + + relationship = self.ssh.lsrcrelationship(vol_attrs['RC_name']) + return relationship[0] if len(relationship) > 0 else None + + def delete_rc_volume(self, volume_name, target_vol=False): + vol_name = volume_name + if target_vol: + vol_name = instorage_const.REPLICA_AUX_VOL_PREFIX + volume_name + + try: + rel_info = self.get_relationship_info(vol_name) + if rel_info: + self.delete_relationship(vol_name) + self.delete_vdisk(vol_name, False) + except Exception as e: + msg = (_('Unable to delete the volume for ' + 'volume %(vol)s. Exception: %(err)s.') % + {'vol': vol_name, 'err': e}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + def switch_relationship(self, relationship, aux=True): + self.ssh.switchrelationship(relationship, aux) + + def get_partnership_info(self, system_name): + partnership = self.ssh.lspartnership(system_name) + return partnership[0] if len(partnership) > 0 else None + + def get_partnershipcandidate_info(self, system_name): + candidates = self.ssh.lspartnershipcandidate() + for candidate in candidates: + if system_name == candidate['name']: + return candidate + return None + + def mkippartnership(self, ip_v4, bandwith=1000, copyrate=50): + self.ssh.mkippartnership(ip_v4, bandwith, copyrate) + + def mkfcpartnership(self, system_name, bandwith=1000, copyrate=50): + self.ssh.mkfcpartnership(system_name, bandwith, copyrate) + + def chpartnership(self, partnership_id): + self.ssh.chpartnership(partnership_id) + + @staticmethod + def can_migrate_to_host(host, state): + if 'location_info' not in host['capabilities']: + return None + info = host['capabilities']['location_info'] + try: + (dest_type, dest_id, dest_pool) = info.split(':') + except ValueError: + return None + if (dest_type != 'InStorageMCSDriver' or dest_id != + state['system_id']): + return None + return dest_pool + + def add_vdisk_qos(self, vdisk, qos): + """Add the QoS configuration to the volume.""" + for key, value in qos.items(): + if key in self.mcs_qos_keys.keys(): + param = self.mcs_qos_keys[key]['param'] + self.ssh.chvdisk(vdisk, ['-' + param, str(value)]) + + def update_vdisk_qos(self, vdisk, qos): + """Update all the QoS in terms of a key and value. + + mcs_qos_keys saves all the supported QoS parameters. Going through + this dict, we set the new values to all the parameters. If QoS is + available in the QoS configuration, the value is taken from it; + if not, the value will be set to default. + """ + for key, value in self.mcs_qos_keys.items(): + param = value['param'] + if key in qos.keys(): + # If the value is set in QoS, take the value from + # the QoS configuration. + v = qos[key] + else: + # If not, set the value to default. + v = value['default'] + self.ssh.chvdisk(vdisk, ['-' + param, str(v)]) + + def disable_vdisk_qos(self, vdisk, qos): + """Disable the QoS.""" + for key, value in qos.items(): + if key in self.mcs_qos_keys.keys(): + param = self.mcs_qos_keys[key]['param'] + # Take the default value. + value = self.mcs_qos_keys[key]['default'] + self.ssh.chvdisk(vdisk, ['-' + param, value]) + + def change_vdisk_options(self, vdisk, changes, opts, state): + if 'warning' in opts: + opts['warning'] = '%s%%' % str(opts['warning']) + if 'intier' in opts: + opts['intier'] = 'on' if opts['intier'] else 'off' + if 'autoexpand' in opts: + opts['autoexpand'] = 'on' if opts['autoexpand'] else 'off' + + for key in changes: + self.ssh.chvdisk(vdisk, ['-' + key, opts[key]]) + + def change_vdisk_iogrp(self, vdisk, state, iogrp): + if state['code_level'] < (3, 0, 0, 0): + LOG.debug('Ignore change IO group as storage code level is ' + '%(code_level)s, below the required 3, 0, 0, 0.', + {'code_level': state['code_level']}) + else: + self.ssh.movevdisk(vdisk, str(iogrp[0])) + self.ssh.addvdiskaccess(vdisk, str(iogrp[0])) + self.ssh.rmvdiskaccess(vdisk, str(iogrp[1])) + + def vdisk_by_uid(self, vdisk_uid): + """Returns the properties of the vdisk with the specified UID. + + Returns None if no such disk exists. + """ + + vdisks = self.ssh.lsvdisks_from_filter('vdisk_UID', vdisk_uid) + + if len(vdisks) == 0: + return None + + if len(vdisks) != 1: + msg = (_('Expected single vdisk returned from lsvdisk when ' + 'filtering on vdisk_UID. %(count)s were returned.') % + {'count': len(vdisks)}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + vdisk = vdisks.result[0] + + return self.ssh.lsvdisk(vdisk['name']) + + def is_vdisk_in_use(self, vdisk): + """Returns True if the specified vdisk is mapped to at least 1 host.""" + resp = self.ssh.lsvdiskhostmap(vdisk) + return len(resp) != 0 + + def rename_vdisk(self, vdisk, new_name): + self.ssh.chvdisk(vdisk, ['-name', new_name]) + + def change_vdisk_primary_copy(self, vdisk, copy_id): + self.ssh.chvdisk(vdisk, ['-primary', copy_id]) + + +class InStorageSSH(object): + """SSH interface to Inspur InStorage systems.""" + + def __init__(self, run_ssh): + self._ssh = run_ssh + + def _run_ssh(self, ssh_cmd): + try: + return self._ssh(ssh_cmd) + except processutils.ProcessExecutionError as e: + msg = (_('CLI Exception output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': e.stdout, + 'err': e.stderr}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def run_ssh_inq(self, ssh_cmd, delim='!', with_header=False): + """Run an SSH command and return parsed output.""" + raw = self._run_ssh(ssh_cmd) + return CLIParser(raw, ssh_cmd=ssh_cmd, delim=delim, + with_header=with_header) + + def run_ssh_assert_no_output(self, ssh_cmd): + """Run an SSH command and assert no output returned.""" + out, err = self._run_ssh(ssh_cmd) + if len(out.strip()) != 0: + msg = (_('Expected no output from CLI command %(cmd)s, ' + 'got %(out)s.') % {'cmd': ' '.join(ssh_cmd), 'out': out}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def run_ssh_check_created(self, ssh_cmd): + """Run an SSH command and return the ID of the created object.""" + out, err = self._run_ssh(ssh_cmd) + try: + match_obj = re.search(r'\[([0-9]+)\],? successfully created', out) + return match_obj.group(1) + except (AttributeError, IndexError): + msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': out, + 'err': err}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def lsnode(self, node_id=None): + with_header = True + ssh_cmd = ['mcsinq', 'lsnode', '-delim', '!'] + if node_id: + with_header = False + ssh_cmd.append(node_id) + return self.run_ssh_inq(ssh_cmd, with_header=with_header) + + def lslicense(self): + ssh_cmd = ['mcsinq', 'lslicense', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd)[0] + + def lsguicapabilities(self): + ssh_cmd = ['mcsinq', 'lsguicapabilities', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd)[0] + + def lssystem(self): + ssh_cmd = ['mcsinq', 'lssystem', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd)[0] + + def lsmdiskgrp(self, pool): + ssh_cmd = ['mcsinq', 'lsmdiskgrp', '-bytes', '-delim', '!', + '"%s"' % pool] + return self.run_ssh_inq(ssh_cmd)[0] + + def lsiogrp(self): + ssh_cmd = ['mcsinq', 'lsiogrp', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsportip(self): + ssh_cmd = ['mcsinq', 'lsportip', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lshost(self, host=None): + with_header = True + ssh_cmd = ['mcsinq', 'lshost', '-delim', '!'] + if host: + with_header = False + ssh_cmd.append('"%s"' % host) + return self.run_ssh_inq(ssh_cmd, with_header=with_header) + + def lsiscsiauth(self): + ssh_cmd = ['mcsinq', 'lsiscsiauth', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsfabric(self, wwpn=None, host=None): + ssh_cmd = ['mcsinq', 'lsfabric', '-delim', '!'] + if wwpn: + ssh_cmd.extend(['-wwpn', wwpn]) + elif host: + ssh_cmd.extend(['-host', '"%s"' % host]) + else: + msg = (_('Must pass wwpn or host to lsfabric.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsrcrelationship(self, rc_rel): + key_value = 'name=%s' % rc_rel + ssh_cmd = ['mcsinq', 'lsrcrelationship', '-filtervalue', + key_value, '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lspartnership(self, system_name): + key_value = 'name=%s' % system_name + ssh_cmd = ['mcsinq', 'lspartnership', '-filtervalue', + key_value, '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lspartnershipcandidate(self): + ssh_cmd = ['mcsinq', 'lspartnershipcandidate', '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsvdiskhostmap(self, vdisk): + ssh_cmd = ['mcsinq', 'lsvdiskhostmap', '-delim', '!', '"%s"' % vdisk] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lshostvdiskmap(self, host): + ssh_cmd = ['mcsinq', 'lshostvdiskmap', '-delim', '!', '"%s"' % host] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsvdisk(self, vdisk): + """Return vdisk attributes or None if it doesn't exist.""" + ssh_cmd = ['mcsinq', 'lsvdisk', '-bytes', '-delim', '!', + '"%s"' % vdisk] + out, err = self._ssh(ssh_cmd, check_exit_code=False) + if not err: + return CLIParser((out, err), ssh_cmd=ssh_cmd, delim='!', + with_header=False)[0] + if 'CMMVC5754E' in err: + return None + msg = (_('CLI Exception output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': out, + 'err': err}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def lsvdisks_from_filter(self, filter_name, value): + """Performs an lsvdisk command, filtering the results as specified. + + Returns an iterable for all matching vdisks. + """ + ssh_cmd = ['mcsinq', 'lsvdisk', '-bytes', '-delim', '!', + '-filtervalue', '%s=%s' % (filter_name, value)] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lsvdisklcmappings(self, vdisk): + ssh_cmd = ['mcsinq', 'lsvdisklcmappings', '-delim', '!', + '"%s"' % vdisk] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lslcmap(self, lc_map_id): + ssh_cmd = ['mcsinq', 'lslcmap', '-filtervalue', + 'id=%s' % lc_map_id, '-delim', '!'] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + def lslcconsistgrp(self, lc_consistgrp): + ssh_cmd = ['mcsinq', 'lslcconsistgrp', '-delim', '!', lc_consistgrp] + out, err = self._ssh(ssh_cmd) + return CLIParser((out, err), ssh_cmd=ssh_cmd, delim='!', + with_header=False) + + def lsvdiskcopy(self, vdisk, copy_id=None): + ssh_cmd = ['mcsinq', 'lsvdiskcopy', '-delim', '!'] + with_header = True + if copy_id: + ssh_cmd += ['-copy', copy_id] + with_header = False + ssh_cmd += ['"%s"' % vdisk] + return self.run_ssh_inq(ssh_cmd, with_header=with_header) + + def lsvdisksyncprogress(self, vdisk, copy_id): + ssh_cmd = ['mcsinq', 'lsvdisksyncprogress', '-delim', '!', + '-copy', copy_id, '"%s"' % vdisk] + return self.run_ssh_inq(ssh_cmd, with_header=True)[0] + + def lsportfc(self, node_id): + ssh_cmd = ['mcsinq', 'lsportfc', '-delim', '!', + '-filtervalue', 'node_id=%s' % node_id] + return self.run_ssh_inq(ssh_cmd, with_header=True) + + @staticmethod + def _create_port_arg(port_type, port_name): + if port_type == 'initiator': + port = ['-iscsiname'] + else: + port = ['-hbawwpn'] + port.append(port_name) + return port + + def mkhost(self, host_name, port_type, port_name): + port = self._create_port_arg(port_type, port_name) + ssh_cmd = ['mcsop', 'mkhost', '-force'] + port + ssh_cmd += ['-name', '"%s"' % host_name] + return self.run_ssh_check_created(ssh_cmd) + + def addhostport(self, host, port_type, port_name): + port = self._create_port_arg(port_type, port_name) + ssh_cmd = ['mcsop', 'addhostport', '-force'] + port + ['"%s"' % host] + self.run_ssh_assert_no_output(ssh_cmd) + + def add_chap_secret(self, secret, host): + ssh_cmd = ['mcsop', 'chhost', '-chapsecret', secret, '"%s"' % host] + self.run_ssh_assert_no_output(ssh_cmd) + + def mkvdiskhostmap(self, host, vdisk, lun, multihostmap): + """Map vdisk to host. + + If vdisk already mapped and multihostmap is True, use the force flag. + """ + ssh_cmd = ['mcsop', 'mkvdiskhostmap', '-host', '"%s"' % host, vdisk] + + if lun: + ssh_cmd.insert(ssh_cmd.index(vdisk), '-scsi') + ssh_cmd.insert(ssh_cmd.index(vdisk), lun) + + if multihostmap: + ssh_cmd.insert(ssh_cmd.index('mkvdiskhostmap') + 1, '-force') + try: + self.run_ssh_check_created(ssh_cmd) + result_lun = self.get_vdiskhostmapid(vdisk, host) + if result_lun is None or (lun and lun != result_lun): + msg = (_('mkvdiskhostmap error:\n command: %(cmd)s\n ' + 'lun: %(lun)s\n result_lun: %(result_lun)s') % + {'cmd': ssh_cmd, + 'lun': lun, + 'result_lun': result_lun}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + return result_lun + except Exception as ex: + if (not multihostmap and hasattr(ex, 'message') and + 'CMMVC6071E' in ex.message): + LOG.error('volume is not allowed to be mapped to multi host') + raise exception.VolumeDriverException( + message=_('CMMVC6071E The VDisk-to-host mapping was not ' + 'created because the VDisk is already mapped ' + 'to a host.\n"')) + with excutils.save_and_reraise_exception(): + LOG.error('Error mapping VDisk-to-host') + + def mkrcrelationship(self, master, aux, system, asynccopy): + ssh_cmd = ['mcsop', 'mkrcrelationship', '-master', master, + '-aux', aux, '-cluster', system] + if asynccopy: + ssh_cmd.append('-async') + return self.run_ssh_check_created(ssh_cmd) + + def rmrcrelationship(self, relationship, force=False): + ssh_cmd = ['mcsop', 'rmrcrelationship'] + if force: + ssh_cmd += ['-force'] + ssh_cmd += [relationship] + self.run_ssh_assert_no_output(ssh_cmd) + + def switchrelationship(self, relationship, aux=True): + primary = 'aux' if aux else 'master' + ssh_cmd = ['mcsop', 'switchrcrelationship', '-primary', + primary, relationship] + self.run_ssh_assert_no_output(ssh_cmd) + + def startrcrelationship(self, rc_rel, primary=None): + ssh_cmd = ['mcsop', 'startrcrelationship', '-force'] + if primary: + ssh_cmd.extend(['-primary', primary]) + ssh_cmd.append(rc_rel) + self.run_ssh_assert_no_output(ssh_cmd) + + def stoprcrelationship(self, relationship, access=False): + ssh_cmd = ['mcsop', 'stoprcrelationship'] + if access: + ssh_cmd.append('-access') + ssh_cmd.append(relationship) + self.run_ssh_assert_no_output(ssh_cmd) + + def mkippartnership(self, ip_v4, bandwith=1000, backgroundcopyrate=50): + ssh_cmd = ['mcsop', 'mkippartnership', '-type', 'ipv4', + '-clusterip', ip_v4, '-linkbandwidthmbits', + six.text_type(bandwith), + '-backgroundcopyrate', six.text_type(backgroundcopyrate)] + return self.run_ssh_assert_no_output(ssh_cmd) + + def mkfcpartnership(self, system_name, bandwith=1000, + backgroundcopyrate=50): + ssh_cmd = ['mcsop', 'mkfcpartnership', '-linkbandwidthmbits', + six.text_type(bandwith), + '-backgroundcopyrate', six.text_type(backgroundcopyrate), + system_name] + return self.run_ssh_assert_no_output(ssh_cmd) + + def chpartnership(self, partnership_id, start=True): + action = '-start' if start else '-stop' + ssh_cmd = ['mcsop', 'chpartnership', action, partnership_id] + return self.run_ssh_assert_no_output(ssh_cmd) + + def rmvdiskhostmap(self, host, vdisk): + ssh_cmd = ['mcsop', 'rmvdiskhostmap', '-host', '"%s"' % host, + '"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def get_vdiskhostmapid(self, vdisk, host): + resp = self.lsvdiskhostmap(vdisk) + for mapping_info in resp: + if mapping_info['host_name'] == host: + lun_id = mapping_info['SCSI_id'] + return lun_id + return None + + def rmhost(self, host): + ssh_cmd = ['mcsop', 'rmhost', '"%s"' % host] + self.run_ssh_assert_no_output(ssh_cmd) + + def mkvdisk(self, name, size, units, pool, opts, params): + ssh_cmd = ['mcsop', 'mkvdisk', '-name', name, '-mdiskgrp', + '"%s"' % pool, '-iogrp', six.text_type(opts['iogrp']), + '-size', size, '-unit', units] + params + try: + return self.run_ssh_check_created(ssh_cmd) + except Exception as ex: + if hasattr(ex, 'msg') and 'CMMVC6372W' in ex.msg: + vdisk = self.lsvdisk(name) + if vdisk: + LOG.warning('CMMVC6372W The virtualized storage ' + 'capacity that the cluster is using is ' + 'approaching the virtualized storage ' + 'capacity that is licensed.') + return vdisk['id'] + with excutils.save_and_reraise_exception(): + LOG.exception('Failed to create vdisk %(vol)s.', {'vol': name}) + + def rmvdisk(self, vdisk, force=True): + ssh_cmd = ['mcsop', 'rmvdisk'] + if force: + ssh_cmd += ['-force'] + ssh_cmd += ['"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def chvdisk(self, vdisk, params): + ssh_cmd = ['mcsop', 'chvdisk'] + params + ['"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def movevdisk(self, vdisk, iogrp): + ssh_cmd = ['mcsop', 'movevdisk', '-iogrp', iogrp, '"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def expandvdisksize(self, vdisk, amount): + ssh_cmd = ( + ['mcsop', 'expandvdisksize', '-size', six.text_type(amount), + '-unit', 'gb', '"%s"' % vdisk]) + self.run_ssh_assert_no_output(ssh_cmd) + + def mklcmap(self, source, target, full_copy, copy_rate, consistgrp=None): + ssh_cmd = ['mcsop', 'mklcmap', '-source', '"%s"' % source, '-target', + '"%s"' % target, '-autodelete'] + if not full_copy: + ssh_cmd.extend(['-copyrate', '0']) + else: + ssh_cmd.extend(['-copyrate', six.text_type(copy_rate)]) + if consistgrp: + ssh_cmd.extend(['-consistgrp', consistgrp]) + out, err = self._ssh(ssh_cmd, check_exit_code=False) + if 'successfully created' not in out: + msg = (_('CLI Exception output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': out, + 'err': err}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + try: + match_obj = re.search(r'LocalCopy Mapping, id \[([0-9]+)\], ' + 'successfully created', out) + lc_map_id = match_obj.group(1) + except (AttributeError, IndexError): + msg = (_('Failed to parse CLI output:\n command: %(cmd)s\n ' + 'stdout: %(out)s\n stderr: %(err)s.') % + {'cmd': ssh_cmd, + 'out': out, + 'err': err}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + return lc_map_id + + def prestartlcmap(self, lc_map_id): + ssh_cmd = ['mcsop', 'prestartlcmap', lc_map_id] + self.run_ssh_assert_no_output(ssh_cmd) + + def startlcmap(self, lc_map_id): + ssh_cmd = ['mcsop', 'startlcmap', lc_map_id] + self.run_ssh_assert_no_output(ssh_cmd) + + def prestartlcconsistgrp(self, lc_consist_group): + ssh_cmd = ['mcsop', 'prestartlcconsistgrp', lc_consist_group] + self.run_ssh_assert_no_output(ssh_cmd) + + def startlcconsistgrp(self, lc_consist_group): + ssh_cmd = ['mcsop', 'startlcconsistgrp', lc_consist_group] + self.run_ssh_assert_no_output(ssh_cmd) + + def stoplcconsistgrp(self, lc_consist_group): + ssh_cmd = ['mcsop', 'stoplcconsistgrp', lc_consist_group] + self.run_ssh_assert_no_output(ssh_cmd) + + def chlcmap(self, lc_map_id, copyrate='50', autodel='on'): + ssh_cmd = ['mcsop', 'chlcmap', '-copyrate', copyrate, + '-autodelete', autodel, lc_map_id] + self.run_ssh_assert_no_output(ssh_cmd) + + def stoplcmap(self, lc_map_id): + ssh_cmd = ['mcsop', 'stoplcmap', lc_map_id] + self.run_ssh_assert_no_output(ssh_cmd) + + def rmlcmap(self, lc_map_id): + ssh_cmd = ['mcsop', 'rmlcmap', '-force', lc_map_id] + self.run_ssh_assert_no_output(ssh_cmd) + + def mklcconsistgrp(self, lc_consist_group): + ssh_cmd = ['mcsop', 'mklcconsistgrp', '-name', lc_consist_group] + return self.run_ssh_check_created(ssh_cmd) + + def rmlcconsistgrp(self, lc_consist_group): + ssh_cmd = ['mcsop', 'rmlcconsistgrp', '-force', lc_consist_group] + return self.run_ssh_assert_no_output(ssh_cmd) + + def addvdiskcopy(self, vdisk, dest_pool, params): + ssh_cmd = (['mcsop', 'addvdiskcopy'] + + params + + ['-mdiskgrp', '"%s"' % + dest_pool, '"%s"' % + vdisk]) + return self.run_ssh_check_created(ssh_cmd) + + def rmvdiskcopy(self, vdisk, copy_id): + ssh_cmd = ['mcsop', 'rmvdiskcopy', '-copy', copy_id, '"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def addvdiskaccess(self, vdisk, iogrp): + ssh_cmd = ['mcsop', 'addvdiskaccess', '-iogrp', iogrp, + '"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + def rmvdiskaccess(self, vdisk, iogrp): + ssh_cmd = ['mcsop', 'rmvdiskaccess', '-iogrp', iogrp, '"%s"' % vdisk] + self.run_ssh_assert_no_output(ssh_cmd) + + +class CLIParser(object): + """Parse MCS CLI output and generate iterable.""" + + def __init__(self, raw, ssh_cmd=None, delim='!', with_header=True): + super(CLIParser, self).__init__() + if ssh_cmd: + self.ssh_cmd = ' '.join(ssh_cmd) + else: + self.ssh_cmd = 'None' + self.raw = raw + self.delim = delim + self.with_header = with_header + self.result = self._parse() + + def select(self, *keys): + for a in self.result: + vs = [] + for k in keys: + v = a.get(k, None) + if isinstance(v, six.string_types) or v is None: + v = [v] + if isinstance(v, list): + vs.append(v) + for item in zip(*vs): + if len(item) == 1: + yield item[0] + else: + yield item + + def __getitem__(self, key): + try: + return self.result[key] + except KeyError: + msg = (_('Did not find the expected key %(key)s in %(fun)s: ' + '%(raw)s.') % {'key': key, 'fun': self.ssh_cmd, + 'raw': self.raw}) + raise exception.VolumeBackendAPIException(data=msg) + + def __iter__(self): + for a in self.result: + yield a + + def __len__(self): + return len(self.result) + + def _parse(self): + def get_reader(content, delim): + for line in content.lstrip().splitlines(): + line = line.strip() + if line: + yield line.split(delim) + else: + yield [] + + if isinstance(self.raw, six.string_types): + stdout, stderr = self.raw, '' + else: + stdout, stderr = self.raw + reader = get_reader(stdout, self.delim) + result = [] + + if self.with_header: + hds = tuple() + for row in reader: + hds = row + break + for row in reader: + cur = dict() + if len(hds) != len(row): + msg = (_('Unexpected CLI response: header/row mismatch. ' + 'header: %(header)s, row: %(row)s.') + % {'header': hds, + 'row': row}) + raise exception.VolumeBackendAPIException(data=msg) + for k, v in zip(hds, row): + CLIParser.append_dict(cur, k, v) + result.append(cur) + else: + cur = dict() + for row in reader: + if row: + CLIParser.append_dict(cur, row[0], ' '.join(row[1:])) + elif cur: # start new section + result.append(cur) + cur = dict() + if cur: + result.append(cur) + return result + + @staticmethod + def append_dict(dict_, key, value): + key, value = key.strip(), value.strip() + obj = dict_.get(key, None) + if obj is None: + dict_[key] = value + elif isinstance(obj, list): + obj.append(value) + dict_[key] = obj + else: + dict_[key] = [obj, value] + return dict_ diff --git a/cinder/volume/drivers/inspur/instorage/instorage_const.py b/cinder/volume/drivers/inspur/instorage/instorage_const.py new file mode 100644 index 00000000000..02ae25cf27d --- /dev/null +++ b/cinder/volume/drivers/inspur/instorage/instorage_const.py @@ -0,0 +1,40 @@ +# Copyright 2017 Inspur Corp. +# 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. +# + +DEV_MODEL_INSTORAGE = '1813' +DEV_MODEL_INSTORAGE_AS5X00 = '2076' + + +REP_CAP_DEVS = (DEV_MODEL_INSTORAGE, DEV_MODEL_INSTORAGE_AS5X00) + +# constants used for replication +ASYNC = 'async' +SYNC = 'sync' +VALID_REP_TYPES = (ASYNC, SYNC) +FAILBACK_VALUE = 'default' + +DEFAULT_RC_TIMEOUT = 3600 * 24 * 7 +DEFAULT_RC_INTERVAL = 5 + +REPLICA_AUX_VOL_PREFIX = 'aux_' + +# remote mirror copy status +REP_CONSIS_SYNC = 'consistent_synchronized' +REP_CONSIS_STOP = 'consistent_stopped' +REP_SYNC = 'synchronized' +REP_IDL = 'idling' +REP_IDL_DISC = 'idling_disconnected' +REP_STATUS_ON_LINE = 'online' diff --git a/cinder/volume/drivers/inspur/instorage/instorage_iscsi.py b/cinder/volume/drivers/inspur/instorage/instorage_iscsi.py new file mode 100644 index 00000000000..fc5d54f14f1 --- /dev/null +++ b/cinder/volume/drivers/inspur/instorage/instorage_iscsi.py @@ -0,0 +1,298 @@ +# Copyright 2017 Inspur Corp. +# 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. +# +""" +ISCSI volume driver for Inspur InStorage family and MCS storage systems. + +Notes: +1. Make sure you config the password or key file. If you specify both +a password and a key file, this driver will use the key file only. +2. When a key file is used for authentication, the private key is stored +in a secure manner by the user or system administrator. +3. The defaults for creating volumes are +"-rsize 2% -autoexpand -grainsize 256 -warning 0". +These can be changed in the configuration file +or by using volume types(recommended only for advanced users). + +Limitations: +1. The driver expects CLI output in English, +but the error messages may be in a localized format. +2. when you clone or create volumes from snapshots, +it not support that the source and target_rep are different size. + +Perform necessary work to make an iSCSI connection: +To be able to create an iSCSI connection from a given host to a volume, +we must: +1. Translate the given iSCSI name to a host name +2. Create new host on the storage system if it does not yet exist +3. Map the volume to the host if it is not already done +4. Return the connection information for relevant nodes +(in the proper I/O group) +""" + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import excutils +import six + +from cinder import coordination +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils as cinder_utils +from cinder.volume import driver + +from cinder.volume.drivers.inspur.instorage import instorage_common + +LOG = logging.getLogger(__name__) + +instorage_mcs_iscsi_opts = [ + cfg.BoolOpt('instorage_mcs_iscsi_chap_enabled', + default=True, + help='Configure CHAP authentication for iSCSI connections ' + '(Default: Enabled)'), +] + +CONF = cfg.CONF +CONF.register_opts(instorage_mcs_iscsi_opts) + + +@interface.volumedriver +class InStorageMCSISCSIDriver(instorage_common.InStorageMCSCommonDriver, + driver.ISCSIDriver): + """Inspur InStorage iSCSI volume driver. + + Version history: + + .. code-block:: none + + 1.0 - Initial driver + """ + + VERSION = "1.0.0" + + # ThirdPartySystems wiki page + CI_WIKI_NAME = "INSPUR_CI" + + def __init__(self, *args, **kwargs): + super(InStorageMCSISCSIDriver, self).__init__(*args, **kwargs) + self.protocol = 'iSCSI' + self.configuration.append_config_values( + instorage_mcs_iscsi_opts) + + @cinder_utils.trace + @coordination.synchronized('instorage-host' + '{self._state[system_id]}' + '{connector[host]}') + def initialize_connection(self, volume, connector): + """Perform necessary work to make an iSCSI connection.""" + volume_name = self._get_target_vol(volume) + + # Check if a host object is defined for this host name + host_name = self._assistant.get_host_from_connector(connector) + if host_name is None: + # Host does not exist - add a new host to InStorage/MCS + host_name = self._assistant.create_host(connector) + + chap_secret = self._assistant.get_chap_secret_for_host(host_name) + chap_enabled = self.configuration.instorage_mcs_iscsi_chap_enabled + if chap_enabled and chap_secret is None: + chap_secret = self._assistant.add_chap_secret_to_host(host_name) + elif not chap_enabled and chap_secret: + LOG.warning('CHAP secret exists for host but CHAP is disabled.') + + lun_id = self._assistant.map_vol_to_host(volume_name, + host_name, + False) + + try: + properties = self._get_single_iscsi_data(volume, connector, + lun_id, chap_secret) + multipath = connector.get('multipath', False) + if multipath: + properties = self._get_multi_iscsi_data(volume, connector, + lun_id, properties) + except Exception: + with excutils.save_and_reraise_exception(): + self._do_terminate_connection(volume, connector) + LOG.error('initialize_connection: Failed ' + 'to collect return ' + 'properties for volume %(vol)s and connector ' + '%(conn)s.\n', {'vol': volume, 'conn': connector}) + + return {'driver_volume_type': 'iscsi', 'data': properties} + + @cinder_utils.trace + def _get_single_iscsi_data(self, volume, connector, lun_id, chap_secret): + volume_name = self._get_target_vol(volume) + volume_attributes = self._assistant.get_vdisk_attributes(volume_name) + if volume_attributes is None: + msg = (_('_get_single_iscsi_data: Failed to get attributes' + ' for volume %s.') % volume_name) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + try: + preferred_node = volume_attributes['preferred_node_id'] + IO_group = volume_attributes['IO_group_id'] + except KeyError as e: + msg = (_('_get_single_iscsi_data: Did not find expected column' + ' name in %(volume)s: %(key)s %(error)s.'), + {'volume': volume_name, 'key': e.args[0], + 'error': e}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + # Get preferred node and other nodes in I/O group + preferred_node_entry = None + io_group_nodes = [] + for node in self._state['storage_nodes'].values(): + if self.protocol not in node['enabled_protocols']: + continue + + if node['IO_group'] != IO_group: + continue + io_group_nodes.append(node) + if node['id'] == preferred_node: + preferred_node_entry = node + + if not len(io_group_nodes): + msg = (_('_get_single_iscsi_data: No node found in ' + 'I/O group %(gid)s for volume %(vol)s.') % { + 'gid': IO_group, 'vol': volume_name}) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + if not preferred_node_entry: + # Get 1st node in I/O group + preferred_node_entry = io_group_nodes[0] + LOG.warning('_get_single_iscsi_data: Did not find a ' + 'preferred node for volume %s.', volume_name) + + properties = { + 'target_discovered': False, + 'target_lun': lun_id, + 'volume_id': volume.id} + + if preferred_node_entry['ipv4']: + ipaddr = preferred_node_entry['ipv4'][0] + else: + ipaddr = '[%s]' % preferred_node_entry['ipv6'][0] + # ipv6 need surround with brackets when it use port + properties['target_portal'] = '%s:%s' % (ipaddr, '3260') + properties['target_iqn'] = preferred_node_entry['iscsi_name'] + if chap_secret: + properties.update(auth_method='CHAP', + auth_username=connector['initiator'], + auth_password=chap_secret, + discovery_auth_method='CHAP', + discovery_auth_username=connector['initiator'], + discovery_auth_password=chap_secret) + return properties + + @cinder_utils.trace + def _get_multi_iscsi_data(self, volume, connector, lun_id, properties): + try: + resp = self._assistant.ssh.lsportip() + except Exception as ex: + msg = (_('_get_multi_iscsi_data: Failed to ' + 'get port ip because of exception: ' + '%s.') % six.text_type(ex)) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + properties['target_iqns'] = [] + properties['target_portals'] = [] + properties['target_luns'] = [] + for node in self._state['storage_nodes'].values(): + for ip_data in resp: + if ip_data['node_id'] != node['id']: + continue + link_state = ip_data.get('link_state', None) + valid_port = '' + if ((ip_data['state'] == 'configured' and + link_state == 'active') or + ip_data['state'] == 'online'): + valid_port = (ip_data['IP_address'] or + ip_data['IP_address_6']) + if valid_port: + properties['target_portals'].append( + '%s:%s' % (valid_port, '3260')) + properties['target_iqns'].append( + node['iscsi_name']) + properties['target_luns'].append(lun_id) + + if not len(properties['target_portals']): + msg = (_('_get_multi_iscsi_data: Failed to find valid port ' + 'for volume %s.') % volume.name) + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + return properties + + def terminate_connection(self, volume, connector, **kwargs): + """Cleanup after an iSCSI connection has been terminated.""" + # If a fake connector is generated by nova when the host + # is down, then the connector will not have a host property, + # In this case construct the lock without the host property + # so that all the fake connectors to an MCS are serialized + host = "" + if connector is not None and 'host' in connector: + host = connector['host'] + + @coordination.synchronized('instorage-host' + self._state['system_id'] + + host) + def _do_terminate_connection_locked(): + return self._do_terminate_connection(volume, connector, **kwargs) + return _do_terminate_connection_locked() + + @cinder_utils.trace + def _do_terminate_connection(self, volume, connector, **kwargs): + """Cleanup after an iSCSI connection has been terminated. + + When we clean up a terminated connection between a given connector + and volume, we: + 1. Translate the given connector to a host name + 2. Remove the volume-to-host mapping if it exists + 3. Delete the host if it has no more mappings (hosts are created + automatically by this driver when mappings are created) + """ + vol_name = self._get_target_vol(volume) + + info = {} + if connector is not None and 'host' in connector: + # get host according to iSCSI protocol + info = {'driver_volume_type': 'iscsi', + 'data': {}} + + host_name = self._assistant.get_host_from_connector(connector) + if host_name is None: + msg = (_('terminate_connection: Failed to get host name from' + ' connector.')) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + else: + host_name = None + + # Unmap volumes, if hostname is None, need to get value from vdiskmap + host_name = self._assistant.unmap_vol_from_host(vol_name, host_name) + + # Host_name could be none + if host_name: + resp = self._assistant.check_host_mapped_vols(host_name) + if not len(resp): + self._assistant.delete_host(host_name) + + return info diff --git a/cinder/volume/drivers/inspur/instorage/replication.py b/cinder/volume/drivers/inspur/instorage/replication.py new file mode 100644 index 00000000000..ddfedc12659 --- /dev/null +++ b/cinder/volume/drivers/inspur/instorage/replication.py @@ -0,0 +1,240 @@ +# Copyright 2017 Inspur Corp. +# 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. +# + +import random + +from eventlet import greenthread +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils +import six + +from cinder import exception +from cinder.i18n import _ +from cinder.objects import fields +from cinder import ssh_utils +from cinder import utils as cinder_utils +from cinder.volume.drivers.inspur.instorage import instorage_const + +LOG = logging.getLogger(__name__) + + +class InStorageMCSReplicationManager(object): + + def __init__(self, driver, replication_target=None, target_assistant=None): + self.sshpool = None + self.driver = driver + self.target = replication_target + self.target_assistant = target_assistant(self._run_ssh) + self._local_assistant = self.driver._local_backend_assistant + self.async_m = InStorageMCSReplicationAsyncCopy( + self.driver, replication_target, self.target_assistant) + self.sync_m = InStorageMCSReplicationSyncCopy( + self.driver, replication_target, self.target_assistant) + + def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1): + cinder_utils.check_ssh_injection(cmd_list) + command = ' '. join(cmd_list) + + if not self.sshpool: + self.sshpool = ssh_utils.SSHPool( + self.target.get('san_ip'), + self.target.get('san_ssh_port', 22), + self.target.get('ssh_conn_timeout', 30), + self.target.get('san_login'), + password=self.target.get('san_password'), + privatekey=self.target.get('san_private_key', ''), + min_size=self.target.get('ssh_min_pool_conn', 1), + max_size=self.target.get('ssh_max_pool_conn', 5),) + last_exception = None + try: + with self.sshpool.item() as ssh: + while attempts > 0: + attempts -= 1 + try: + return processutils.ssh_execute( + ssh, command, check_exit_code=check_exit_code) + except Exception as e: + LOG.error(e) + last_exception = e + greenthread.sleep(random.randint(20, 500) / 100.0) + try: + raise processutils.ProcessExecutionError( + exit_code=last_exception.exit_code, + stdout=last_exception.stdout, + stderr=last_exception.stderr, + cmd=last_exception.cmd) + except AttributeError: + raise processutils.ProcessExecutionError( + exit_code=-1, stdout="", + stderr="Error running SSH command", + cmd=command) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Error running SSH command: %s", command) + + def get_target_assistant(self): + return self.target_assistant + + def get_replica_obj(self, rep_type): + if rep_type == instorage_const.ASYNC: + return self.async_m + elif rep_type == instorage_const.SYNC: + return self.sync_m + else: + return None + + def _partnership_validate_create(self, client, remote_name, remote_ip): + try: + partnership_info = client.get_partnership_info(remote_name) + if not partnership_info: + candidate_info = client.get_partnershipcandidate_info( + remote_name) + if candidate_info: + client.mkfcpartnership(remote_name) + else: + client.mkippartnership(remote_ip) + partnership_info = client.get_partnership_info(remote_name) + if partnership_info['partnership'] != 'fully_configured': + client.chpartnership(partnership_info['id']) + except Exception: + msg = (_('Unable to establish the partnership with ' + 'the InStorage cluster %s.') % remote_name) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + def establish_target_partnership(self): + local_system_info = self._local_assistant.get_system_info() + target_system_info = self.target_assistant.get_system_info() + local_system_name = local_system_info['system_name'] + target_system_name = target_system_info['system_name'] + local_ip = self.driver.configuration.safe_get('san_ip') + target_ip = self.target.get('san_ip') + # Establish partnership only when the local system and the replication + # target system is different. + if target_system_name != local_system_name: + self._partnership_validate_create(self._local_assistant, + target_system_name, target_ip) + self._partnership_validate_create(self.target_assistant, + local_system_name, local_ip) + + +class InStorageMCSReplication(object): + + def __init__(self, asynccopy, driver, + replication_target=None, target_assistant=None): + + self.asynccopy = asynccopy + self.driver = driver + self.target = replication_target or {} + self.target_assistant = target_assistant + + @cinder_utils.trace + def volume_replication_setup(self, context, vref): + target_vol_name = instorage_const.REPLICA_AUX_VOL_PREFIX + vref.name + try: + attr = self.target_assistant.get_vdisk_attributes(target_vol_name) + if not attr: + opts = self.driver._get_vdisk_params(vref.volume_type_id) + pool = self.target.get('pool_name') + src_attr = self.driver._assistant.get_vdisk_attributes( + vref.name) + opts['iogrp'] = src_attr['IO_group_id'] + self.target_assistant.create_vdisk(target_vol_name, + six.text_type(vref['size']), + 'gb', pool, opts) + + system_info = self.target_assistant.get_system_info() + self.driver._assistant.create_relationship( + vref.name, target_vol_name, system_info.get('system_name'), + self.asynccopy) + except Exception as e: + msg = (_("Unable to set up copy mode replication for %(vol)s. " + "Exception: %(err)s.") % {'vol': vref.id, 'err': e}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + @cinder_utils.trace + def failover_volume_host(self, context, vref): + target_vol = instorage_const.REPLICA_AUX_VOL_PREFIX + vref.name + + try: + rel_info = self.target_assistant.get_relationship_info(target_vol) + # Reverse the role of the primary and secondary volumes + self.target_assistant.switch_relationship(rel_info['name']) + return {'replication_status': fields.ReplicationStatus.FAILED_OVER} + except Exception as e: + LOG.exception('Unable to fail-over the volume %(id)s to the ' + 'secondary back-end by switchrcrelationship ' + 'command.', {"id": vref.id}) + # If the switch command fail, try to make the aux volume + # writeable again. + try: + self.target_assistant.stop_relationship(target_vol, + access=True) + return { + 'replication_status': fields.ReplicationStatus.FAILED_OVER} + except Exception as e: + msg = (_('Unable to fail-over the volume %(id)s to the ' + 'secondary back-end, error: %(error)s') % + {"id": vref.id, "error": six.text_type(e)}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + def replication_failback(self, volume): + tgt_volume = instorage_const.REPLICA_AUX_VOL_PREFIX + volume.name + rel_info = self.target_assistant.get_relationship_info(tgt_volume) + if rel_info: + try: + self.target_assistant.switch_relationship(rel_info['name'], + aux=False) + return {'replication_status': fields.ReplicationStatus.ENABLED, + 'status': 'available'} + except Exception as e: + msg = (_('Unable to fail-back the volume:%(vol)s to the ' + 'master back-end, error:%(error)s') % + {"vol": volume.name, "error": six.text_type(e)}) + LOG.error(msg) + raise exception.VolumeDriverException(message=msg) + + +class InStorageMCSReplicationAsyncCopy(InStorageMCSReplication): + """Support for InStorage/MCS async copy mode replication. + + Async Copy establishes a Async Copy relationship between + two volumes of equal size. The volumes in a Async Copy relationship + are referred to as the master (source) volume and the auxiliary + (target) volume. This mode is dedicated to the asynchronous volume + replication. + """ + + def __init__(self, driver, replication_target=None, target_assistant=None): + super(InStorageMCSReplicationAsyncCopy, self).__init__( + True, driver, replication_target, target_assistant) + + +class InStorageMCSReplicationSyncCopy(InStorageMCSReplication): + """Support for InStorage/MCS sync copy mode replication. + + Sync Copy establishes a Sync Copy relationship between + two volumes of equal size. The volumes in a Sync Copy relationship + are referred to as the master (source) volume and the auxiliary + (target) volume. + """ + + def __init__(self, driver, replication_target=None, target_assistant=None): + super(InStorageMCSReplicationSyncCopy, self).__init__( + False, driver, replication_target, target_assistant) diff --git a/releasenotes/notes/bp-inspur-instorage-driver-40371862c9559238.yaml b/releasenotes/notes/bp-inspur-instorage-driver-40371862c9559238.yaml new file mode 100644 index 00000000000..a9d78efba55 --- /dev/null +++ b/releasenotes/notes/bp-inspur-instorage-driver-40371862c9559238.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + New Cinder volume driver for Inspur InStorage. + The new driver supports iSCSI.