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:
parent
0c969f766d
commit
68afbfea71
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue