Add freezer protection plugin for karbor

Change-Id: I619b06ff33fb4339a5851098867a46298a9aeb51
blueprint: freezer-protection-plugin
This commit is contained in:
jiaopengju 2017-08-14 16:48:45 +08:00
parent e9160d22ce
commit bf752c4a84
7 changed files with 789 additions and 0 deletions

View File

@ -0,0 +1,74 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from freezerclient.v1 import client as freezer_client
from oslo_config import cfg
from oslo_log import log as logging
from karbor.common import config
from karbor.services.protection.clients import utils
LOG = logging.getLogger(__name__)
SERVICE = "freezer"
freezer_client_opts = [
cfg.StrOpt(SERVICE + '_endpoint',
help='URL of the freezer endpoint.'),
cfg.StrOpt(SERVICE + '_catalog_info',
default='backup:freezer:publicURL',
help='Info to match when looking for freezer in the service '
'catalog. Format is: separated values of the form: '
'<service_type>:<service_name>:<endpoint_type> - '
'Only used if freezer_endpoint is unset'),
cfg.StrOpt(SERVICE + '_ca_cert_file',
help='Location of the CA certificate file '
'to use for client requests in SSL connections.'),
cfg.BoolOpt(SERVICE + '_auth_insecure',
default=False,
help='Bypass verification of server certificate when '
'making SSL connection to Freezer.'),
]
CONFIG_GROUP = '%s_client' % SERVICE
CONF = cfg.CONF
CONF.register_opts(config.service_client_opts, group=CONFIG_GROUP)
CONF.register_opts(config.keystone_client_opts, group=CONFIG_GROUP)
CONF.register_opts(freezer_client_opts, group=CONFIG_GROUP)
CONF.set_default('service_name', 'freezer', CONFIG_GROUP)
CONF.set_default('service_type', 'backup', CONFIG_GROUP)
FREEZERCLIENT_VERSION = '3'
def create(context, conf, **kwargs):
conf.register_opts(freezer_client_opts, group=CONFIG_GROUP)
client_config = conf[CONFIG_GROUP]
url = utils.get_url(SERVICE, context, client_config,
append_project_fmt='%(url)s/%(project)s', **kwargs)
if kwargs.get('session'):
return freezer_client.Client(version=FREEZERCLIENT_VERSION,
session=kwargs.get('session'),
endpoint=url
)
args = {
'project_id': context.project_id,
'project_name': context.project_name,
'cacert': client_config.freezer_ca_cert_file,
'insecure': client_config.freezer_auth_insecure,
'endpoint': url,
'token': context.auth_token,
'version': FREEZERCLIENT_VERSION,
'auth_url': client_config.auth_uri
}
return freezer_client.Client(**args)

View File

