Skip to content

Commit 4d3bad9

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add 'openstack server evacuate' command"
2 parents 0a7f269 + 01eb4e8 commit 4d3bad9

5 files changed

Lines changed: 283 additions & 0 deletions

File tree

doc/source/cli/command-objects/server.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Compute v2
1010
.. autoprogram-cliff:: openstack.compute.v2
1111
:command: server create
1212

13+
.. autoprogram-cliff:: openstack.compute.v2
14+
:command: server evacuate
15+
1316
.. autoprogram-cliff:: openstack.compute.v2
1417
:command: server delete
1518

openstackclient/compute/v2/server.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,6 +2513,118 @@ def _show_progress(progress):
25132513
return zip(*sorted(details.items()))
25142514

25152515

2516+
class EvacuateServer(command.ShowOne):
2517+
_description = _("""Evacuate a server to a different host.
2518+
2519+
This command is used to recreate a server after the host it was on has failed.
2520+
It can only be used if the compute service that manages the server is down.
2521+
This command should only be used by an admin after they have confirmed that the
2522+
instance is not running on the failed host.
2523+
2524+
If the server instance was created with an ephemeral root disk on non-shared
2525+
storage the server will be rebuilt using the original glance image preserving
2526+
the ports and any attached data volumes.
2527+
2528+
If the server uses boot for volume or has its root disk on shared storage the
2529+
root disk will be preserved and reused for the evacuated instance on the new
2530+
host.""")
2531+
2532+
def get_parser(self, prog_name):
2533+
parser = super(EvacuateServer, self).get_parser(prog_name)
2534+
parser.add_argument(
2535+
'server',
2536+
metavar='<server>',
2537+
help=_('Server (name or ID)'),
2538+
)
2539+
2540+
parser.add_argument(
2541+
'--wait', action='store_true',
2542+
help=_('Wait for evacuation to complete'),
2543+
)
2544+
parser.add_argument(
2545+
'--host', metavar='<host>', default=None,
2546+
help=_(
2547+
'Set the preferred host on which to rebuild the evacuated '
2548+
'server. The host will be validated by the scheduler. '
2549+
'(supported by --os-compute-api-version 2.29 or above)'
2550+
),
2551+
)
2552+
shared_storage_group = parser.add_mutually_exclusive_group()
2553+
shared_storage_group.add_argument(
2554+
'--password', metavar='<password>', default=None,
2555+
help=_(
2556+
'Set the password on the evacuated instance. This option is '
2557+
'mutually exclusive with the --shared-storage option'
2558+
),
2559+
)
2560+
shared_storage_group.add_argument(
2561+
'--shared-storage', action='store_true', dest='shared_storage',
2562+
help=_(
2563+
'Indicate that the instance is on shared storage. '
2564+
'This will be auto-calculated with '
2565+
'--os-compute-api-version 2.14 and greater and should not '
2566+
'be used with later microversions. This option is mutually '
2567+
'exclusive with the --password option'
2568+
),
2569+
)
2570+
return parser
2571+
2572+
def take_action(self, parsed_args):
2573+
2574+
def _show_progress(progress):
2575+
if progress:
2576+
self.app.stdout.write('\rProgress: %s' % progress)
2577+
self.app.stdout.flush()
2578+
2579+
compute_client = self.app.client_manager.compute
2580+
image_client = self.app.client_manager.image
2581+
2582+
if parsed_args.host:
2583+
if compute_client.api_version < api_versions.APIVersion('2.29'):
2584+
msg = _(
2585+
'--os-compute-api-version 2.29 or later is required '
2586+
'to specify a preferred host.'
2587+
)
2588+
raise exceptions.CommandError(msg)
2589+
2590+
if parsed_args.shared_storage:
2591+
if compute_client.api_version > api_versions.APIVersion('2.13'):
2592+
msg = _(
2593+
'--os-compute-api-version 2.13 or earlier is required '
2594+
'to specify shared-storage.'
2595+
)
2596+
raise exceptions.CommandError(msg)
2597+
2598+
kwargs = {
2599+
'host': parsed_args.host,
2600+
'password': parsed_args.password,
2601+
}
2602+
2603+
if compute_client.api_version <= api_versions.APIVersion('2.13'):
2604+
kwargs['on_shared_storage'] = parsed_args.shared_storage
2605+
2606+
server = utils.find_resource(
2607+
compute_client.servers, parsed_args.server)
2608+
2609+
server = server.evacuate(**kwargs)
2610+
2611+
if parsed_args.wait:
2612+
if utils.wait_for_status(
2613+
compute_client.servers.get,
2614+
server.id,
2615+
callback=_show_progress,
2616+
):
2617+
self.app.stdout.write(_('Complete\n'))
2618+
else:
2619+
LOG.error(_('Error evacuating server: %s'), server.id)
2620+
self.app.stdout.write(_('Error evacuating server\n'))
2621+
raise SystemExit
2622+
2623+
details = _prep_server_detail(
2624+
compute_client, image_client, server, refresh=False)
2625+
return zip(*sorted(details.items()))
2626+
2627+
25162628
class RemoveFixedIP(command.Command):
25172629
_description = _("Remove fixed IP address from server")
25182630

