Hitachi: Update retype and support storage assisted migration

This patch update retype operation to different pool and support storage
assisted migration.
Storage assisted migration feature is also used when retype a volume,
which doesn't have any snapshots, to different pool.

Implements: blueprint hitachi-vsp-update-retype
Change-Id: I1f992f7986652098656662bf129b1dd8427ac694
This commit is contained in:
Atsushi Kawai 2023-02-16 05:36:08 +00:00
parent b651965b24
commit d148f41664
12 changed files with 345 additions and 52 deletions

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -204,6 +204,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "HDP"], "attributes": ["CVS", "HDP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -1043,14 +1044,69 @@ class HBSDRESTFCDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'hbsd:test': 'test'} new_specs = {'hbsd:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
@mock.patch.object(requests.Session, "request")
def test_migrate_volume(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
host = {
'capabilities': {
'location_info': {
'storage_id': CONFIG_MAP['serial'],
'pool_id': 30,
},
},
}
ret = self.driver.migrate_volume(self.ctxt, TEST_VOLUME[0], host)
self.assertEqual(2, request.call_count)
actual = (True, None)
self.assertTupleEqual(actual, ret)
@mock.patch.object(requests.Session, "request")
def test_migrate_volume_diff_pool(self, request):
request.side_effect = [FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(202, COMPLETED_SUCCEEDED_RESULT),
FakeResponse(202, COMPLETED_SUCCEEDED_RESULT),
FakeResponse(200, GET_SNAPSHOTS_RESULT),
FakeResponse(200, NOTFOUND_RESULT),
FakeResponse(200, NOTFOUND_RESULT),
FakeResponse(200, NOTFOUND_RESULT),
FakeResponse(200, NOTFOUND_RESULT),
FakeResponse(202, COMPLETED_SUCCEEDED_RESULT),
FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(200, GET_LDEV_RESULT),
FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)]
host = {
'capabilities': {
'location_info': {
'storage_id': CONFIG_MAP['serial'],
'pool_id': 40,
},
},
}
ret = self.driver.migrate_volume(self.ctxt, TEST_VOLUME[0], host)
self.assertEqual(15, request.call_count)
actual = (True, {'provider_location': '1'})
self.assertTupleEqual(actual, ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -190,6 +190,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "HDP"], "attributes": ["CVS", "HDP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -783,14 +784,39 @@ class HBSDRESTISCSIDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'hbsd:test': 'test'} new_specs = {'hbsd:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
@mock.patch.object(requests.Session, "request")
def test_migrate_volume(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
host = {
'capabilities': {
'location_info': {
'storage_id': CONFIG_MAP['serial'],
'pool_id': 30,
},
},
}
ret = self.driver.migrate_volume(self.ctxt, TEST_VOLUME[0], host)
self.assertEqual(2, request.call_count)
actual = (True, None)
self.assertTupleEqual(actual, ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2022, Hewlett Packard Enterprise, Ltd. # Copyright (C) 2022, 2023, Hewlett Packard Enterprise, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -186,6 +186,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "THP"], "attributes": ["CVS", "THP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -959,14 +960,23 @@ class HPEXPRESTFCDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
new_specs = {'hpe_xp:test': 'test'} def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'hbsd:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2022, Hewlett Packard Enterprise, Ltd. # Copyright (C) 2022, 2023. Hewlett Packard Enterprise, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -189,6 +189,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "THP"], "attributes": ["CVS", "THP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -772,14 +773,23 @@ class HPEXPRESTISCSIDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
new_specs = {'hpe_xp:test': 'test'} def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'hbsd:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2021 NEC corporation # Copyright (C) 2021, 2023, NEC corporation
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -186,6 +186,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "DP"], "attributes": ["CVS", "DP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -953,14 +954,23 @@ class VStorageRESTFCDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'nec:test': 'test'} new_specs = {'nec:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2021 NEC corporation # Copyright (C) 2021, 2023, NEC corporation
# #
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -190,6 +190,7 @@ GET_LDEV_RESULT = {
"blockCapacity": 2097152, "blockCapacity": 2097152,
"attributes": ["CVS", "DP"], "attributes": ["CVS", "DP"],
"status": "NML", "status": "NML",
"poolId": 30,
} }
GET_LDEV_RESULT_MAPPED = { GET_LDEV_RESULT_MAPPED = {
@ -811,14 +812,23 @@ class VStorageRESTISCSIDriverTest(test.TestCase):
self.driver.unmanage_snapshot, self.driver.unmanage_snapshot,
TEST_SNAPSHOT[0]) TEST_SNAPSHOT[0])
def test_retype(self): @mock.patch.object(requests.Session, "request")
def test_retype(self, request):
request.return_value = FakeResponse(200, GET_LDEV_RESULT)
new_specs = {'nec:test': 'test'} new_specs = {'nec:test': 'test'}
new_type_ref = volume_types.create(self.ctxt, 'new', new_specs) new_type_ref = volume_types.create(self.ctxt, 'new', new_specs)
diff = {} diff = {}
host = {} host = {
'capabilities': {
'location_info': {
'pool_id': 30,
},
},
}
ret = self.driver.retype( ret = self.driver.retype(
self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host) self.ctxt, TEST_VOLUME[0], new_type_ref, diff, host)
self.assertFalse(ret) self.assertEqual(1, request.call_count)
self.assertTrue(ret)
def test_backup_use_temp_snapshot(self): def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot()) self.assertTrue(self.driver.backup_use_temp_snapshot())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -175,7 +175,7 @@ class HBSDCommon():
return pool['location_info']['pool_id'] return pool['location_info']['pool_id']
return None return None
def create_ldev(self, size, pool_id): def create_ldev(self, size, pool_id, ldev_range):
"""Create an LDEV and return its LDEV number.""" """Create an LDEV and return its LDEV number."""
raise NotImplementedError() raise NotImplementedError()
@ -186,8 +186,9 @@ class HBSDCommon():
def create_volume(self, volume): def create_volume(self, volume):
"""Create a volume and return its properties.""" """Create a volume and return its properties."""
pool_id = self.get_pool_id_of_volume(volume) pool_id = self.get_pool_id_of_volume(volume)
ldev_range = self.storage_info['ldev_range']
try: try:
ldev = self.create_ldev(volume['size'], pool_id) ldev = self.create_ldev(volume['size'], pool_id, ldev_range)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
utils.output_log(MSG.CREATE_LDEV_FAILED) utils.output_log(MSG.CREATE_LDEV_FAILED)
@ -200,20 +201,29 @@ class HBSDCommon():
"""Return a dictionary of LDEV-related items.""" """Return a dictionary of LDEV-related items."""
raise NotImplementedError() raise NotImplementedError()
def create_pair_on_storage(self, pvol, svol, is_snapshot=False): def create_pair_on_storage(
self, pvol, svol, snap_pool_id, is_snapshot=False):
"""Create a copy pair on the storage.""" """Create a copy pair on the storage."""
raise NotImplementedError() raise NotImplementedError()
def _copy_on_storage( def wait_copy_completion(self, pvol, svol):
self, pvol, size, pool_id, is_snapshot=False): """Wait until copy is completed."""
raise NotImplementedError()
def copy_on_storage(
self, pvol, size, pool_id, snap_pool_id, ldev_range,
is_snapshot=False, sync=False):
"""Create a copy of the specified LDEV on the storage.""" """Create a copy of the specified LDEV on the storage."""
ldev_info = self.get_ldev_info(['status', 'attributes'], pvol) ldev_info = self.get_ldev_info(['status', 'attributes'], pvol)
if ldev_info['status'] != 'NML': if ldev_info['status'] != 'NML':
msg = utils.output_log(MSG.INVALID_LDEV_STATUS_FOR_COPY, ldev=pvol) msg = utils.output_log(MSG.INVALID_LDEV_STATUS_FOR_COPY, ldev=pvol)
self.raise_error(msg) self.raise_error(msg)
svol = self.create_ldev(size, pool_id) svol = self.create_ldev(size, pool_id, ldev_range)
try: try:
self.create_pair_on_storage(pvol, svol, is_snapshot=is_snapshot) self.create_pair_on_storage(
pvol, svol, snap_pool_id, is_snapshot=is_snapshot)
if sync:
self.wait_copy_completion(pvol, svol)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
try: try:
@ -234,7 +244,10 @@ class HBSDCommon():
size = volume['size'] size = volume['size']
pool_id = self.get_pool_id_of_volume(volume) pool_id = self.get_pool_id_of_volume(volume)
new_ldev = self._copy_on_storage(ldev, size, pool_id) snap_pool_id = self.storage_info['snap_pool_id']
ldev_range = self.storage_info['ldev_range']
new_ldev = self.copy_on_storage(
ldev, size, pool_id, snap_pool_id, ldev_range)
self.modify_ldev_name(new_ldev, volume['id'].replace("-", "")) self.modify_ldev_name(new_ldev, volume['id'].replace("-", ""))
return { return {
@ -323,8 +336,10 @@ class HBSDCommon():
self.raise_error(msg) self.raise_error(msg)
size = snapshot['volume_size'] size = snapshot['volume_size']
pool_id = self.get_pool_id_of_volume(snapshot['volume']) pool_id = self.get_pool_id_of_volume(snapshot['volume'])
new_ldev = self._copy_on_storage( snap_pool_id = self.storage_info['snap_pool_id']
ldev, size, pool_id, is_snapshot=True) ldev_range = self.storage_info['ldev_range']
new_ldev = self.copy_on_storage(
ldev, size, pool_id, snap_pool_id, ldev_range, is_snapshot=True)
return { return {
'provider_location': str(new_ldev), 'provider_location': str(new_ldev),
} }
@ -947,7 +962,12 @@ class HBSDCommon():
MSG.SNAPSHOT_UNMANAGE_FAILED, snapshot_id=snapshot['id']) MSG.SNAPSHOT_UNMANAGE_FAILED, snapshot_id=snapshot['id'])
raise NotImplementedError() raise NotImplementedError()
def retype(self): def migrate_volume(self, volume, host):
"""Migrate the specified volume."""
return False
def retype(self, ctxt, volume, new_type, diff, host):
"""Retype the specified volume."""
return False return False
def has_snap_pair(self, pvol, svol): def has_snap_pair(self, pvol, svol):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -72,6 +72,7 @@ class HBSDFCDriver(driver.FibreChannelDriver):
2.2.2 - Add Target Port Assignment. 2.2.2 - Add Target Port Assignment.
2.2.3 - Add port scheduler. 2.2.3 - Add port scheduler.
2.3.0 - Support multi pool. 2.3.0 - Support multi pool.
2.3.1 - Update retype and support storage assisted migration.
""" """
@ -246,7 +247,12 @@ class HBSDFCDriver(driver.FibreChannelDriver):
@volume_utils.trace @volume_utils.trace
def retype(self, ctxt, volume, new_type, diff, host): def retype(self, ctxt, volume, new_type, diff, host):
"""Retype the specified volume.""" """Retype the specified volume."""
return self.common.retype() return self.common.retype(ctxt, volume, new_type, diff, host)
@volume_utils.trace
def migrate_volume(self, ctxt, volume, host):
"""Migrate the specified volume."""
return self.common.migrate_volume(volume, host)
def backup_use_temp_snapshot(self): def backup_use_temp_snapshot(self):
return True return True

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -72,6 +72,7 @@ class HBSDISCSIDriver(driver.ISCSIDriver):
2.2.2 - Add Target Port Assignment. 2.2.2 - Add Target Port Assignment.
2.2.3 - Add port scheduler. 2.2.3 - Add port scheduler.
2.3.0 - Support multi pool. 2.3.0 - Support multi pool.
2.3.1 - Update retype and support storage assisted migration.
""" """
@ -242,7 +243,12 @@ class HBSDISCSIDriver(driver.ISCSIDriver):
@volume_utils.trace @volume_utils.trace
def retype(self, ctxt, volume, new_type, diff, host): def retype(self, ctxt, volume, new_type, diff, host):
"""Retype the specified volume.""" """Retype the specified volume."""
return self.common.retype() return self.common.retype(ctxt, volume, new_type, diff, host)
@volume_utils.trace
def migrate_volume(self, ctxt, volume, host):
"""Migrate the specified volume."""
return self.common.migrate_volume(volume, host)
def backup_use_temp_snapshot(self): def backup_use_temp_snapshot(self):
return True return True

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -26,6 +26,7 @@ from oslo_utils import units
from cinder import exception from cinder import exception
from cinder.objects import fields from cinder.objects import fields
from cinder.objects import SnapshotList
from cinder.volume import configuration from cinder.volume import configuration
from cinder.volume.drivers.hitachi import hbsd_common as common from cinder.volume.drivers.hitachi import hbsd_common as common
from cinder.volume.drivers.hitachi import hbsd_rest_api as rest_api from cinder.volume.drivers.hitachi import hbsd_rest_api as rest_api
@ -274,7 +275,7 @@ class HBSDREST(common.HBSDCommon):
if self.client is not None: if self.client is not None:
self.client.enter_keep_session() self.client.enter_keep_session()
def _create_ldev_on_storage(self, size, pool_id): def _create_ldev_on_storage(self, size, pool_id, ldev_range):
"""Create an LDEV on the storage system.""" """Create an LDEV on the storage system."""
body = { body = {
'byteFormatCapacity': '%sG' % size, 'byteFormatCapacity': '%sG' % size,
@ -287,9 +288,9 @@ class HBSDREST(common.HBSDCommon):
body['endLdevId'] = max_ldev body['endLdevId'] = max_ldev
return self.client.add_ldev(body, no_log=True) return self.client.add_ldev(body, no_log=True)
def create_ldev(self, size, pool_id): def create_ldev(self, size, pool_id, ldev_range):
"""Create an LDEV of the specified size and the specified type.""" """Create an LDEV of the specified size and the specified type."""
ldev = self._create_ldev_on_storage(size, pool_id=pool_id) ldev = self._create_ldev_on_storage(size, pool_id, ldev_range)
LOG.debug('Created logical device. (LDEV: %s)', ldev) LOG.debug('Created logical device. (LDEV: %s)', ldev)
return ldev return ldev
@ -386,7 +387,7 @@ class HBSDREST(common.HBSDCommon):
utils.output_log( utils.output_log(
MSG.DELETE_PAIR_FAILED, pvol=pvol, svol=svol) MSG.DELETE_PAIR_FAILED, pvol=pvol, svol=svol)
def _create_clone_pair(self, pvol, svol): def _create_clone_pair(self, pvol, svol, snap_pool_id):
"""Create a clone copy pair on the storage.""" """Create a clone copy pair on the storage."""
snapshot_name = '%(prefix)s%(svol)s' % { snapshot_name = '%(prefix)s%(svol)s' % {
'prefix': self.driver_info['driver_prefix'] + '-clone', 'prefix': self.driver_info['driver_prefix'] + '-clone',
@ -428,12 +429,13 @@ class HBSDREST(common.HBSDCommon):
utils.output_log( utils.output_log(
MSG.DELETE_PAIR_FAILED, pvol=pvol, svol=svol) MSG.DELETE_PAIR_FAILED, pvol=pvol, svol=svol)
def create_pair_on_storage(self, pvol, svol, is_snapshot=False): def create_pair_on_storage(
self, pvol, svol, snap_pool_id, is_snapshot=False):
"""Create a copy pair on the storage.""" """Create a copy pair on the storage."""
if is_snapshot: if is_snapshot:
self._create_snap_pair(pvol, svol) self._create_snap_pair(pvol, svol)
else: else:
self._create_clone_pair(pvol, svol) self._create_clone_pair(pvol, svol, snap_pool_id)
def get_ldev_info(self, keys, ldev, **kwargs): def get_ldev_info(self, keys, ldev, **kwargs):
"""Return a dictionary of LDEV-related items.""" """Return a dictionary of LDEV-related items."""
@ -1136,7 +1138,8 @@ class HBSDREST(common.HBSDCommon):
self.raise_error(msg) self.raise_error(msg)
size = snapshot.volume_size size = snapshot.volume_size
pool_id = self.get_pool_id_of_volume(snapshot.volume) pool_id = self.get_pool_id_of_volume(snapshot.volume)
pair['svol'] = self.create_ldev(size, pool_id) ldev_range = self.storage_info['ldev_range']
pair['svol'] = self.create_ldev(size, pool_id, ldev_range)
except Exception as exc: except Exception as exc:
pair['msg'] = utils.get_exception_msg(exc) pair['msg'] = utils.get_exception_msg(exc)
raise loopingcall.LoopingCallDone(pair) raise loopingcall.LoopingCallDone(pair)
@ -1191,3 +1194,121 @@ class HBSDREST(common.HBSDCommon):
return self._create_cgsnapshot(context, group_snapshot, snapshots) return self._create_cgsnapshot(context, group_snapshot, snapshots)
else: else:
return self._create_non_cgsnapshot(group_snapshot, snapshots) return self._create_non_cgsnapshot(group_snapshot, snapshots)
def migrate_volume(self, volume, host, new_type=None):
"""Migrate the specified volume."""
attachments = volume.volume_attachment
if attachments:
return False, None
pvol = utils.get_ldev(volume)
if pvol is None:
msg = utils.output_log(
MSG.INVALID_LDEV_FOR_VOLUME_COPY, type='volume', id=volume.id)
self.raise_error(msg)
pair_info = self.get_pair_info(pvol)
if pair_info:
if pair_info['pvol'] == pvol:
svols = []
copy_methods = []
svol_statuses = []
for svol_info in pair_info['svol_info']:
svols.append(str(svol_info['ldev']))
copy_methods.append(utils.THIN)
svol_statuses.append(svol_info['status'])
if svols:
pair_info = ['(%s, %s, %s, %s)' %
(pvol, svol, copy_method, status)
for svol, copy_method, status in
zip(svols, copy_methods, svol_statuses)]
msg = utils.output_log(
MSG.MIGRATE_VOLUME_FAILED,
volume=volume.id, ldev=pvol,
pair_info=', '.join(pair_info))
self.raise_error(msg)
else:
svol_info = pair_info['svol_info'][0]
if svol_info['is_psus'] and svol_info['status'] != 'PSUP':
return False, None
else:
pair_info = '(%s, %s, %s, %s)' % (
pair_info['pvol'], svol_info['ldev'],
utils.THIN, svol_info['status'])
msg = utils.output_log(
MSG.MIGRATE_VOLUME_FAILED,
volume=volume.id, ldev=svol_info['ldev'],
pair_info=pair_info)
self.raise_error(msg)
old_storage_id = self.conf.hitachi_storage_id
new_storage_id = (
host['capabilities']['location_info'].get('storage_id'))
if new_type is None:
old_pool_id = self.get_ldev_info(['poolId'], pvol)['poolId']
new_pool_id = host['capabilities']['location_info'].get('pool_id')
if old_storage_id != new_storage_id:
return False, None
ldev_range = host['capabilities']['location_info'].get('ldev_range')
if (new_type or old_pool_id != new_pool_id or
(ldev_range and
(pvol < ldev_range[0] or ldev_range[1] < pvol))):
snap_pool_id = host['capabilities']['location_info'].get(
'snap_pool_id')
ldev_range = host['capabilities']['location_info'].get(
'ldev_range')
svol = self.copy_on_storage(
pvol, volume.size, new_pool_id, snap_pool_id, ldev_range,
is_snapshot=False, sync=True)
self.modify_ldev_name(svol, volume['id'].replace("-", ""))
try:
self.delete_ldev(pvol)
except exception.VolumeDriverException:
utils.output_log(MSG.DELETE_LDEV_FAILED, ldev=pvol)
return True, {
'provider_location': str(svol),
}
return True, None
def retype(self, ctxt, volume, new_type, diff, host):
"""Retype the specified volume."""
def _check_specs_diff(diff):
for specs_key, specs_val in diff.items():
for diff_key, diff_val in specs_val.items():
if diff_val[0] != diff_val[1]:
return False
return True
ldev = utils.get_ldev(volume)
if ldev is None:
msg = utils.output_log(
MSG.INVALID_LDEV_FOR_VOLUME_COPY, type='volume',
id=volume['id'])
self.raise_error(msg)
ldev_info = self.get_ldev_info(
['poolId'], ldev)
old_pool_id = ldev_info['poolId']
new_pool_id = host['capabilities']['location_info'].get('pool_id')
if not _check_specs_diff(diff) or new_pool_id != old_pool_id:
snaps = SnapshotList.get_all_for_volume(ctxt, volume.id)
if not snaps:
return self.migrate_volume(volume, host, new_type)
return False
return True
def wait_copy_completion(self, pvol, svol):
"""Wait until copy is completed."""
self._wait_copy_pair_status(svol, set([SMPL, PSUE]))
status = self._get_copy_pair_status(svol)
if status == PSUE:
msg = utils.output_log(
MSG.VOLUME_COPY_FAILED, pvol=pvol, svol=svol)
self.raise_error(msg)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2022, Hitachi, Ltd. # Copyright (C) 2020, 2023, Hitachi, Ltd.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -25,7 +25,7 @@ from oslo_utils import units
from cinder import exception from cinder import exception
VERSION = '2.3.0' VERSION = '2.3.1'
CI_WIKI_NAME = 'Hitachi_VSP_CI' CI_WIKI_NAME = 'Hitachi_VSP_CI'
PARAM_PREFIX = 'hitachi' PARAM_PREFIX = 'hitachi'
VENDOR_NAME = 'Hitachi' VENDOR_NAME = 'Hitachi'
@ -43,6 +43,9 @@ GIGABYTE_PER_BLOCK_SIZE = units.Gi / 512
NORMAL_LDEV_TYPE = 'Normal' NORMAL_LDEV_TYPE = 'Normal'
FULL = 'Full copy'
THIN = 'Thin copy'
INFO_SUFFIX = 'I' INFO_SUFFIX = 'I'
WARNING_SUFFIX = 'W' WARNING_SUFFIX = 'W'
ERROR_SUFFIX = 'E' ERROR_SUFFIX = 'E'
@ -479,6 +482,14 @@ class HBSDMsg(enum.Enum):
'resource of host group or wwn was found. (ports: %(ports)s)', 'resource of host group or wwn was found. (ports: %(ports)s)',
'suffix': ERROR_SUFFIX, 'suffix': ERROR_SUFFIX,
} }
MIGRATE_VOLUME_FAILED = {
'msg_id': 760,
'loglevel': base_logging.ERROR,
'msg': 'Failed to migrate a volume. The volume is in a copy pair that '
'cannot be deleted. (volume: %(volume)s, LDEV: %(ldev)s, '
'(P-VOL, S-VOL, copy method, status): %(pair_info)s)',
'suffix': ERROR_SUFFIX,
}
def __init__(self, error_info): def __init__(self, error_info):
"""Initialize Enum attributes.""" """Initialize Enum attributes."""

View File

@ -0,0 +1,7 @@
---
features:
- |
Hitachi driver: Update retype to different pool and support storage
assisted migration.
Storage assisted migration feature is also used when retype a volume,
which doesn't have any snapshots, to different pool.