@ -0,0 +1,471 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import random
from functools import partial
from oslo_config import cfg
from oslo_log import log as logging
from karbor.common import constants
from karbor import exception
from karbor.services.protection.client_factory import ClientFactory
from karbor.services.protection import protection_plugin
from karbor.services.protection.protection_plugins import utils
from karbor.services.protection.protection_plugins.volume import \
volume_freezer_plugin_schemas
LOG = logging.getLogger(__name__)
freezer_backup_opts = [
cfg.IntOpt(
'poll_interval', default=20,
help='Poll interval for Freezer Backup Resource status.'
),
cfg.StrOpt(
'scheduler_client_id', default=None,
help='The freezer scheduler client id to schedule the jobs'
),
cfg.StrOpt(
'container', default='karbor',
help='The container for Freezer backup storage.'
),
cfg.StrOpt(
'storage', default='swift',
help='The storage type for Freezer backup storage.'
),
cfg.StrOpt(
'ssh_key',
help='The ssh key for Freezer ssh driver.'
),
cfg.StrOpt(
'ssh_username',
help='The ssh user name for Freezer ssh driver.'
),
cfg.StrOpt(
'ssh_host',
help='The ssh host for Freezer ssh driver.'
),
cfg.StrOpt(
'ssh_port',
help='The ssh port for Freezer ssh driver.'
),
cfg.StrOpt(
'endpoint',
help='The storage endpoint for Freezer S3 driver.'
),
cfg.StrOpt(
'access_key',
help='The storage access key for Freezer S3 driver.'
),
cfg.StrOpt(
'secret_key',
help='The storage secret key for Freezer S3 driver.'
)
]
def get_job_status(freezer_job_operation, job_id):
LOG.debug('Polling freezer job status, job_id: {0}'.format(job_id))
job_status = freezer_job_operation.get_status(job_id)
LOG.debug('Polled freezer job status, job_id: {0}, job_status: {1}'.format(
job_id, job_status
))
return job_status
class FreezerStorage(object):
def __init__(self, storage_type, storage_path, **kwargs):
self.storage_type = storage_type
self.storage_path = storage_path
self.config = kwargs
def get_storage(self):
storage = {
'storage': self.storage_type,
'container': self.storage_path
}
if self.storage_type == 's3':
storage['endpoint'] = self.config.get('endpoint', None)
storage['access_key'] = self.config.get('access_key', None)
storage['secret_key'] = self.config.get('secret_key', None)
if self.storage_type == 'ssh':
storage['ssh_key'] = self.config.get('ssh_key', None)
storage['ssh_port'] = self.config.get('ssh_port', None)
storage['ssh_username'] = self.config.get('ssh_username', None)
storage['ssh_host'] = self.config.get('ssh_host', None)
return storage
class FreezerTask(object):
def __init__(self, context):
self.context = context
self.client = ClientFactory.create_client('freezer', self.context)
def _client(self):
return self.client
def get(self, job_id):
return self._client().jobs.get(job_id)
def get_status(self, job_id):
return self._client().jobs.get(job_id).get('job_schedule',
{}).get('result')
def create(self, backup_name, storage, description, resource,
action_type, scheduler_client_id):
return self._build(backup_name, storage, description,
resource, action_type, scheduler_client_id)
def create_delete_job(self, job):
for job_action in job['job_actions']:
job_action['freezer_action']['action'] = 'admin'
job_action['freezer_action']['remove_older_than'] = '-1'
job_id = self._client().jobs.create(job)
self._client().jobs.start_job(job_id)
return job_id, job
def create_restore_job(self, job):
for job_action in job['job_actions']:
job_action['freezer_action']['action'] = 'restore'
job_id = self._client().jobs.create(job)
self._client().jobs.start_job(job_id)
return job_id, job
def delete(self, job_id):
actions = self.actions(job_id)
for action in actions:
self._client().actions.delete(action.get('action_id'))
return self._client().jobs.delete(job_id)
def actions(self, job_id):
job = self.get(job_id)
if not job:
return []
return job.get('job_actions', [])
def _build(self, backup_name, storage, description,
resource, action_type, scheduler_client_id):
client_id = scheduler_client_id if scheduler_client_id else \
FreezerSchedulerClient(self._client()).get_random_client_id()
job = {
'description': resource.id if not description else description,
'job_actions': [self._build_action(
backup_name=backup_name,
storage=storage,
resource=resource,
action_type=action_type,
)],
'client_id': client_id
}
job_id = self._client().jobs.create(job)
self._client().jobs.start_job(job_id)
return job_id, job
@staticmethod
def _build_action(backup_name, storage, resource, action_type):
backup_name = backup_name.replace(' ', '_')
action = {
'backup_name': backup_name,
'action': action_type,
'mode': 'cinder',
'cinder_vol_id': resource.id
}
action = dict(action, **storage.get_storage())
if action_type == 'admin':
action['remove_older_than'] = '-1'
return {'freezer_action': action}
class FreezerSchedulerClient(object):
"""Freezer scheduler to schedule the jobs.
All the freezer scheduler clients should be able to schedule jobs
which resource type is nova instance or cinder volume.
"""
def __init__(self, freezer_client):
self.client = freezer_client
def get_random_client_id(self):
clients = self.client.clients.list()
if len(clients) < 1:
raise Exception('No freezer-scheduler client exist')
client_index = random.randint(0, len(clients) - 1)
return [
c.get('client', {}).get('client_id') for c in clients
][client_index]
class ProtectOperation(protection_plugin.Operation):
def __init__(self, poll_interval, freezer_storage, scheduler_client_id):
super(ProtectOperation, self).__init__()
self._poll_interval = poll_interval
self._scheduler_client_id = scheduler_client_id
self.freezer_storage = freezer_storage
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
resource_id = resource.id
bank_section = checkpoint.get_resource_bank_section(resource_id)
LOG.info('Creating freezer protection backup, resource_id: {0}'
.format(resource_id))
bank_section.update_object('status',
constants.RESOURCE_STATUS_PROTECTING)
backup_name = parameters.get('backup_name', 'backup{0}'
.format(resource_id))
description = parameters.get('description', None)
self.freezer_storage.storage_path = "{0}/{1}".format(
self.freezer_storage.storage_path, checkpoint.id)
job_id, job_info = None, None
freezer_task = FreezerTask(context)
try:
job_id, job_info = freezer_task.create(
backup_name=backup_name,
storage=self.freezer_storage,
description=description,
resource=resource,
action_type='backup',
scheduler_client_id=self._scheduler_client_id
)
LOG.debug('Creating freezer backup job successful, job_id: {0}'
.format(job_id))
is_success = utils.status_poll(
partial(get_job_status, freezer_task, job_id),
interval=self._poll_interval,
success_statuses={'success'},
failure_statuses={'fail'},
ignore_statuses={'aborted', ''},
ignore_unexpected=True
)
if is_success is not True:
LOG.error("The status of freezer job (id: {0}) is invalid."
.format(job_id))
raise exception.CreateResourceFailed(
name="Freezer Backup FreezerTask",
reason="The status of freezer job is invalid.",
resource_id=resource_id,
resource_type=resource.type)
resource_definition = {
'job_id': job_id,
'job_info': job_info
}
bank_section.update_object("metadata", resource_definition)
bank_section.update_object("status",
constants.RESOURCE_STATUS_AVAILABLE)
except exception.CreateResourceFailed as e:
LOG.error('Error creating backup (resource_id: {0}, reason: {1})'
.format(resource_id, e))
if job_id:
freezer_task.delete(job_id)
bank_section.update_object('status',
constants.RESOURCE_STATUS_ERROR)
raise
LOG.debug('Finish creating freezer backup resource')
freezer_task.delete(job_id)
class RestoreOperation(protection_plugin.Operation):
def __init__(self, poll_interval):
super(RestoreOperation, self).__init__()
self._poll_interval = poll_interval
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
resource_id = resource.id
bank_section = checkpoint.get_resource_bank_section(resource_id)
LOG.info("Creating freezer protection backup, resource_id: {0}"
.format(resource_id))
resource_metadata = bank_section.get_object('metadata')
freezer_job_info = resource_metadata.get('job_info', None)
if not freezer_job_info:
raise exception.RestoreResourceFailed(
name='Freezer Backup FreezerTask',
reason='The content of freezer job is invalid.',
resource_id=resource_id,
resource_type=resource.type
)
freezer_task = FreezerTask(context)
job_id, job_info = None, None
try:
job_id, job_info = freezer_task.create_restore_job(
freezer_job_info
)
is_success = utils.status_poll(
partial(get_job_status, freezer_task, job_id),
interval=self._poll_interval,
success_statuses={'success'},
failure_statuses={'fail'},
ignore_statuses={'aborted', ''},
ignore_unexpected=True
)
if is_success is not True:
LOG.error("The status of freezer job (id: {0}) is invalid."
.format(job_id))
raise exception.RestoreResourceFailed(
name="Freezer Backup FreezerTask",
reason="The status of freezer job is invalid.",
resource_id=resource_id,
resource_type=resource.type
)
except Exception as e:
LOG.error("Restore freezer backup resource failed, resource_type:"
"{0}, resource_id: {1}"
.format(resource.type, resource.id))
if job_id:
freezer_task.delete(job_id)
raise exception.RestoreResourceFailed(
name="Freezer Backup FreezerTask",
reason=e,
resource_id=resource_id,
resource_type=resource.type
)
LOG.debug('Finish restoring freezer backup resource')
freezer_task.delete(job_id)
class DeleteOperation(protection_plugin.Operation):
def __init__(self, poll_interval):
super(DeleteOperation, self).__init__()
self._poll_interval = poll_interval
def on_main(self, checkpoint, resource, context, parameters, **kwargs):
resource_id = resource.id
bank_section = checkpoint.get_resource_bank_section(resource_id)
LOG.info("Deleting freezer protection backup, resource_id: {0}"
.format(resource_id))
bank_section.update_object('status',
constants.RESOURCE_STATUS_DELETING)
resource_metadata = bank_section.get_object('metadata')
freezer_task_info = resource_metadata.get('job_info', None)
if not freezer_task_info:
raise exception.DeleteResourceFailed(
name='Freezer Backup FreezerTask',
reason='The content of freezer job is invalid.',
resource_id=resource_id,
resource_type=resource.type
)
freezer_job_operation = FreezerTask(context)
job_id, job_info = None, None
try:
job_id, job_info = freezer_job_operation.create_delete_job(
freezer_task_info
)
is_success = utils.status_poll(
partial(get_job_status, freezer_job_operation, job_id),
interval=self._poll_interval,
success_statuses={'success'},
failure_statuses={'fail'},
ignore_statuses={'aborted', ''},
ignore_unexpected=True
)
if is_success is not True:
LOG.error("The status of freezer job (id: {0}) is invalid."
.format(job_id))
raise exception.CreateResourceFailed(
name="Freezer Backup FreezerTask",
reason="The status of freezer job is invalid.",
resource_id=resource_id,
resource_type=resource.type
)
except Exception as e:
LOG.error("Delete freezer backup resource failed, resource_type:"
"{0}, resource_id: {1}"
.format(resource.type, resource.id))
if job_id:
freezer_job_operation.delete(job_id)
raise exception.DeleteResourceFailed(
name="Freezer Backup FreezerTask",
reason=e,
resource_id=resource_id,
resource_type=resource.type
)
LOG.debug('Finish deleting freezer backup resource')
bank_section.delete_object('metadata')
bank_section.update_object('status',
constants.RESOURCE_STATUS_DELETED)
freezer_job_operation.delete(job_id)
class FreezerProtectionPlugin(protection_plugin.ProtectionPlugin):
_SUPPORT_RESOURCE_TYPES = [constants.VOLUME_RESOURCE_TYPE]
def __init__(self, config=None):
super(FreezerProtectionPlugin, self).__init__(config)
self._config.register_opts(freezer_backup_opts,
'freezer_protection_plugin')
self._plugin_config = self._config.freezer_protection_plugin
self._poll_interval = self._plugin_config.poll_interval
self._scheduler_client_id = self._plugin_config.scheduler_client_id
self._freezer_storage = FreezerStorage(
storage_type=self._plugin_config.storage,
storage_path=self._plugin_config.container,
endpoint=self._plugin_config.endpoint,
access_key=self._plugin_config.access_key,
secret_key=self._plugin_config.secret_key,
ssh_key=self._plugin_config.ssh_key,
ssh_port=self._plugin_config.ssh_port,
ssh_username=self._plugin_config.ssh_username,
ssh_host=self._plugin_config.ssh_host
)
@classmethod
def get_supported_resources_types(cls):
return cls._SUPPORT_RESOURCE_TYPES
@classmethod
def get_options_schema(cls, resource_type):
return volume_freezer_plugin_schemas.OPTIONS_SCHEMA
@classmethod
def get_restore_schema(cls, resource_type):
return volume_freezer_plugin_schemas.RESTORE_SCHEMA
@classmethod
def get_saved_info_schema(cls, resource_type):
return volume_freezer_plugin_schemas.SAVED_INFO_SCHEMA
@classmethod
def get_saved_info(cls, metadata_store, resource):
pass
def get_protect_operation(self, resource):
return ProtectOperation(self._poll_interval,
self._freezer_storage,
self._scheduler_client_id
)
def get_restore_operation(self, resource):
return RestoreOperation(self._poll_interval)
def get_delete_operation(self, resource):
return DeleteOperation(self._poll_interval)