openstackclient/tests/unit/compute/v2/test_server.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4984,6 +4984,167 @@ def test_rebuild_with_key_name_and_unset(self):
49844984
self.cmd, arglist, verifylist)
49854985

49864986

4987+
class TestEvacuateServer(TestServer):
4988+
4989+
def setUp(self):
4990+
super(TestEvacuateServer, self).setUp()
4991+
# Return value for utils.find_resource for image
4992+
self.image = image_fakes.FakeImage.create_one_image()
4993+
self.images_mock.get.return_value = self.image
4994+
4995+
# Fake the rebuilt new server.
4996+
attrs = {
4997+
'image': {
4998+
'id': self.image.id
4999+
},
5000+
'networks': {},
5001+
'adminPass': 'passw0rd',
5002+
}
5003+
new_server = compute_fakes.FakeServer.create_one_server(attrs=attrs)
5004+
5005+
# Fake the server to be rebuilt. The IDs of them should be the same.
5006+
attrs['id'] = new_server.id
5007+
methods = {
5008+
'evacuate': new_server,
5009+
}
5010+
self.server = compute_fakes.FakeServer.create_one_server(
5011+
attrs=attrs,
5012+
methods=methods
5013+
)
5014+
5015+
# Return value for utils.find_resource for server.
5016+
self.servers_mock.get.return_value = self.server
5017+
5018+
self.cmd = server.EvacuateServer(self.app, None)
5019+
5020+
def _test_evacuate(self, args, verify_args, evac_args):
5021+
parsed_args = self.check_parser(self.cmd, args, verify_args)
5022+
5023+
# Get the command object to test
5024+
self.cmd.take_action(parsed_args)
5025+
5026+
self.servers_mock.get.assert_called_with(self.server.id)
5027+
self.server.evacuate.assert_called_with(**evac_args)
5028+
5029+
def test_evacuate(self):
5030+
args = [
5031+
self.server.id,
5032+
]
5033+
verify_args = [
5034+
('server', self.server.id),
5035+
]
5036+
evac_args = {
5037+
'host': None, 'on_shared_storage': False, 'password': None,
5038+
}
5039+
self._test_evacuate(args, verify_args, evac_args)
5040+
5041+
def test_evacuate_with_password(self):
5042+
args = [
5043+
self.server.id,
5044+
'--password', 'password',
5045+
]
5046+
verify_args = [
5047+
('server', self.server.id),
5048+
('password', 'password'),
5049+
]
5050+
evac_args = {
5051+
'host': None, 'on_shared_storage': False, 'password': 'password',
5052+
}
5053+
self._test_evacuate(args, verify_args, evac_args)
5054+
5055+
def test_evacuate_with_host(self):
5056+
self.app.client_manager.compute.api_version = \
5057+
api_versions.APIVersion('2.29')
5058+
5059+
host = 'target-host'
5060+
args = [
5061+
self.server.id,
5062+
'--host', 'target-host',
5063+
]
5064+
verify_args = [
5065+
('server', self.server.id),
5066+
('host', 'target-host'),
5067+
]
5068+
evac_args = {'host': host, 'password': None}
5069+
5070+
self._test_evacuate(args, verify_args, evac_args)
5071+
5072+
def test_evacuate_with_host_pre_v229(self):
5073+
self.app.client_manager.compute.api_version = \
5074+
api_versions.APIVersion('2.28')
5075+
5076+
args = [
5077+
self.server.id,
5078+
'--host', 'target-host',
5079+
]
5080+
verify_args = [
5081+
('server', self.server.id),
5082+
('host', 'target-host'),
5083+
]
5084+
parsed_args = self.check_parser(self.cmd, args, verify_args)
5085+
5086+
self.assertRaises(
5087+
exceptions.CommandError,
5088+
self.cmd.take_action,
5089+
parsed_args)
5090+
5091+
def test_evacuate_without_share_storage(self):
5092+
self.app.client_manager.compute.api_version = \
5093+
api_versions.APIVersion('2.13')
5094+
5095+
args = [
5096+
self.server.id,
5097+
'--shared-storage'
5098+
]
5099+
verify_args = [
5100+
('server', self.server.id),
5101+
('shared_storage', True),
5102+
]
5103+
evac_args = {
5104+
'host': None, 'on_shared_storage': True, 'password': None,
5105+
}
5106+
self._test_evacuate(args, verify_args, evac_args)
5107+
5108+
def test_evacuate_without_share_storage_post_v213(self):
5109+
self.app.client_manager.compute.api_version = \
5110+
api_versions.APIVersion('2.14')
5111+
5112+
args = [
5113+
self.server.id,
5114+
'--shared-storage'
5115+
]
5116+
verify_args = [
5117+
('server', self.server.id),
5118+
('shared_storage', True),
5119+
]
5120+
parsed_args = self.check_parser(self.cmd, args, verify_args)
5121+
5122+
self.assertRaises(
5123+
exceptions.CommandError,
5124+
self.cmd.take_action,
5125+
parsed_args)
5126+
5127+
@mock.patch.object(common_utils, 'wait_for_status', return_value=True)
5128+
def test_evacuate_with_wait_ok(self, mock_wait_for_status):
5129+
args = [
5130+
self.server.id,
5131+
'--wait',
5132+
]
5133+
verify_args = [
5134+
('server', self.server.id),
5135+
('wait', True),
5136+
]
5137+
evac_args = {
5138+
'host': None, 'on_shared_storage': False, 'password': None,
5139+
}
5140+
self._test_evacuate(args, verify_args, evac_args)
5141+
mock_wait_for_status.assert_called_once_with(
5142+
self.servers_mock.get,
5143+
self.server.id,
5144+
callback=mock.ANY,
5145+
)
5146+
5147+
49875148
class TestServerRemoveFixedIP(TestServer):
49885149

49895150
def setUp(self):
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Add ``server evacuate`` command. This command will recreate an instance
5+
from scratch on a new host and is intended to be used when the original
6+
host fails.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ openstack.compute.v2 =
103103
server_create = openstackclient.compute.v2.server:CreateServer
104104
server_delete = openstackclient.compute.v2.server:DeleteServer
105105
server_dump_create = openstackclient.compute.v2.server:CreateServerDump
106+
server_evacuate = openstackclient.compute.v2.server:EvacuateServer
106107
server_list = openstackclient.compute.v2.server:ListServer
107108
server_lock = openstackclient.compute.v2.server:LockServer
108109
server_migrate = openstackclient.compute.v2.server:MigrateServer

0 commit comments

Comments
 (0)