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))
|
reason=six.text_type(e))
|
||||||
return [resource.Resource(type=self._SUPPORT_RESOURCE_TYPE,
|
return [resource.Resource(type=self._SUPPORT_RESOURCE_TYPE,
|
||||||
id=server.image['id'],
|
id=server.image['id'],
|
||||||
name=image.name)]
|
name=image.name,
|
||||||
|
extra_info={'server_id': server.id})]
|
||||||
|
|
||||||
def _get_dependent_resources_by_project(self,
|
def _get_dependent_resources_by_project(self,
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -30,6 +30,10 @@ image_backup_opts = [
|
||||||
'the size of image\'s chunk).'),
|
'the size of image\'s chunk).'),
|
||||||
cfg.IntOpt('poll_interval', default=10,
|
cfg.IntOpt('poll_interval', default=10,
|
||||||
help='Poll interval for image status'),
|
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__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -47,23 +51,46 @@ def get_image_status(glance_client, image_id):
|
||||||
return status
|
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):
|
class ProtectOperation(protection_plugin.Operation):
|
||||||
def __init__(self, backup_image_object_size,
|
def __init__(self, backup_image_object_size,
|
||||||
poll_interval):
|
poll_interval, enable_server_snapshot):
|
||||||
super(ProtectOperation, self).__init__()
|
super(ProtectOperation, self).__init__()
|
||||||
self._data_block_size_bytes = backup_image_object_size
|
self._data_block_size_bytes = backup_image_object_size
|
||||||
self._interval = poll_interval
|
self._interval = poll_interval
|
||||||
|
self._enable_server_snapshot = enable_server_snapshot
|
||||||
|
|
||||||
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
|
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
|
||||||
image_id = resource.id
|
LOG.debug('Start creating image backup, resource info: %s',
|
||||||
bank_section = checkpoint.get_resource_bank_section(image_id)
|
resource.to_dict())
|
||||||
|
resource_id = resource.id
|
||||||
resource_definition = {"resource_id": image_id}
|
bank_section = checkpoint.get_resource_bank_section(resource_id)
|
||||||
glance_client = ClientFactory.create_client('glance', context)
|
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)
|
LOG.info("Creating image backup, image_id: %s.", image_id)
|
||||||
try:
|
try:
|
||||||
bank_section.update_object("status",
|
|
||||||
constants.RESOURCE_STATUS_PROTECTING)
|
|
||||||
image_info = glance_client.images.get(image_id)
|
image_info = glance_client.images.get(image_id)
|
||||||
if image_info.status != "active":
|
if image_info.status != "active":
|
||||||
is_success = utils.status_poll(
|
is_success = utils.status_poll(
|
||||||
|
@ -81,14 +108,12 @@ class ProtectOperation(protection_plugin.Operation):
|
||||||
reason="The status of image is invalid.",
|
reason="The status of image is invalid.",
|
||||||
resource_id=image_id,
|
resource_id=image_id,
|
||||||
resource_type=constants.IMAGE_RESOURCE_TYPE)
|
resource_type=constants.IMAGE_RESOURCE_TYPE)
|
||||||
|
|
||||||
image_metadata = {
|
image_metadata = {
|
||||||
"disk_format": image_info.disk_format,
|
"disk_format": image_info.disk_format,
|
||||||
"container_format": image_info.container_format,
|
"container_format": image_info.container_format,
|
||||||
"checksum": image_info.checksum
|
"checksum": image_info.checksum,
|
||||||
}
|
}
|
||||||
resource_definition["image_metadata"] = image_metadata
|
resource_definition["image_metadata"] = image_metadata
|
||||||
|
|
||||||
bank_section.update_object("metadata", resource_definition)
|
bank_section.update_object("metadata", resource_definition)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
LOG.error("Create image backup failed, image_id: %s.", image_id)
|
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_id=image_id,
|
||||||
resource_type=constants.IMAGE_RESOURCE_TYPE)
|
resource_type=constants.IMAGE_RESOURCE_TYPE)
|
||||||
self._create_backup(glance_client, bank_section, image_id)
|
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):
|
def _create_backup(self, glance_client, bank_section, image_id):
|
||||||
try:
|
try:
|
||||||
|
@ -161,9 +251,10 @@ class DeleteOperation(protection_plugin.Operation):
|
||||||
|
|
||||||
|
|
||||||
class RestoreOperation(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__()
|
super(RestoreOperation, self).__init__()
|
||||||
self._interval = poll_interval
|
self._interval = poll_interval
|
||||||
|
self._enable_server_snapshot = enable_server_snapshot
|
||||||
|
|
||||||
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
|
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
|
||||||
original_image_id = resource.id
|
original_image_id = resource.id
|
||||||
|
@ -250,6 +341,8 @@ class GlanceProtectionPlugin(protection_plugin.ProtectionPlugin):
|
||||||
self._data_block_size_bytes = (
|
self._data_block_size_bytes = (
|
||||||
self._plugin_config.backup_image_object_size)
|
self._plugin_config.backup_image_object_size)
|
||||||
self._poll_interval = self._plugin_config.poll_interval
|
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 (
|
if self._data_block_size_bytes % 65536 != 0 or (
|
||||||
self._data_block_size_bytes <= 0):
|
self._data_block_size_bytes <= 0):
|
||||||
|
@ -283,10 +376,12 @@ class GlanceProtectionPlugin(protection_plugin.ProtectionPlugin):
|
||||||
|
|
||||||
def get_protect_operation(self, resource):
|
def get_protect_operation(self, resource):
|
||||||
return ProtectOperation(self._data_block_size_bytes,
|
return ProtectOperation(self._data_block_size_bytes,
|
||||||
self._poll_interval)
|
self._poll_interval,
|
||||||
|
self._enable_server_snapshot)
|
||||||
|
|
||||||
def get_restore_operation(self, resource):
|
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):
|
def get_verify_operation(self, resource):
|
||||||
return VerifyOperation()
|
return VerifyOperation()
|
||||||
|
|
|
@ -139,8 +139,11 @@ class ImageProtectablePluginTest(base.TestCase):
|
||||||
mock_server_get.return_value = vm
|
mock_server_get.return_value = vm
|
||||||
mock_image_get.return_value = image
|
mock_image_get.return_value = image
|
||||||
self.assertEqual(plugin.get_dependent_resources(self._context, vm),
|
self.assertEqual(plugin.get_dependent_resources(self._context, vm),
|
||||||
[resource.Resource(type=constants.IMAGE_RESOURCE_TYPE,
|
[resource.Resource(
|
||||||
id='123', name='name123')])
|
type=constants.IMAGE_RESOURCE_TYPE,
|
||||||
|
id='123',
|
||||||
|
name='name123',
|
||||||
|
extra_info={'server_id': 'server1'})])
|
||||||
|
|
||||||
@mock.patch.object(images.Controller, 'list')
|
@mock.patch.object(images.Controller, 'list')
|
||||||
def test_get_project_dependent_resources(self, mock_image_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 \
|
from karbor.services.protection.protection_plugins.image \
|
||||||
import image_plugin_schemas
|
import image_plugin_schemas
|
||||||
from karbor.tests import base
|
from karbor.tests import base
|
||||||
|
from keystoneauth1 import session as keystone_session
|
||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture
|
from oslo_config import fixture
|
||||||
|
@ -63,6 +64,11 @@ Image = collections.namedtuple(
|
||||||
"status"]
|
"status"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Server = collections.namedtuple(
|
||||||
|
"Server",
|
||||||
|
["status"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def call_hooks(operation, checkpoint, resource, context, parameters, **kwargs):
|
def call_hooks(operation, checkpoint, resource, context, parameters, **kwargs):
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
|
@ -102,6 +108,10 @@ class GlanceProtectionPluginTest(base.TestCase):
|
||||||
group='image_backup_plugin',
|
group='image_backup_plugin',
|
||||||
backup_image_object_size=65536,
|
backup_image_object_size=65536,
|
||||||
)
|
)
|
||||||
|
plugin_config_fixture.load_raw_values(
|
||||||
|
group='image_backup_plugin',
|
||||||
|
enable_server_snapshot=True,
|
||||||
|
)
|
||||||
self.plugin = GlanceProtectionPlugin(plugin_config)
|
self.plugin = GlanceProtectionPlugin(plugin_config)
|
||||||
cfg.CONF.set_default('glance_endpoint',
|
cfg.CONF.set_default('glance_endpoint',
|
||||||
'http://127.0.0.1:9292',
|
'http://127.0.0.1:9292',
|
||||||
|
@ -157,6 +167,50 @@ class GlanceProtectionPluginTest(base.TestCase):
|
||||||
call_hooks(protect_operation, self.checkpoint, resource, self.cntxt,
|
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):
|
def test_delete_backup(self):
|
||||||
resource = Resource(id="123",
|
resource = Resource(id="123",
|
||||||
type=constants.IMAGE_RESOURCE_TYPE,
|
type=constants.IMAGE_RESOURCE_TYPE,
|
||||||
|
|
Loading…
Reference in New Issue