From 01eb4e839394fe433a92a06daf1499fb00f2fe69 Mon Sep 17 00:00:00 2001 From: Sean Mooney Date: Fri, 15 Mar 2019 13:03:10 +0000 Subject: [PATCH] Add 'openstack server evacuate' command This change adds a new 'openstack server evacuate' command to provide parity with the 'nova evacuate' command. The term "evacuate" is notoriously poor, in that it implies the instance is moved rather than recreated, but it is retained since people are familiar with it now. Change-Id: I1e32ca51036c501862d8e89b3144a9695d98a06f --- doc/source/cli/command-objects/server.rst | 3 + openstackclient/compute/v2/server.py | 112 ++++++++++++ .../tests/unit/compute/v2/test_server.py | 161 ++++++++++++++++++ .../add-server-evacuate-8359246692cb642f.yaml | 6 + setup.cfg | 1 + 5 files changed, 283 insertions(+) create mode 100644 releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml diff --git a/doc/source/cli/command-objects/server.rst b/doc/source/cli/command-objects/server.rst index 89eb4e3710..cf7df1dae3 100644 --- a/doc/source/cli/command-objects/server.rst +++ b/doc/source/cli/command-objects/server.rst @@ -10,6 +10,9 @@ Compute v2 .. autoprogram-cliff:: openstack.compute.v2 :command: server create +.. autoprogram-cliff:: openstack.compute.v2 + :command: server evacuate + .. autoprogram-cliff:: openstack.compute.v2 :command: server delete diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 294b468331..7f1bc08852 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -2502,6 +2502,118 @@ class RebuildServer(command.ShowOne): return zip(*sorted(details.items())) +class EvacuateServer(command.ShowOne): + _description = _("""Evacuate a server to a different host. + +This command is used to recreate a server after the host it was on has failed. +It can only be used if the compute service that manages the server is down. +This command should only be used by an admin after they have confirmed that the +instance is not running on the failed host. + +If the server instance was created with an ephemeral root disk on non-shared +storage the server will be rebuilt using the original glance image preserving +the ports and any attached data volumes. + +If the server uses boot for volume or has its root disk on shared storage the +root disk will be preserved and reused for the evacuated instance on the new +host.""") + + def get_parser(self, prog_name): + parser = super(EvacuateServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server (name or ID)'), + ) + + parser.add_argument( + '--wait', action='store_true', + help=_('Wait for evacuation to complete'), + ) + parser.add_argument( + '--host', metavar='', default=None, + help=_( + 'Set the preferred host on which to rebuild the evacuated ' + 'server. The host will be validated by the scheduler. ' + '(supported by --os-compute-api-version 2.29 or above)' + ), + ) + shared_storage_group = parser.add_mutually_exclusive_group() + shared_storage_group.add_argument( + '--password', metavar='', default=None, + help=_( + 'Set the password on the evacuated instance. This option is ' + 'mutually exclusive with the --shared-storage option' + ), + ) + shared_storage_group.add_argument( + '--shared-storage', action='store_true', dest='shared_storage', + help=_( + 'Indicate that the instance is on shared storage. ' + 'This will be auto-calculated with ' + '--os-compute-api-version 2.14 and greater and should not ' + 'be used with later microversions. This option is mutually ' + 'exclusive with the --password option' + ), + ) + return parser + + def take_action(self, parsed_args): + + def _show_progress(progress): + if progress: + self.app.stdout.write('\rProgress: %s' % progress) + self.app.stdout.flush() + + compute_client = self.app.client_manager.compute + image_client = self.app.client_manager.image + + if parsed_args.host: + if compute_client.api_version < api_versions.APIVersion('2.29'): + msg = _( + '--os-compute-api-version 2.29 or later is required ' + 'to specify a preferred host.' + ) + raise exceptions.CommandError(msg) + + if parsed_args.shared_storage: + if compute_client.api_version > api_versions.APIVersion('2.13'): + msg = _( + '--os-compute-api-version 2.13 or earlier is required ' + 'to specify shared-storage.' + ) + raise exceptions.CommandError(msg) + + kwargs = { + 'host': parsed_args.host, + 'password': parsed_args.password, + } + + if compute_client.api_version <= api_versions.APIVersion('2.13'): + kwargs['on_shared_storage'] = parsed_args.shared_storage + + server = utils.find_resource( + compute_client.servers, parsed_args.server) + + server = server.evacuate(**kwargs) + + if parsed_args.wait: + if utils.wait_for_status( + compute_client.servers.get, + server.id, + callback=_show_progress, + ): + self.app.stdout.write(_('Complete\n')) + else: + LOG.error(_('Error evacuating server: %s'), server.id) + self.app.stdout.write(_('Error evacuating server\n')) + raise SystemExit + + details = _prep_server_detail( + compute_client, image_client, server, refresh=False) + return zip(*sorted(details.items())) + + class RemoveFixedIP(command.Command): _description = _("Remove fixed IP address from server") diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index 380ef66b4e..20e3f70e95 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -4982,6 +4982,167 @@ class TestServerRebuild(TestServer): self.cmd, arglist, verifylist) +class TestEvacuateServer(TestServer): + + def setUp(self): + super(TestEvacuateServer, self).setUp() + # Return value for utils.find_resource for image + self.image = image_fakes.FakeImage.create_one_image() + self.images_mock.get.return_value = self.image + + # Fake the rebuilt new server. + attrs = { + 'image': { + 'id': self.image.id + }, + 'networks': {}, + 'adminPass': 'passw0rd', + } + new_server = compute_fakes.FakeServer.create_one_server(attrs=attrs) + + # Fake the server to be rebuilt. The IDs of them should be the same. + attrs['id'] = new_server.id + methods = { + 'evacuate': new_server, + } + self.server = compute_fakes.FakeServer.create_one_server( + attrs=attrs, + methods=methods + ) + + # Return value for utils.find_resource for server. + self.servers_mock.get.return_value = self.server + + self.cmd = server.EvacuateServer(self.app, None) + + def _test_evacuate(self, args, verify_args, evac_args): + parsed_args = self.check_parser(self.cmd, args, verify_args) + + # Get the command object to test + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with(self.server.id) + self.server.evacuate.assert_called_with(**evac_args) + + def test_evacuate(self): + args = [ + self.server.id, + ] + verify_args = [ + ('server', self.server.id), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_password(self): + args = [ + self.server.id, + '--password', 'password', + ] + verify_args = [ + ('server', self.server.id), + ('password', 'password'), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': 'password', + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_host(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.29') + + host = 'target-host' + args = [ + self.server.id, + '--host', 'target-host', + ] + verify_args = [ + ('server', self.server.id), + ('host', 'target-host'), + ] + evac_args = {'host': host, 'password': None} + + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_with_host_pre_v229(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.28') + + args = [ + self.server.id, + '--host', 'target-host', + ] + verify_args = [ + ('server', self.server.id), + ('host', 'target-host'), + ] + parsed_args = self.check_parser(self.cmd, args, verify_args) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_evacuate_without_share_storage(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.13') + + args = [ + self.server.id, + '--shared-storage' + ] + verify_args = [ + ('server', self.server.id), + ('shared_storage', True), + ] + evac_args = { + 'host': None, 'on_shared_storage': True, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + + def test_evacuate_without_share_storage_post_v213(self): + self.app.client_manager.compute.api_version = \ + api_versions.APIVersion('2.14') + + args = [ + self.server.id, + '--shared-storage' + ] + verify_args = [ + ('server', self.server.id), + ('shared_storage', True), + ] + parsed_args = self.check_parser(self.cmd, args, verify_args) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_evacuate_with_wait_ok(self, mock_wait_for_status): + args = [ + self.server.id, + '--wait', + ] + verify_args = [ + ('server', self.server.id), + ('wait', True), + ] + evac_args = { + 'host': None, 'on_shared_storage': False, 'password': None, + } + self._test_evacuate(args, verify_args, evac_args) + mock_wait_for_status.assert_called_once_with( + self.servers_mock.get, + self.server.id, + callback=mock.ANY, + ) + + class TestServerRemoveFixedIP(TestServer): def setUp(self): diff --git a/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml b/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml new file mode 100644 index 0000000000..41bbed736a --- /dev/null +++ b/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``server evacuate`` command. This command will recreate an instance + from scratch on a new host and is intended to be used when the original + host fails. diff --git a/setup.cfg b/setup.cfg index 8363ec6cfd..a29852e353 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,6 +103,7 @@ openstack.compute.v2 = server_create = openstackclient.compute.v2.server:CreateServer server_delete = openstackclient.compute.v2.server:DeleteServer server_dump_create = openstackclient.compute.v2.server:CreateServerDump + server_evacuate = openstackclient.compute.v2.server:EvacuateServer server_list = openstackclient.compute.v2.server:ListServer server_lock = openstackclient.compute.v2.server:LockServer server_migrate = openstackclient.compute.v2.server:MigrateServer