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
This commit is contained in:
Sean Mooney 2019-03-15 13:03:10 +00:00 committed by Stephen Finucane
parent 7fdbc6b8af
commit 01eb4e8393
5 changed files with 283 additions and 0 deletions

View File

@ -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

View File

@ -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")

View File

@ -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):

View File

@ -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.

View File

@ -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