From 78e0bf023b533698663c46be54d938233a368c2c Mon Sep 17 00:00:00 2001 From: hongp Date: Wed, 14 Jan 2026 13:47:46 +0900 Subject: [PATCH] Add --cluster option to volume migration This patch adds the '--cluster' optional argument to the 'volume migration' command. This allows users to migrate volumes to a destination cluster instead of a specific host, which is particularly useful in Active-Active configurations. The '--cluster' option requires Cinder API microversion 3.16 or higher. The '--host' and '--cluster' options are mutually exclusive; one of them must be provided for the migration to start. Change-Id: Ibd45ac35e39a2a9f88a39f8041c06c4469727098 Signed-off-by: hongp --- .../tests/unit/volume/v3/test_volume.py | 63 ++++++++++++++++++- openstackclient/volume/v3/volume.py | 24 ++++++- ...-to-volume-migration-9fe0cc84e9c80a4c.yaml | 7 +++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 33dcfe5a47..a004dc122d 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -1702,9 +1702,10 @@ class TestVolumeMigrate(volume_fakes.TestVolume): host="host@backend-name#pool", force_host_copy=False, lock_volume=False, + cluster=None, ) - def test_volume_migrate_with_option(self): + def test_volume_migrate_with_host(self): arglist = [ "--force-host-copy", "--lock-volume", @@ -1731,9 +1732,66 @@ class TestVolumeMigrate(volume_fakes.TestVolume): host="host@backend-name#pool", force_host_copy=True, lock_volume=True, + cluster=None, ) - def test_volume_migrate_without_host(self): + def test_volume_migrate_with_cluster(self): + self.set_volume_api_version('3.16') + arglist = [ + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.volume_sdk_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_sdk_client.migrate_volume.assert_called_once_with( + self.volume.id, + host=None, + force_host_copy=False, + lock_volume=False, + cluster="cluster@backend-name#pool", + ) + + def test_volume_migrate_with_cluster_pre_v316(self): + self.set_volume_api_version('3.15') + arglist = [ + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.volume_sdk_client.migrate_volume.assert_not_called() + + def test_volume_migrate_without_host_and_cluster(self): arglist = [ self.volume.id, ] @@ -1750,7 +1808,6 @@ class TestVolumeMigrate(volume_fakes.TestVolume): arglist, verifylist, ) - self.volume_sdk_client.find_volume.assert_not_called() self.volume_sdk_client.migrate_volume.assert_not_called() diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 50ea77fb5a..d1c1e43b7f 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -736,14 +736,22 @@ class MigrateVolume(command.Command): metavar="", help=_("Volume to migrate (name or ID)"), ) - parser.add_argument( + destination_group = parser.add_mutually_exclusive_group(required=True) + destination_group.add_argument( '--host', metavar="", - required=True, help=_( "Destination host (takes the form: host@backend-name#pool)" ), ) + destination_group.add_argument( + '--cluster', + metavar="", + help=_( + "Destination cluster to migrate the volume to " + "(requires --os-volume-api-version 3.16 or higher)" + ), + ) parser.add_argument( '--force-host-copy', action="store_true", @@ -761,7 +769,6 @@ class MigrateVolume(command.Command): "(possibly by another operation)" ), ) - # TODO(stephenfin): Add --cluster argument return parser def take_action(self, parsed_args): @@ -769,11 +776,22 @@ class MigrateVolume(command.Command): volume = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) + + if parsed_args.cluster and not sdk_utils.supports_microversion( + volume_client, '3.16' + ): + msg = _( + "--os-volume-api-version 3.16 or greater is required to " + "support the volume migration with cluster" + ) + raise exceptions.CommandError(msg) + volume_client.migrate_volume( volume.id, host=parsed_args.host, force_host_copy=parsed_args.force_host_copy, lock_volume=parsed_args.lock_volume, + cluster=parsed_args.cluster, ) diff --git a/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml new file mode 100644 index 0000000000..5b9e878056 --- /dev/null +++ b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``volume migration`` command now supports the ``--cluster`` + optional argument, allowing volumes to be migrated to a destination + cluster. This feature requires Cinder API microversion 3.16 or + higher and is mutually exclusive with the ``--host`` option. \ No newline at end of file