summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Mooney <work@seanmooney.info>2019-03-15 13:03:10 +0000
committerStephen Finucane <sfinucan@redhat.com>2020-11-02 17:08:17 +0000
commit01eb4e839394fe433a92a06daf1499fb00f2fe69 (patch)
treedd77dc8337d8b714a567241c977072c636031188
parent7fdbc6b8af681fc48bd3a78822ffab52c29329ac (diff)
downloadpython-openstackclient-01eb4e839394fe433a92a06daf1499fb00f2fe69.tar.gz
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
-rw-r--r--doc/source/cli/command-objects/server.rst3
-rw-r--r--openstackclient/compute/v2/server.py112
-rw-r--r--openstackclient/tests/unit/compute/v2/test_server.py161
-rw-r--r--releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml6
-rw-r--r--setup.cfg1
5 files changed, 283 insertions, 0 deletions
diff --git a/doc/source/cli/command-objects/server.rst b/doc/source/cli/command-objects/server.rst
index 89eb4e37..cf7df1da 100644
--- a/doc/source/cli/command-objects/server.rst
+++ b/doc/source/cli/command-objects/server.rst
@@ -11,6 +11,9 @@ Compute v2
:command: server create
.. autoprogram-cliff:: openstack.compute.v2
+ :command: server evacuate
+
+.. autoprogram-cliff:: openstack.compute.v2
:command: server delete
.. autoprogram-cliff:: openstack.compute.v2
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):
diff --git a/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml b/releasenotes/notes/add-server-evacuate-8359246692cb642f.yaml
new file mode 100644
index 00000000..41bbed73
--- /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 8363ec6c..a29852e3 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