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
This commit is contained in:
jiaopengju 2019-07-28 09:59:53 +08:00
parent 0c969f766d
commit 68afbfea71
4 changed files with 169 additions and 16 deletions

View File

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

View File

@ -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)
LOG.info("Creating image backup, image_id: %s.", image_id)
try:
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:
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()

View File

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

View File

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