From 68afbfea71434ad34b8bc0c473d36f1a04e74290 Mon Sep 17 00:00:00 2001 From: jiaopengju Date: Sun, 28 Jul 2019 09:59:53 +0800 Subject: [PATCH] Add support for image boot server backup with data This patch added support for image boot server backup with data. If the image's parent resource is server type, and the option 'enable_server_backup' is True, it will take snapshot at first, then backup the new created image by doing snapshot. Change-Id: If548e667f44f671278b2b13c19f97a15d867d905 Story: 1712059 Task: 33997 --- .../protection/protectable_plugins/image.py | 3 +- .../image/image_protection_plugin.py | 121 ++++++++++++++++-- .../plugins/test_image_protectable_plugin.py | 7 +- .../test_glance_protection_plugin.py | 54 ++++++++ 4 files changed, 169 insertions(+), 16 deletions(-) diff --git a/karbor/services/protection/protectable_plugins/image.py b/karbor/services/protection/protectable_plugins/image.py index 35b2e20a..fd23de74 100644 --- a/karbor/services/protection/protectable_plugins/image.py +++ b/karbor/services/protection/protectable_plugins/image.py @@ -82,7 +82,8 @@ class ImageProtectablePlugin(protectable_plugin.ProtectablePlugin): reason=six.text_type(e)) return [resource.Resource(type=self._SUPPORT_RESOURCE_TYPE, id=server.image['id'], - name=image.name)] + name=image.name, + extra_info={'server_id': server.id})] def _get_dependent_resources_by_project(self, context, diff --git a/karbor/services/protection/protection_plugins/image/image_protection_plugin.py b/karbor/services/protection/protection_plugins/image/image_protection_plugin.py index 3d1e70f5..bdcdf5b9 100644 --- a/karbor/services/protection/protection_plugins/image/image_protection_plugin.py +++ b/karbor/services/protection/protection_plugins/image/image_protection_plugin.py @@ -30,6 +30,10 @@ image_backup_opts = [ 'the size of image\'s chunk).'), cfg.IntOpt('poll_interval', default=10, help='Poll interval for image status'), + cfg.BoolOpt('enable_server_snapshot', + default=True, + help='Enable server snapshot when server is ' + 'the parent resource of image') ] LOG = logging.getLogger(__name__) @@ -47,23 +51,46 @@ def get_image_status(glance_client, image_id): return status +def get_server_status(nova_client, server_id): + LOG.debug('Polling server (server_id: %s)', server_id) + try: + server = nova_client.servers.get(server_id) + status = server.status + except exception.NotFound: + status = 'not-found' + LOG.debug('Polled server (server_id: %s) status: %s', server_id, status) + return status + + class ProtectOperation(protection_plugin.Operation): def __init__(self, backup_image_object_size, - poll_interval): + poll_interval, enable_server_snapshot): super(ProtectOperation, self).__init__() self._data_block_size_bytes = backup_image_object_size self._interval = poll_interval + self._enable_server_snapshot = enable_server_snapshot def on_main(self, checkpoint, resource, context, parameters, **kwargs): - image_id = resource.id - bank_section = checkpoint.get_resource_bank_section(image_id) - - resource_definition = {"resource_id": image_id} + LOG.debug('Start creating image backup, resource info: %s', + resource.to_dict()) + resource_id = resource.id + bank_section = checkpoint.get_resource_bank_section(resource_id) glance_client = ClientFactory.create_client('glance', context) + bank_section.update_object("status", + constants.RESOURCE_STATUS_PROTECTING) + resource_definition = {'resource_id': resource_id} + if resource.extra_info and self._enable_server_snapshot: + image_id = self._create_server_snapshot(context, glance_client, + parameters, resource, + resource_definition, + resource_id) + need_delete_temp_image = True + else: + image_id = resource_id + need_delete_temp_image = False + LOG.info("Creating image backup, image_id: %s.", image_id) try: - bank_section.update_object("status", - constants.RESOURCE_STATUS_PROTECTING) image_info = glance_client.images.get(image_id) if image_info.status != "active": is_success = utils.status_poll( @@ -81,14 +108,12 @@ class ProtectOperation(protection_plugin.Operation): reason="The status of image is invalid.", resource_id=image_id, resource_type=constants.IMAGE_RESOURCE_TYPE) - image_metadata = { "disk_format": image_info.disk_format, "container_format": image_info.container_format, - "checksum": image_info.checksum + "checksum": image_info.checksum, } resource_definition["image_metadata"] = image_metadata - bank_section.update_object("metadata", resource_definition) except Exception as err: LOG.error("Create image backup failed, image_id: %s.", image_id) @@ -100,6 +125,71 @@ class ProtectOperation(protection_plugin.Operation): resource_id=image_id, resource_type=constants.IMAGE_RESOURCE_TYPE) self._create_backup(glance_client, bank_section, image_id) + if need_delete_temp_image: + try: + glance_client.images.delete(image_id) + except Exception as error: + LOG.warning('Failed to delete temporary image: %s', error) + else: + LOG.debug('Delete temporary image(%s) success', image_id) + + def _create_server_snapshot(self, context, glance_client, parameters, + resource, resource_definition, resource_id): + server_id = resource.extra_info.get('server_id') + resource_definition['resource_id'] = resource_id + resource_definition['server_id'] = server_id + nova_client = ClientFactory.create_client('nova', context) + is_success = utils.status_poll( + partial(get_server_status, nova_client, server_id), + interval=self._interval, + success_statuses={'ACTIVE', 'STOPPED', 'SUSPENDED', + 'PAUSED'}, + failure_statuses={'DELETED', 'ERROR', 'RESIZED', 'SHELVED', + 'SHELVED_OFFLOADED', 'SOFT_DELETED', + 'RESCUED', 'not-found'}, + ignore_statuses={'BUILDING'}, + ) + if not is_success: + raise exception.CreateResourceFailed( + name="Image Backup", + reason='The parent server of the image is not in valid' + ' status', + resource_id=resource_id, + resource_type=constants.IMAGE_RESOURCE_TYPE) + temp_image_name = 'Temp_image_name_for_karbor' + server_id + try: + image_uuid = nova_client.servers.create_image( + server_id, temp_image_name, parameters) + except Exception as e: + msg = "Failed to create the server snapshot: %s" % e + LOG.exception(msg) + raise exception.CreateResourceFailed( + name="Image Backup", + reason=msg, + resource_id=resource_id, + resource_type=constants.IMAGE_RESOURCE_TYPE + ) + else: + is_success = utils.status_poll( + partial(get_image_status, glance_client, image_uuid), + interval=self._interval, + success_statuses={'active'}, + failure_statuses={'killed', 'deleted', 'pending_delete', + 'deactivated'}, + ignore_statuses={'queued', 'saving', 'uploading'}) + if not is_success: + msg = "Image has been created, but fail to become " \ + "active, so delete it and raise exception." + LOG.error(msg) + glance_client.images.delete(image_uuid) + image_uuid = None + if not image_uuid: + raise exception.CreateResourceFailed( + name="Image Backup", + reason="Create parent server snapshot failed.", + resource_id=resource_id, + resource_type=constants.IMAGE_RESOURCE_TYPE) + return image_uuid def _create_backup(self, glance_client, bank_section, image_id): try: @@ -161,9 +251,10 @@ class DeleteOperation(protection_plugin.Operation): class RestoreOperation(protection_plugin.Operation): - def __init__(self, poll_interval): + def __init__(self, poll_interval, enable_server_snapshot): super(RestoreOperation, self).__init__() self._interval = poll_interval + self._enable_server_snapshot = enable_server_snapshot def on_main(self, checkpoint, resource, context, parameters, **kwargs): original_image_id = resource.id @@ -250,6 +341,8 @@ class GlanceProtectionPlugin(protection_plugin.ProtectionPlugin): self._data_block_size_bytes = ( self._plugin_config.backup_image_object_size) self._poll_interval = self._plugin_config.poll_interval + self._enable_server_snapshot = ( + self._plugin_config.enable_server_snapshot) if self._data_block_size_bytes % 65536 != 0 or ( self._data_block_size_bytes <= 0): @@ -283,10 +376,12 @@ class GlanceProtectionPlugin(protection_plugin.ProtectionPlugin): def get_protect_operation(self, resource): return ProtectOperation(self._data_block_size_bytes, - self._poll_interval) + self._poll_interval, + self._enable_server_snapshot) def get_restore_operation(self, resource): - return RestoreOperation(self._poll_interval) + return RestoreOperation(self._poll_interval, + self._enable_server_snapshot) def get_verify_operation(self, resource): return VerifyOperation() diff --git a/karbor/tests/unit/plugins/test_image_protectable_plugin.py b/karbor/tests/unit/plugins/test_image_protectable_plugin.py index e2ff896f..14fea43d 100644 --- a/karbor/tests/unit/plugins/test_image_protectable_plugin.py +++ b/karbor/tests/unit/plugins/test_image_protectable_plugin.py @@ -139,8 +139,11 @@ class ImageProtectablePluginTest(base.TestCase): mock_server_get.return_value = vm mock_image_get.return_value = image self.assertEqual(plugin.get_dependent_resources(self._context, vm), - [resource.Resource(type=constants.IMAGE_RESOURCE_TYPE, - id='123', name='name123')]) + [resource.Resource( + type=constants.IMAGE_RESOURCE_TYPE, + id='123', + name='name123', + extra_info={'server_id': 'server1'})]) @mock.patch.object(images.Controller, 'list') def test_get_project_dependent_resources(self, mock_image_list): diff --git a/karbor/tests/unit/protection/test_glance_protection_plugin.py b/karbor/tests/unit/protection/test_glance_protection_plugin.py index 378a977e..e110b2a3 100644 --- a/karbor/tests/unit/protection/test_glance_protection_plugin.py +++ b/karbor/tests/unit/protection/test_glance_protection_plugin.py @@ -23,6 +23,7 @@ from karbor.services.protection.protection_plugins. \ from karbor.services.protection.protection_plugins.image \ import image_plugin_schemas from karbor.tests import base +from keystoneauth1 import session as keystone_session import mock from oslo_config import cfg from oslo_config import fixture @@ -63,6 +64,11 @@ Image = collections.namedtuple( "status"] ) +Server = collections.namedtuple( + "Server", + ["status"] +) + def call_hooks(operation, checkpoint, resource, context, parameters, **kwargs): def noop(*args, **kwargs): @@ -102,6 +108,10 @@ class GlanceProtectionPluginTest(base.TestCase): group='image_backup_plugin', backup_image_object_size=65536, ) + plugin_config_fixture.load_raw_values( + group='image_backup_plugin', + enable_server_snapshot=True, + ) self.plugin = GlanceProtectionPlugin(plugin_config) cfg.CONF.set_default('glance_endpoint', 'http://127.0.0.1:9292', @@ -157,6 +167,50 @@ class GlanceProtectionPluginTest(base.TestCase): call_hooks(protect_operation, self.checkpoint, resource, self.cntxt, {}) + @mock.patch('karbor.services.protection.client_factory.ClientFactory.' + '_generate_session') + @mock.patch('karbor.services.protection.protection_plugins.image.' + 'image_protection_plugin.utils.status_poll') + @mock.patch('karbor.services.protection.clients.nova.create') + @mock.patch('karbor.services.protection.clients.glance.create') + def test_create_backup_with_server_id_in_extra_info( + self, mock_glance_create, mock_nova_create, mock_status_poll, + mock_generate_session): + cfg.CONF.set_default('nova_endpoint', + 'http://127.0.0.1:8774/v2.1', + 'nova_client') + self.nova_client = client_factory.ClientFactory.create_client( + "nova", self.cntxt) + mock_generate_session.return_value = keystone_session.Session( + auth=None) + resource = Resource(id="123", + type=constants.IMAGE_RESOURCE_TYPE, + name='fake', + extra_info={'server_id': 'fake_server_id'}) + + protect_operation = self.plugin.get_protect_operation(resource) + mock_glance_create.return_value = self.glance_client + self.glance_client.images.get = mock.MagicMock() + self.glance_client.images.return_value = Image( + disk_format="", + container_format="", + status="active" + ) + + mock_nova_create.return_value = self.nova_client + self.nova_client.servers.get = mock.MagicMock() + self.nova_client.servers.get.return_value = Server( + status='ACTIVE') + self.nova_client.servers.image_create = mock.MagicMock() + self.nova_client.servers.image_create.return_value = '345' + fake_bank_section.update_object = mock.MagicMock() + self.glance_client.images.data = mock.MagicMock() + self.glance_client.images.data.return_value = [] + + mock_status_poll.return_value = True + call_hooks(protect_operation, self.checkpoint, resource, self.cntxt, + {}) + def test_delete_backup(self): resource = Resource(id="123", type=constants.IMAGE_RESOURCE_TYPE,