View File

@ -0,0 +1,51 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
OPTIONS_SCHEMA = {
"title": "Freezer Protection Options",
"type": "object",
"properties": {
"backup_name": {
"type": "string",
"title": "Backup Name",
"description": "The name of the backup.",
"default": None
},
"description": {
"type": "string",
"title": "Description",
"description": "The description of the backup."
}
},
"required": ["backup_name"]
}
RESTORE_SCHEMA = {
"title": "Freezer Protection Restore",
"type": "object",
"properties": {
"restore_name": {
"type": "string",
"title": "Restore Resource Name",
"description": "The name of the restore resource ",
"default": None
},
},
"required": ["restore_name"]
}
SAVED_INFO_SCHEMA = {
"title": "Freezer Protection Saved Info",
"type": "object",
"properties": {},
"required": []
}

View File

@ -0,0 +1,190 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
from karbor.common import constants
from karbor.context import RequestContext
from karbor.resource import Resource
from karbor.services.protection.bank_plugin import Bank
from karbor.services.protection.bank_plugin import BankPlugin
from karbor.services.protection.bank_plugin import BankSection
from karbor.services.protection.protection_plugins.volume import \
volume_freezer_plugin_schemas
from karbor.services.protection.protection_plugins.volume.\
volume_freezer_plugin import FreezerProtectionPlugin
from karbor.tests import base
import mock
from oslo_config import cfg
from oslo_config import fixture
class FakeBankPlugin(BankPlugin):
def update_object(self, key, value):
return
def get_object(self, key):
return
def list_objects(self, prefix=None, limit=None, marker=None,
sort_dir=None):
return
def delete_object(self, key):
return
def get_owner_id(self):
return
fake_bank = Bank(FakeBankPlugin())
fake_bank_section = BankSection(bank=fake_bank, section="fake")
ResourceNode = collections.namedtuple(
"ResourceNode",
["value",
"child_nodes"]
)
Job = collections.namedtuple(
"Job",
["job_schedule"]
)
def call_hooks(operation, checkpoint, resource, context, parameters, **kwargs):
def noop(*args, **kwargs):
pass
hooks = (
'on_prepare_begin',
'on_prepare_finish',
'on_main',
'on_complete',
)
for hook_name in hooks:
hook = getattr(operation, hook_name, noop)
hook(checkpoint, resource, context, parameters, **kwargs)
class FakeCheckpoint(object):
def __init__(self):
self.bank_section = fake_bank_section
self.id = "fake_id"
def get_resource_bank_section(self, resource_id):
return self.bank_section
class VolumeFreezerProtectionPluginTest(base.TestCase):
def setUp(self):
super(VolumeFreezerProtectionPluginTest, self).setUp()
plugin_config = cfg.ConfigOpts()
plugin_config_fixture = self.useFixture(fixture.Config(plugin_config))
plugin_config_fixture.load_raw_values(
group='volume_freezer_plugin',
poll_interval=0,
)
self.plugin = FreezerProtectionPlugin(plugin_config)
self.cntxt = RequestContext(user_id='demo',
project_id='fake_project_id',
auth_token='fake_token')
self.freezer_client = mock.MagicMock()
self.checkpoint = FakeCheckpoint()
def test_get_options_schema(self):
options_schema = self.plugin.get_options_schema(
constants.VOLUME_RESOURCE_TYPE)
self.assertEqual(options_schema,
volume_freezer_plugin_schemas.OPTIONS_SCHEMA)
def test_get_restore_schema(self):
options_schema = self.plugin.get_restore_schema(
constants.VOLUME_RESOURCE_TYPE)
self.assertEqual(options_schema,
volume_freezer_plugin_schemas.RESTORE_SCHEMA)
def test_get_saved_info_schema(self):
options_schema = self.plugin.get_saved_info_schema(
constants.VOLUME_RESOURCE_TYPE)
self.assertEqual(options_schema,
volume_freezer_plugin_schemas.SAVED_INFO_SCHEMA)
@mock.patch('karbor.services.protection.protection_plugins.volume.'
'volume_freezer_plugin.utils.status_poll')
@mock.patch('karbor.services.protection.clients.freezer.create')
def test_create_backup(self, mock_freezer_create, mock_status_poll):
resource = Resource(id="123",
type=constants.VOLUME_RESOURCE_TYPE,
name='fake')
fake_bank_section.update_object = mock.MagicMock()
protect_operation = self.plugin.get_protect_operation(resource)
mock_freezer_create.return_value = self.freezer_client
self.freezer_client.clients.list = mock.MagicMock()
self.freezer_client.clients.list.return_value = [
{
'cliend_id': 'fake_client_id'
}
]
self.freezer_client.jobs.create = mock.MagicMock()
self.freezer_client.jobs.create.return_value = "123"
self.freezer_client.jobs.delete = mock.MagicMock()
mock_status_poll.return_value = True
call_hooks(protect_operation, self.checkpoint, resource, self.cntxt,
{})
@mock.patch('karbor.services.protection.protection_plugins.volume.'
'volume_freezer_plugin.utils.status_poll')
@mock.patch('karbor.services.protection.clients.freezer.create')
def test_delete_backup(self, mock_freezer_create, mock_status_poll):
resource = Resource(id="123",
type=constants.VOLUME_RESOURCE_TYPE,
name='fake')
delete_operation = self.plugin.get_delete_operation(resource)
fake_bank_section.update_object = mock.MagicMock()
fake_bank_section.get_object = mock.MagicMock()
fake_bank_section.get_object.return_value = {
'job_info': {
'description': '123',
'job_actions': [{
'freezer_action': {
'backup_name': 'test',
'action': 'backup',
'mode': 'cinder',
'cinder_vol_id': 'test',
'storage': 'swift',
'container': 'karbor/123'
}
}]
}
}
mock_freezer_create.return_value = self.freezer_client
self.freezer_client.jobs.create = mock.MagicMock()
self.freezer_client.jobs.create.return_value = '321'
self.freezer_client.jobs.delete = mock.MagicMock()
mock_status_poll.return_value = True
call_hooks(delete_operation, self.checkpoint, resource, self.cntxt,
{})
def test_get_supported_resources_types(self):
types = self.plugin.get_supported_resources_types()
self.assertEqual(types,
[constants.VOLUME_RESOURCE_TYPE])

