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:
parent
7fdbc6b8af
commit
01eb4e8393
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user