Merge "Add 'openstack server evacuate' command"
This commit is contained in:
		@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2513,6 +2513,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")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4984,6 +4984,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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user