diff --git a/cinder/tests/unit/volume/drivers/huawei/test_huawei_drivers.py b/cinder/tests/unit/volume/drivers/huawei/test_huawei_drivers.py index 89d0f440c19..eae2dd61b11 100644 --- a/cinder/tests/unit/volume/drivers/huawei/test_huawei_drivers.py +++ b/cinder/tests/unit/volume/drivers/huawei/test_huawei_drivers.py @@ -544,7 +544,8 @@ FAKE_CREATE_SNAPSHOT_INFO_RESPONSE = """ }, "data": { "ID": "11", - "NAME": "YheUoRwbSX2BxN7" + "NAME": "YheUoRwbSX2BxN7", + "WWN": "fake-wwn" } } """ @@ -558,7 +559,8 @@ FAKE_GET_SNAPSHOT_INFO_RESPONSE = """ }, "data": { "ID": "11", - "NAME": "YheUoRwbSX2BxN7" + "NAME": "YheUoRwbSX2BxN7", + "WWN": "fake-wwn" } } """ @@ -2068,15 +2070,6 @@ MAP_COMMAND_TO_FAKE_RESPONSE['/mappingview/associate/portgroup?TYPE=245&ASSOC' REPLICA_BACKEND_ID = 'huawei-replica-1' -def cg_or_cg_snapshot(func): - def wrapper(self, *args, **kwargs): - self.mock_object(volume_utils, - 'is_group_a_cg_snapshot_type', - return_value=True) - return func(self, *args, **kwargs) - return wrapper - - class FakeHuaweiConf(huawei_conf.HuaweiConf): def __init__(self, conf, protocol): self.conf = conf @@ -2322,14 +2315,16 @@ class HuaweiTestBase(test.TestCase): @mock.patch.object(rest_client, 'RestClient') def test_create_snapshot_success(self, mock_client): lun_info = self.driver.create_snapshot(self.snapshot) - self.assertDictEqual({"huawei_snapshot_id": "11"}, - json.loads(lun_info['provider_location'])) + self.assertDictEqual( + {"huawei_snapshot_id": "11", "huawei_snapshot_wwn": "fake-wwn"}, + json.loads(lun_info['provider_location'])) self.snapshot.volume_id = ID self.snapshot.volume = self.volume lun_info = self.driver.create_snapshot(self.snapshot) - self.assertDictEqual({"huawei_snapshot_id": "11"}, - json.loads(lun_info['provider_location'])) + self.assertDictEqual( + {"huawei_snapshot_id": "11", "huawei_snapshot_wwn": "fake-wwn"}, + json.loads(lun_info['provider_location'])) @ddt.data('1', '', '0') def test_copy_volume(self, input_speed): @@ -4537,53 +4532,53 @@ class HuaweiISCSIDriverTestCase(HuaweiTestBase): iqn = self.driver.client._get_tgt_iqn_from_rest(ip) self.assertIsNone(iqn) - @cg_or_cg_snapshot def test_create_group_snapshot(self): test_snapshots = [self.snapshot] ctxt = context.get_admin_context() - model, snapshots = ( - self.driver.create_group_snapshot(ctxt, self.group_snapshot, - test_snapshots)) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + model, snapshots = self.driver.create_group_snapshot( + ctxt, self.group_snapshot, test_snapshots) self.assertEqual('21ec7341-9256-497b-97d9-ef48edcf0635', snapshots[0]['id']) self.assertEqual('available', snapshots[0]['status']) - self.assertDictEqual({'huawei_snapshot_id': '11'}, - json.loads(snapshots[0]['provider_location'])) + self.assertDictEqual( + {'huawei_snapshot_id': '11', 'huawei_snapshot_wwn': 'fake-wwn'}, + json.loads(snapshots[0]['provider_location'])) self.assertEqual(fields.GroupSnapshotStatus.AVAILABLE, model['status']) - @cg_or_cg_snapshot def test_create_group_snapshot_with_create_snapshot_fail(self): test_snapshots = [self.snapshot] ctxt = context.get_admin_context() - self.mock_object(rest_client.RestClient, 'create_snapshot', - side_effect=( - exception.VolumeBackendAPIException(data='err'))) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + self.mock_object( + rest_client.RestClient, 'create_snapshot', + side_effect=exception.VolumeBackendAPIException(data='err')) self.assertRaises(exception.VolumeBackendAPIException, self.driver.create_group_snapshot, - ctxt, - self.group_snapshot, - test_snapshots) + ctxt, self.group_snapshot, test_snapshots) - @cg_or_cg_snapshot def test_create_group_snapshot_with_active_snapshot_fail(self): test_snapshots = [self.snapshot] ctxt = context.get_admin_context() - self.mock_object(rest_client.RestClient, 'activate_snapshot', - side_effect=( - exception.VolumeBackendAPIException(data='err'))) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + self.mock_object( + rest_client.RestClient, 'activate_snapshot', + side_effect=exception.VolumeBackendAPIException(data='err')) self.assertRaises(exception.VolumeBackendAPIException, self.driver.create_group_snapshot, - ctxt, - self.group_snapshot, - test_snapshots) + ctxt, self.group_snapshot, test_snapshots) - @cg_or_cg_snapshot def test_delete_group_snapshot(self): test_snapshots = [self.snapshot] ctxt = context.get_admin_context() - self.driver.delete_group_snapshot(ctxt, self.group_snapshot, - test_snapshots) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + self.driver.delete_group_snapshot( + ctxt, self.group_snapshot, test_snapshots) class FCSanLookupService(object): @@ -5386,82 +5381,79 @@ class HuaweiFCDriverTestCase(HuaweiTestBase): res = self.driver.client.is_host_associated_to_hostgroup('1') self.assertFalse(res) - @mock.patch.object(huawei_driver.HuaweiBaseDriver, - '_get_group_type', - return_value=[{"hypermetro": "true"}]) - @cg_or_cg_snapshot - def test_create_hypermetro_group_success(self, mock_grouptype): - """Test that create_group return successfully.""" - ctxt = context.get_admin_context() - # Create group - model_update = self.driver.create_group(ctxt, self.group) + @ddt.data([{"hypermetro": "true"}], []) + def test_create_group_success(self, cg_type): + self.mock_object(huawei_driver.HuaweiBaseDriver, '_get_group_type', + return_value=cg_type) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + model_update = self.driver.create_group(None, self.group) + self.assertEqual(fields.GroupStatus.AVAILABLE, model_update['status']) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status'], - "Group created failed") + @ddt.data( + ([fake_snapshot.fake_snapshot_obj( + None, provider_location=SNAP_PROVIDER_LOCATION, id=ID)], + [], False), + ([], [fake_volume.fake_volume_obj( + None, provider_location=PROVIDER_LOCATION, id=ID)], True), + ) + @ddt.unpack + def test_create_group_from_src(self, snapshots, source_vols, tmp_snap): + self.mock_object(huawei_driver.HuaweiBaseDriver, '_get_group_type', + return_value=[]) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) - @mock.patch.object(huawei_driver.HuaweiBaseDriver, - '_get_group_type', - return_value=[{"hypermetro": "false"}]) - @cg_or_cg_snapshot - def test_create_normal_group_success(self, mock_grouptype): - """Test that create_group return successfully.""" - ctxt = context.get_admin_context() - # Create group - model_update = self.driver.create_group(ctxt, self.group) + create_snap_mock = self.mock_object( + self.driver, '_create_group_snapshot', + wraps=self.driver._create_group_snapshot) + delete_snap_mock = self.mock_object( + self.driver, '_delete_group_snapshot', + wraps=self.driver._delete_group_snapshot) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status'], - "Group created failed") + model_update, volumes_model_update = self.driver.create_group_from_src( + None, self.group, [self.volume], snapshots=snapshots, + source_vols=source_vols) - @mock.patch.object(huawei_driver.HuaweiBaseDriver, - '_get_group_type', - return_value=[{"hypermetro": "true"}]) - @cg_or_cg_snapshot - def test_delete_hypermetro_group_success(self, mock_grouptype): - """Test that delete_group return successfully.""" + if tmp_snap: + create_snap_mock.assert_called_once() + delete_snap_mock.assert_called_once() + else: + create_snap_mock.assert_not_called() + delete_snap_mock.assert_not_called() + + self.assertDictEqual({'status': fields.GroupStatus.AVAILABLE}, + model_update) + self.assertEqual(1, len(volumes_model_update)) + self.assertEqual(ID, volumes_model_update[0]['id']) + + @ddt.data([{"hypermetro": "true"}], []) + def test_delete_group_success(self, cg_type): test_volumes = [self.volume] ctxt = context.get_admin_context() - # Delete group - model, volumes = self.driver.delete_group(ctxt, self.group, - test_volumes) - self.assertEqual(fields.GroupStatus.DELETED, - model['status'], - "Group deleted failed") + self.mock_object(huawei_driver.HuaweiBaseDriver, '_get_group_type', + return_value=cg_type) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + model, volumes = self.driver.delete_group( + ctxt, self.group, test_volumes) + self.assertEqual(fields.GroupStatus.DELETED, model['status']) - @mock.patch.object(huawei_driver.HuaweiBaseDriver, - '_get_group_type', - return_value=[{"hypermetro": "false"}]) - @cg_or_cg_snapshot - def test_delete_normal_group_success(self, mock_grouptype): - """Test that delete_group return successfully.""" - ctxt = context.get_admin_context() - test_volumes = [self.volume] - # Delete group - model, volumes = self.driver.delete_group(ctxt, self.group, - test_volumes) - self.assertEqual(fields.GroupStatus.DELETED, - model['status'], - "Group deleted failed") - - @mock.patch.object(huawei_driver.HuaweiBaseDriver, - '_get_group_type', + @mock.patch.object(huawei_driver.HuaweiBaseDriver, '_get_group_type', return_value=[{"hypermetro": "true"}]) @mock.patch.object(huawei_driver.huawei_utils, 'get_lun_metadata', return_value={'hypermetro_id': '3400a30d844d0007', 'remote_lun_id': '59'}) - @cg_or_cg_snapshot def test_update_group_success(self, mock_grouptype, mock_metadata): - """Test that update_group return successfully.""" ctxt = context.get_admin_context() add_volumes = [self.volume] remove_volumes = [self.volume] - # Update group - model_update = self.driver.update_group(ctxt, self.group, - add_volumes, remove_volumes) + self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type', + return_value=True) + model_update = self.driver.update_group( + ctxt, self.group, add_volumes, remove_volumes) self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update[0]['status'], - "Group update failed") + model_update[0]['status']) def test_is_initiator_associated_to_host_raise(self): self.assertRaises(exception.VolumeBackendAPIException, diff --git a/cinder/volume/drivers/huawei/huawei_driver.py b/cinder/volume/drivers/huawei/huawei_driver.py index 1638fe460bf..df02022ac78 100644 --- a/cinder/volume/drivers/huawei/huawei_driver.py +++ b/cinder/volume/drivers/huawei/huawei_driver.py @@ -31,6 +31,7 @@ from cinder import coordination from cinder import exception from cinder.i18n import _ from cinder import interface +from cinder import objects from cinder.objects import fields from cinder.volume import configuration from cinder.volume import driver @@ -194,6 +195,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): pool['thick_provisioning_support'] = True pool['thin_provisioning_support'] = True pool['smarttier'] = True + pool['consistencygroup_support'] = True pool['consistent_group_snapshot_enabled'] = True if self.configuration.san_product == "Dorado": @@ -226,28 +228,6 @@ class HuaweiBaseDriver(driver.VolumeDriver): opts = self._get_volume_params_from_specs(specs) return opts - def _get_group_type(self, group): - opts = [] - vol_types = group.volume_types - - for vol_type in vol_types: - specs = vol_type.extra_specs - opts.append(self._get_volume_params_from_specs(specs)) - - return opts - - def _check_volume_type_support(self, opts, vol_type): - if not opts: - return False - - support = True - for opt in opts: - if opt.get(vol_type) != 'true': - support = False - break - - return support - def _get_volume_params_from_specs(self, specs): """Return the volume parameters from extra specs.""" opts_capability = { @@ -687,7 +667,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.configuration.lun_policy) lun_params = { - 'NAME': dst_volume_name, + 'NAME': huawei_utils.encode_name(dst_volume_name), 'PARENTID': pool_info['ID'], 'DESCRIPTION': lun_info['DESCRIPTION'], 'ALLOCTYPE': opts.get('LUNType', lun_info['ALLOCTYPE']), @@ -887,7 +867,7 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.client.extend_lun(lun_id, new_size) - def create_snapshot(self, snapshot): + def _create_snapshot_base(self, snapshot): volume = snapshot.volume if not volume: msg = _("Can't get volume id from snapshot, snapshot: %(id)s" @@ -902,11 +882,23 @@ class HuaweiBaseDriver(driver.VolumeDriver): snapshot_name, snapshot_description) snapshot_id = snapshot_info['ID'] - self.client.activate_snapshot(snapshot_id) + return snapshot_id - location = huawei_utils.to_string(huawei_snapshot_id=snapshot_id) - return {'provider_location': location, - 'lun_info': snapshot_info} + def create_snapshot(self, snapshot): + snapshot_id = self._create_snapshot_base(snapshot) + try: + self.client.activate_snapshot(snapshot_id) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Active snapshot %s failed, now deleting it.", + snapshot_id) + self.client.delete_snapshot(snapshot_id) + + snapshot_info = self.client.get_snapshot_info(snapshot_id) + location = huawei_utils.to_string( + huawei_snapshot_id=snapshot_id, + huawei_snapshot_wwn=snapshot_info['WWN']) + return {'provider_location': location} def delete_snapshot(self, snapshot): LOG.info('Delete snapshot %s.', snapshot.id) @@ -1592,52 +1584,135 @@ class HuaweiBaseDriver(driver.VolumeDriver): self.client.is_host_associated_to_hostgroup(host_id)): self.client.remove_host(host_id) - @huawei_utils.check_whether_operate_consistency_group + def _get_group_type(self, group): + opts = [] + for vol_type in group.volume_types: + specs = vol_type.extra_specs + opts.append(self._get_volume_params_from_specs(specs)) + + return opts + + def _check_group_type_support(self, opts, vol_type): + if not opts: + return False + + for opt in opts: + if opt.get(vol_type) == 'true': + return True + + return False + + def _get_group_type_value(self, opts, vol_type): + if not opts: + return + + for opt in opts: + if vol_type in opt: + return opt[vol_type] + def create_group(self, context, group): """Creates a group.""" + if not volume_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + model_update = {'status': fields.GroupStatus.AVAILABLE} opts = self._get_group_type(group) - if self._check_volume_type_support(opts, 'hypermetro'): + + if self._check_group_type_support(opts, 'hypermetro'): + if not self.check_func_support("HyperMetro_ConsistentGroup"): + msg = _("Can't create consistency group, array does not " + "support hypermetro consistentgroup, " + "group id: %(group_id)s." + ) % {"group_id": group.id} + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + metro = hypermetro.HuaweiHyperMetro(self.client, self.rmt_client, self.configuration) metro.create_consistencygroup(group) return model_update - # Array will create group at create_group_snapshot time. Cinder will - # maintain the group and volumes relationship in the db. return model_update - @huawei_utils.check_whether_operate_consistency_group - def delete_group(self, context, group, volumes): - opts = self._get_group_type(group) - if self._check_volume_type_support(opts, 'hypermetro'): - metro = hypermetro.HuaweiHyperMetro(self.client, - self.rmt_client, - self.configuration) - return metro.delete_consistencygroup(context, group, volumes) + def create_group_from_src(self, context, group, volumes, + group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + if not volume_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() - model_update = {} + model_update = self.create_group(context, group) volumes_model_update = [] - model_update.update({'status': fields.GroupStatus.DELETED}) + delete_snapshots = False - for volume_ref in volumes: - try: - self.delete_volume(volume_ref) - volumes_model_update.append( - {'id': volume_ref.id, 'status': 'deleted'}) - except Exception: - volumes_model_update.append( - {'id': volume_ref.id, 'status': 'error_deleting'}) + if not snapshots and source_vols: + snapshots = [] + for src_vol in source_vols: + vol_kwargs = { + 'id': src_vol.id, + 'provider_location': src_vol.provider_location, + } + snapshot_kwargs = {'id': six.text_type(uuid.uuid4()), + 'volume': objects.Volume(**vol_kwargs)} + snapshot = objects.Snapshot(**snapshot_kwargs) + snapshots.append(snapshot) + + snapshots_model_update = self._create_group_snapshot(snapshots) + for i, model in enumerate(snapshots_model_update): + snapshot = snapshots[i] + snapshot.provider_location = model['provider_location'] + + delete_snapshots = True + + if snapshots: + for i, vol in enumerate(volumes): + snapshot = snapshots[i] + vol_model_update = self.create_volume_from_snapshot( + vol, snapshot) + vol_model_update.update({'id': vol.id}) + volumes_model_update.append(vol_model_update) + + if delete_snapshots: + self._delete_group_snapshot(snapshots) + + return model_update, volumes_model_update + + def delete_group(self, context, group, volumes): + if not volume_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + + opts = self._get_group_type(group) + model_update = {'status': fields.GroupStatus.DELETED} + volumes_model_update = [] + + if self._check_group_type_support(opts, 'hypermetro'): + metro = hypermetro.HuaweiHyperMetro(self.client, + self.rmt_client, + self.configuration) + metro.delete_consistencygroup(context, group, volumes) + + for volume in volumes: + volume_model_update = {'id': volume.id} + try: + self.delete_volume(volume) + except Exception: + LOG.exception('Delete volume %s failed.', volume) + volume_model_update.update({'status': 'error_deleting'}) + else: + volume_model_update.update({'status': 'deleted'}) + + volumes_model_update.append(volume_model_update) return model_update, volumes_model_update - @huawei_utils.check_whether_operate_consistency_group def update_group(self, context, group, add_volumes=None, remove_volumes=None): + if not volume_utils.is_group_a_cg_snapshot_type(group): + raise NotImplementedError() + model_update = {'status': fields.GroupStatus.AVAILABLE} opts = self._get_group_type(group) - if self._check_volume_type_support(opts, 'hypermetro'): + if self._check_group_type_support(opts, 'hypermetro'): metro = hypermetro.HuaweiHyperMetro(self.client, self.rmt_client, self.configuration) @@ -1646,93 +1721,91 @@ class HuaweiBaseDriver(driver.VolumeDriver): remove_volumes) return model_update, None, None - # Array will create group at create_group_snapshot time. Cinder will - # maintain the group and volumes relationship in the db. + for volume in add_volumes: + self._check_volume_exist_on_array( + volume, constants.VOLUME_NOT_EXISTS_RAISE) + return model_update, None, None - @huawei_utils.check_whether_operate_consistency_group - def create_group_from_src(self, context, group, volumes, - group_snapshot=None, snapshots=None, - source_group=None, source_vols=None): - err_msg = _("Huawei Storage doesn't support create_group_from_src.") - LOG.error(err_msg) - raise exception.VolumeBackendAPIException(data=err_msg) - - @huawei_utils.check_whether_operate_consistency_group def create_group_snapshot(self, context, group_snapshot, snapshots): """Create group snapshot.""" - LOG.info('Create group snapshot for group' - ': %(group_id)s', {'group_id': group_snapshot.group_id}) + if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() - model_update = {} + LOG.info('Create group snapshot for group: %(group_id)s', + {'group_id': group_snapshot.group_id}) + + snapshots_model_update = self._create_group_snapshot(snapshots) + model_update = {'status': fields.GroupSnapshotStatus.AVAILABLE} + return model_update, snapshots_model_update + + def _create_group_snapshot(self, snapshots): snapshots_model_update = [] added_snapshots_info = [] try: for snapshot in snapshots: - volume = snapshot.volume - if not volume: - msg = _("Can't get volume id from snapshot, " - "snapshot: %(id)s") % {'id': snapshot.id} - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - lun_id, lun_wwn = huawei_utils.get_volume_lun_id( - self.client, volume) - snapshot_name = huawei_utils.encode_name(snapshot.id) - snapshot_description = snapshot.id - info = self.client.create_snapshot(lun_id, - snapshot_name, - snapshot_description) + snapshot_id = self._create_snapshot_base(snapshot) + info = self.client.get_snapshot_info(snapshot_id) location = huawei_utils.to_string( - huawei_snapshot_id=info['ID']) - snap_model_update = {'id': snapshot.id, - 'status': fields.SnapshotStatus.AVAILABLE, - 'provider_location': location} - snapshots_model_update.append(snap_model_update) + huawei_snapshot_id=info['ID'], + huawei_snapshot_wwn=info['WWN']) + snapshot_model_update = { + 'id': snapshot.id, + 'status': fields.SnapshotStatus.AVAILABLE, + 'provider_location': location, + } + snapshots_model_update.append(snapshot_model_update) added_snapshots_info.append(info) except Exception: with excutils.save_and_reraise_exception(): - LOG.error("Create group snapshots failed. " - "Group snapshot id: %s.", group_snapshot.id) + for added_snapshot in added_snapshots_info: + self.client.delete_snapshot(added_snapshot['ID']) + snapshot_ids = [added_snapshot['ID'] for added_snapshot in added_snapshots_info] try: self.client.activate_snapshot(snapshot_ids) except Exception: with excutils.save_and_reraise_exception(): - LOG.error("Active group snapshots failed. " - "Group snapshot id: %s.", group_snapshot.id) + LOG.error("Active group snapshots %s failed.", snapshot_ids) + for snapshot_id in snapshot_ids: + self.client.delete_snapshot(snapshot_id) - model_update['status'] = fields.GroupSnapshotStatus.AVAILABLE + return snapshots_model_update - return model_update, snapshots_model_update - - @huawei_utils.check_whether_operate_consistency_group def delete_group_snapshot(self, context, group_snapshot, snapshots): """Delete group snapshot.""" + if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot): + raise NotImplementedError() + LOG.info('Delete group snapshot %(snap_id)s for group: ' '%(group_id)s', {'snap_id': group_snapshot.id, 'group_id': group_snapshot.group_id}) - model_update = {} - snapshots_model_update = [] - model_update['status'] = fields.GroupSnapshotStatus.DELETED - - for snapshot in snapshots: - try: - self.delete_snapshot(snapshot) - snapshot_model = {'id': snapshot.id, - 'status': fields.SnapshotStatus.DELETED} - snapshots_model_update.append(snapshot_model) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.error("Delete group snapshot failed. " - "Group snapshot id: %s", group_snapshot.id) + try: + snapshots_model_update = self._delete_group_snapshot(snapshots) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error("Delete group snapshots failed. " + "Group snapshot id: %s", group_snapshot.id) + model_update = {'status': fields.GroupSnapshotStatus.DELETED} return model_update, snapshots_model_update + def _delete_group_snapshot(self, snapshots): + snapshots_model_update = [] + for snapshot in snapshots: + self.delete_snapshot(snapshot) + snapshot_model_update = { + 'id': snapshot.id, + 'status': fields.SnapshotStatus.DELETED + } + snapshots_model_update.append(snapshot_model_update) + + return snapshots_model_update + def _classify_volume(self, volumes): normal_volumes = [] replica_volumes = []