View File

@ -26,6 +26,7 @@ oslo.service>=1.10.0 # Apache-2.0
oslo.versionedobjects>=1.17.0 # Apache-2.0
Paste # MIT
PasteDeploy>=1.5.0 # MIT
python-freezerclient>=1.3.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0
python-cinderclient>=3.2.0 # Apache-2.0

View File

@ -38,6 +38,7 @@ karbor.protections =
karbor-swift-bank-plugin = karbor.services.protection.bank_plugins.swift_bank_plugin:SwiftBankPlugin
karbor-fs-bank-plugin = karbor.services.protection.bank_plugins.file_system_bank_plugin:FileSystemBankPlugin
karbor-s3-bank-plugin = karbor.services.protection.bank_plugins.s3_bank_plugin:S3BankPlugin
karbor-volume-freezer-plugin = karbor.services.protection.protection_plugins.volume.volume_freezer_plugin:FreezerProtectionPlugin
karbor-volume-protection-plugin = karbor.services.protection.protection_plugins.volume.cinder_protection_plugin:CinderBackupProtectionPlugin
karbor-volume-snapshot-plugin = karbor.services.protection.protection_plugins.volume.volume_snapshot_plugin:VolumeSnapshotProtectionPlugin
karbor-image-protection-plugin = karbor.services.protection.protection_plugins.image.image_protection_plugin:GlanceProtectionPlugin

View File

@ -17,6 +17,7 @@ python-swiftclient>=3.2.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0
python-cinderclient>=3.2.0 # Apache-2.0
python-freezerclient>=1.3.0 # Apache-2.0
python-karborclient>=0.6.0 # Apache-2.0
python-neutronclient>=6.3.0 # Apache-2.0
python-troveclient>=2.2.0 # Apache-2.0