diff --git a/doc/source/command-objects/volume.rst b/doc/source/command-objects/volume.rst index 8f1233614c..703a5c7692 100644 --- a/doc/source/command-objects/volume.rst +++ b/doc/source/command-objects/volume.rst @@ -197,6 +197,48 @@ List volumes *Volume version 2 only* +volume migrate +-------------- + +Migrate volume to a new host + +.. program:: volume migrate +.. code:: bash + + os volume migrate + --host + [--force-host-copy] + [--lock-volume | --unlock-volume] + + +.. option:: --host + + Destination host (takes the form: host@backend-name#pool) (required) + +.. option:: --force-host-copy + + Enable generic host-based force-migration, + which bypasses driver optimizations + +.. option:: --lock-volume + + If specified, the volume state will be locked and will not allow + a migration to be aborted (possibly by another operation) + + *Volume version 2 only* + +.. option:: --unlock-volume + + If specified, the volume state will not be locked and the a + migration can be aborted (default) (possibly by another operation) + + *Volume version 2 only* + +.. _volume_migrate-volume: +.. describe:: + + Volume to migrate (name or ID) + volume set ---------- diff --git a/doc/source/commands.rst b/doc/source/commands.rst index de473a063d..072767a4c8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -230,8 +230,8 @@ Those actions with an opposite action are noted in parens if applicable. * ``issue`` (``revoke``) - issue a token * ``list`` - display summary information about multiple objects * ``lock`` (``unlock``) - lock one or more servers so that non-admin user won't be able to execute actions -* ``migrate`` - move a server to a different host; ``--live`` performs a - live migration if possible +* ``migrate`` - move a server or a volume to a different host; ``--live`` performs a + live server migration if possible * ``pause`` (``unpause``) - stop one or more servers and leave them in memory * ``reboot`` - forcibly reboot a server * ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create diff --git a/openstackclient/tests/unit/volume/v1/test_volume.py b/openstackclient/tests/unit/volume/v1/test_volume.py index 73c00844e8..8e79ecbba5 100644 --- a/openstackclient/tests/unit/volume/v1/test_volume.py +++ b/openstackclient/tests/unit/volume/v1/test_volume.py @@ -739,6 +739,68 @@ class TestVolumeList(TestVolume): self.cmd, arglist, verifylist) +class TestVolumeMigrate(TestVolume): + + _volume = volume_fakes.FakeVolume.create_one_volume() + + def setUp(self): + super(TestVolumeMigrate, self).setUp() + + self.volumes_mock.get.return_value = self._volume + self.volumes_mock.migrate_volume.return_value = None + # Get the command object to test + self.cmd = volume.MigrateVolume(self.app, None) + + def test_volume_migrate(self): + arglist = [ + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False) + self.assertIsNone(result) + + def test_volume_migrate_with_option(self): + arglist = [ + "--force-host-copy", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", True), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", True) + self.assertIsNone(result) + + def test_volume_migrate_without_host(self): + arglist = [ + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("volume", self._volume.id), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + class TestVolumeSet(TestVolume): _volume = volume_fakes.FakeVolume.create_one_volume() diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index f4a7c14283..0a436e61c2 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -996,6 +996,96 @@ class TestVolumeList(TestVolume): self.cmd, arglist, verifylist) +class TestVolumeMigrate(TestVolume): + + _volume = volume_fakes.FakeVolume.create_one_volume() + + def setUp(self): + super(TestVolumeMigrate, self).setUp() + + self.volumes_mock.get.return_value = self._volume + self.volumes_mock.migrate_volume.return_value = None + # Get the command object to test + self.cmd = volume.MigrateVolume(self.app, None) + + def test_volume_migrate(self): + arglist = [ + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False, False) + self.assertIsNone(result) + + def test_volume_migrate_with_option(self): + arglist = [ + "--force-host-copy", + "--lock-volume", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", True), + ("lock_volume", True), + ("unlock_volume", False), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", True, True) + self.assertIsNone(result) + + def test_volume_migrate_with_unlock_volume(self): + arglist = [ + "--unlock-volume", + "--host", "host@backend-name#pool", + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", True), + ("host", "host@backend-name#pool"), + ("volume", self._volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.volumes_mock.get.assert_called_once_with(self._volume.id) + self.volumes_mock.migrate_volume.assert_called_once_with( + self._volume.id, "host@backend-name#pool", False, False) + self.assertIsNone(result) + + def test_volume_migrate_without_host(self): + arglist = [ + self._volume.id, + ] + verifylist = [ + ("force_host_copy", False), + ("lock_volume", False), + ("unlock_volume", False), + ("volume", self._volume.id), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + class TestVolumeSet(TestVolume): def setUp(self): diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index cafe8ce6ab..63bcfbb8bb 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -344,6 +344,37 @@ class ListVolume(command.Lister): ) for s in data)) +class MigrateVolume(command.Command): + """Migrate volume to a new host""" + + def get_parser(self, prog_name): + parser = super(MigrateVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar="", + help=_("Volume to migrate (name or ID)") + ) + parser.add_argument( + '--host', + metavar="", + required=True, + help=_("Destination host (takes the form: host@backend-name#pool)") + ) + parser.add_argument( + '--force-host-copy', + action="store_true", + help=_("Enable generic host-based force-migration, " + "which bypasses driver optimizations") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + volume_client.volumes.migrate_volume(volume.id, parsed_args.host, + parsed_args.force_host_copy,) + + class SetVolume(command.Command): """Set volume properties""" diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index cb409711b5..0e4071fbf4 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -409,6 +409,53 @@ class ListVolume(command.Lister): ) for s in data)) +class MigrateVolume(command.Command): + """Migrate volume to a new host""" + + def get_parser(self, prog_name): + parser = super(MigrateVolume, self).get_parser(prog_name) + parser.add_argument( + 'volume', + metavar="", + help=_("Volume to migrate (name or ID)") + ) + parser.add_argument( + '--host', + metavar="", + required=True, + help=_("Destination host (takes the form: host@backend-name#pool)") + ) + parser.add_argument( + '--force-host-copy', + action="store_true", + help=_("Enable generic host-based force-migration, " + "which bypasses driver optimizations") + ) + lock_group = parser.add_mutually_exclusive_group() + lock_group.add_argument( + '--lock-volume', + action="store_true", + help=_("If specified, the volume state will be locked " + "and will not allow a migration to be aborted " + "(possibly by another operation)") + ) + lock_group.add_argument( + '--unlock-volume', + action="store_true", + help=_("If specified, the volume state will not be " + "locked and the a migration can be aborted " + "(default) (possibly by another operation)") + ) + return parser + + def take_action(self, parsed_args): + volume_client = self.app.client_manager.volume + volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + volume_client.volumes.migrate_volume(volume.id, parsed_args.host, + parsed_args.force_host_copy, + parsed_args.lock_volume,) + + class SetVolume(command.Command): """Set volume properties""" diff --git a/releasenotes/notes/volume-migrate-command-52cf6edd62fe17a7.yaml b/releasenotes/notes/volume-migrate-command-52cf6edd62fe17a7.yaml new file mode 100644 index 0000000000..634f008242 --- /dev/null +++ b/releasenotes/notes/volume-migrate-command-52cf6edd62fe17a7.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add ``volume migrate`` command. + [Blueprint `cinder-command-support `_] diff --git a/setup.cfg b/setup.cfg index a4abec1bc5..c94437393e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -463,6 +463,7 @@ openstack.volume.v1 = volume_create = openstackclient.volume.v1.volume:CreateVolume volume_delete = openstackclient.volume.v1.volume:DeleteVolume volume_list = openstackclient.volume.v1.volume:ListVolume + volume_migrate = openstackclient.volume.v1.volume:MigrateVolume volume_set = openstackclient.volume.v1.volume:SetVolume volume_show = openstackclient.volume.v1.volume:ShowVolume volume_unset = openstackclient.volume.v1.volume:UnsetVolume @@ -517,6 +518,7 @@ openstack.volume.v2 = volume_create = openstackclient.volume.v2.volume:CreateVolume volume_delete = openstackclient.volume.v2.volume:DeleteVolume volume_list = openstackclient.volume.v2.volume:ListVolume + volume_migrate = openstackclient.volume.v2.volume:MigrateVolume volume_set = openstackclient.volume.v2.volume:SetVolume volume_show = openstackclient.volume.v2.volume:ShowVolume volume_unset = openstackclient.volume.v2.volume:UnsetVolume