summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/compute/v2/server.py112
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py161
2 files changed, 273 insertions, 0 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 294b4683..7f1bc088 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='<server>',
+ help=_('Server (name or ID)'),
+ )
+
+ parser.add_argument(
+ '--wait', action='store_true',
+ help=_('Wait for evacuation to complete'),
+ )
+ parser.add_argument(
+ '--host', metavar='<host>', 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='<password>', 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 380ef66b..20e3f70e 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):