Removed backups
This commit is contained in:
parent
1dde8fb5cf
commit
bb79e54fa1
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Starter script for manila Volume Backup."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import eventlet
|
||||
|
||||
eventlet.monkey_patch()
|
||||
|
||||
# If ../manila/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'manila', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from manila.openstack.common import gettextutils
|
||||
gettextutils.install('manila')
|
||||
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
from manila import service
|
||||
from manila import utils
|
||||
|
||||
if __name__ == '__main__':
|
||||
flags.parse_args(sys.argv)
|
||||
logging.setup("manila")
|
||||
utils.monkey_patch()
|
||||
launcher = service.ProcessLauncher()
|
||||
server = service.Service.create(binary='manila-backup')
|
||||
launcher.launch_server(server)
|
||||
launcher.wait()
|
@ -645,42 +645,6 @@ class GetLogCommands(object):
|
||||
print "No manila entries in syslog!"
|
||||
|
||||
|
||||
class BackupCommands(object):
|
||||
"""Methods for managing backups."""
|
||||
|
||||
def list(self):
|
||||
"""List all backups (including ones in progress) and the host
|
||||
on which the backup operation is running."""
|
||||
ctxt = context.get_admin_context()
|
||||
backups = db.backup_get_all(ctxt)
|
||||
|
||||
hdr = "%-32s\t%-32s\t%-32s\t%-24s\t%-24s\t%-12s\t%-12s\t%-12s\t%-12s"
|
||||
print hdr % (_('ID'),
|
||||
_('User ID'),
|
||||
_('Project ID'),
|
||||
_('Host'),
|
||||
_('Name'),
|
||||
_('Container'),
|
||||
_('Status'),
|
||||
_('Size'),
|
||||
_('Object Count'))
|
||||
|
||||
res = "%-32s\t%-32s\t%-32s\t%-24s\t%-24s\t%-12s\t%-12s\t%-12d\t%-12d"
|
||||
for backup in backups:
|
||||
object_count = 0
|
||||
if backup['object_count'] is not None:
|
||||
object_count = backup['object_count']
|
||||
print res % (backup['id'],
|
||||
backup['user_id'],
|
||||
backup['project_id'],
|
||||
backup['host'],
|
||||
backup['display_name'],
|
||||
backup['container'],
|
||||
backup['status'],
|
||||
backup['size'],
|
||||
object_count)
|
||||
|
||||
|
||||
class ServiceCommands(object):
|
||||
"""Methods for managing services."""
|
||||
def list(self):
|
||||
|
@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Starter script for manila Volume."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# If ../manila/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'manila', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from manila.openstack.common import gettextutils
|
||||
gettextutils.install('manila')
|
||||
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
from manila import service
|
||||
from manila import utils
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
if __name__ == '__main__':
|
||||
flags.parse_args(sys.argv)
|
||||
logging.setup("manila")
|
||||
utils.monkey_patch()
|
||||
launcher = service.ProcessLauncher()
|
||||
if FLAGS.enabled_backends:
|
||||
for backend in FLAGS.enabled_backends:
|
||||
host = "%s@%s" % (FLAGS.host, backend)
|
||||
server = service.Service.create(
|
||||
host=host,
|
||||
service_name=backend)
|
||||
launcher.launch_server(server)
|
||||
else:
|
||||
server = service.Service.create(binary='manila-volume')
|
||||
launcher.launch_server(server)
|
||||
launcher.wait()
|
@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 OpenStack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Cron script to generate usage notifications for volumes existing during
|
||||
the audit period.
|
||||
|
||||
Together with the notifications generated by volumes
|
||||
create/delete/resize, over that time period, this allows an external
|
||||
system consuming usage notification feeds to calculate volume usage
|
||||
for each tenant.
|
||||
|
||||
Time periods are specified as 'hour', 'month', 'day' or 'year'
|
||||
|
||||
hour = previous hour. If run at 9:07am, will generate usage for 8-9am.
|
||||
month = previous month. If the script is run April 1, it will generate
|
||||
usages for March 1 through March 31.
|
||||
day = previous day. if run on July 4th, it generates usages for July 3rd.
|
||||
year = previous year. If run on Jan 1, it generates usages for
|
||||
Jan 1 through Dec 31 of the previous year.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# If ../manila/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'manila', '__init__.py')):
|
||||
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||
|
||||
from manila.openstack.common import gettextutils
|
||||
gettextutils.install('manila')
|
||||
|
||||
from manila import context
|
||||
from manila import db
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
from manila.openstack.common import rpc
|
||||
from manila import utils
|
||||
import manila.volume.utils
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
if __name__ == '__main__':
|
||||
admin_context = context.get_admin_context()
|
||||
flags.parse_args(sys.argv)
|
||||
logging.setup("manila")
|
||||
begin, end = utils.last_completed_audit_period()
|
||||
print _("Starting volume usage audit")
|
||||
msg = _("Creating usages for %(begin_period)s until %(end_period)s")
|
||||
print (msg % {"begin_period": str(begin), "end_period": str(end)})
|
||||
|
||||
extra_info = {
|
||||
'audit_period_beginning': str(begin),
|
||||
'audit_period_ending': str(end),
|
||||
}
|
||||
|
||||
volumes = db.volume_get_active_by_window(admin_context,
|
||||
begin,
|
||||
end)
|
||||
print _("Found %d volumes") % len(volumes)
|
||||
for volume_ref in volumes:
|
||||
try:
|
||||
manila.volume.utils.notify_usage_exists(
|
||||
admin_context, volume_ref)
|
||||
except Exception, e:
|
||||
print traceback.format_exc(e)
|
||||
|
||||
snapshots = db.snapshot_get_active_by_window(admin_context,
|
||||
begin,
|
||||
end)
|
||||
print _("Found %d snapshots") % len(snapshots)
|
||||
for snapshot_ref in snapshots:
|
||||
try:
|
||||
manila.volume.utils.notify_about_snapshot_usage(admin_context,
|
||||
snapshot_ref,
|
||||
'exists',
|
||||
extra_info)
|
||||
except Exception, e:
|
||||
print traceback.fromat_exc(e)
|
||||
|
||||
print _("Volume usage audit completed")
|
@ -1,278 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""The backups api."""
|
||||
|
||||
import webob
|
||||
from webob import exc
|
||||
from xml.dom import minidom
|
||||
|
||||
from manila.api import common
|
||||
from manila.api import extensions
|
||||
from manila.api.openstack import wsgi
|
||||
from manila.api.views import backups as backup_views
|
||||
from manila.api import xmlutil
|
||||
from manila import backup as backupAPI
|
||||
from manila import exception
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_backup(elem):
|
||||
elem.set('id')
|
||||
elem.set('status')
|
||||
elem.set('size')
|
||||
elem.set('container')
|
||||
elem.set('volume_id')
|
||||
elem.set('object_count')
|
||||
elem.set('availability_zone')
|
||||
elem.set('created_at')
|
||||
elem.set('name')
|
||||
elem.set('description')
|
||||
elem.set('fail_reason')
|
||||
|
||||
|
||||
def make_backup_restore(elem):
|
||||
elem.set('backup_id')
|
||||
elem.set('volume_id')
|
||||
|
||||
|
||||
class BackupTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('backup', selector='backup')
|
||||
make_backup(root)
|
||||
alias = Backups.alias
|
||||
namespace = Backups.namespace
|
||||
return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class BackupsTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('backups')
|
||||
elem = xmlutil.SubTemplateElement(root, 'backup', selector='backups')
|
||||
make_backup(elem)
|
||||
alias = Backups.alias
|
||||
namespace = Backups.namespace
|
||||
return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class BackupRestoreTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('restore', selector='restore')
|
||||
make_backup_restore(root)
|
||||
alias = Backups.alias
|
||||
namespace = Backups.namespace
|
||||
return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class CreateDeserializer(wsgi.MetadataXMLDeserializer):
|
||||
def default(self, string):
|
||||
dom = minidom.parseString(string)
|
||||
backup = self._extract_backup(dom)
|
||||
return {'body': {'backup': backup}}
|
||||
|
||||
def _extract_backup(self, node):
|
||||
backup = {}
|
||||
backup_node = self.find_first_child_named(node, 'backup')
|
||||
|
||||
attributes = ['container', 'display_name',
|
||||
'display_description', 'volume_id']
|
||||
|
||||
for attr in attributes:
|
||||
if backup_node.getAttribute(attr):
|
||||
backup[attr] = backup_node.getAttribute(attr)
|
||||
return backup
|
||||
|
||||
|
||||
class RestoreDeserializer(wsgi.MetadataXMLDeserializer):
|
||||
def default(self, string):
|
||||
dom = minidom.parseString(string)
|
||||
restore = self._extract_restore(dom)
|
||||
return {'body': {'restore': restore}}
|
||||
|
||||
def _extract_restore(self, node):
|
||||
restore = {}
|
||||
restore_node = self.find_first_child_named(node, 'restore')
|
||||
if restore_node.getAttribute('volume_id'):
|
||||
restore['volume_id'] = restore_node.getAttribute('volume_id')
|
||||
return restore
|
||||
|
||||
|
||||
class BackupsController(wsgi.Controller):
|
||||
"""The Backups API controller for the OpenStack API."""
|
||||
|
||||
_view_builder_class = backup_views.ViewBuilder
|
||||
|
||||
def __init__(self):
|
||||
self.backup_api = backupAPI.API()
|
||||
super(BackupsController, self).__init__()
|
||||
|
||||
@wsgi.serializers(xml=BackupTemplate)
|
||||
def show(self, req, id):
|
||||
"""Return data about the given backup."""
|
||||
LOG.debug(_('show called for member %s'), id)
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
backup = self.backup_api.get(context, backup_id=id)
|
||||
except exception.BackupNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=unicode(error))
|
||||
|
||||
return self._view_builder.detail(req, backup)
|
||||
|
||||
def delete(self, req, id):
|
||||
"""Delete a backup."""
|
||||
LOG.debug(_('delete called for member %s'), id)
|
||||
context = req.environ['manila.context']
|
||||
|
||||
LOG.audit(_('Delete backup with id: %s'), id, context=context)
|
||||
|
||||
try:
|
||||
self.backup_api.delete(context, id)
|
||||
except exception.BackupNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=unicode(error))
|
||||
except exception.InvalidBackup as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.serializers(xml=BackupsTemplate)
|
||||
def index(self, req):
|
||||
"""Returns a summary list of backups."""
|
||||
return self._get_backups(req, is_detail=False)
|
||||
|
||||
@wsgi.serializers(xml=BackupsTemplate)
|
||||
def detail(self, req):
|
||||
"""Returns a detailed list of backups."""
|
||||
return self._get_backups(req, is_detail=True)
|
||||
|
||||
def _get_backups(self, req, is_detail):
|
||||
"""Returns a list of backups, transformed through view builder."""
|
||||
context = req.environ['manila.context']
|
||||
backups = self.backup_api.get_all(context)
|
||||
limited_list = common.limited(backups, req)
|
||||
|
||||
if is_detail:
|
||||
backups = self._view_builder.detail_list(req, limited_list)
|
||||
else:
|
||||
backups = self._view_builder.summary_list(req, limited_list)
|
||||
return backups
|
||||
|
||||
# TODO(frankm): Add some checks here including
|
||||
# - whether requested volume_id exists so we can return some errors
|
||||
# immediately
|
||||
# - maybe also do validation of swift container name
|
||||
@wsgi.response(202)
|
||||
@wsgi.serializers(xml=BackupTemplate)
|
||||
@wsgi.deserializers(xml=CreateDeserializer)
|
||||
def create(self, req, body):
|
||||
"""Create a new backup."""
|
||||
LOG.debug(_('Creating new backup %s'), body)
|
||||
if not self.is_valid_body(body, 'backup'):
|
||||
raise exc.HTTPBadRequest()
|
||||
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
backup = body['backup']
|
||||
volume_id = backup['volume_id']
|
||||
except KeyError:
|
||||
msg = _("Incorrect request body format")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
container = backup.get('container', None)
|
||||
name = backup.get('name', None)
|
||||
description = backup.get('description', None)
|
||||
|
||||
LOG.audit(_("Creating backup of volume %(volume_id)s in container"
|
||||
" %(container)s"), locals(), context=context)
|
||||
|
||||
try:
|
||||
new_backup = self.backup_api.create(context, name, description,
|
||||
volume_id, container)
|
||||
except exception.InvalidVolume as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
except exception.VolumeNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=unicode(error))
|
||||
|
||||
retval = self._view_builder.summary(req, dict(new_backup.iteritems()))
|
||||
return retval
|
||||
|
||||
@wsgi.response(202)
|
||||
@wsgi.serializers(xml=BackupRestoreTemplate)
|
||||
@wsgi.deserializers(xml=RestoreDeserializer)
|
||||
def restore(self, req, id, body):
|
||||
"""Restore an existing backup to a volume."""
|
||||
backup_id = id
|
||||
LOG.debug(_('Restoring backup %(backup_id)s (%(body)s)') % locals())
|
||||
if not self.is_valid_body(body, 'restore'):
|
||||
raise exc.HTTPBadRequest()
|
||||
|
||||
context = req.environ['manila.context']
|
||||
|
||||
try:
|
||||
restore = body['restore']
|
||||
except KeyError:
|
||||
msg = _("Incorrect request body format")
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
volume_id = restore.get('volume_id', None)
|
||||
|
||||
LOG.audit(_("Restoring backup %(backup_id)s to volume %(volume_id)s"),
|
||||
locals(), context=context)
|
||||
|
||||
try:
|
||||
new_restore = self.backup_api.restore(context,
|
||||
backup_id=backup_id,
|
||||
volume_id=volume_id)
|
||||
except exception.InvalidInput as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
except exception.InvalidVolume as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
except exception.InvalidBackup as error:
|
||||
raise exc.HTTPBadRequest(explanation=unicode(error))
|
||||
except exception.BackupNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=unicode(error))
|
||||
except exception.VolumeNotFound as error:
|
||||
raise exc.HTTPNotFound(explanation=unicode(error))
|
||||
except exception.VolumeSizeExceedsAvailableQuota as error:
|
||||
raise exc.HTTPRequestEntityTooLarge(
|
||||
explanation=error.message, headers={'Retry-After': 0})
|
||||
except exception.VolumeLimitExceeded as error:
|
||||
raise exc.HTTPRequestEntityTooLarge(
|
||||
explanation=error.message, headers={'Retry-After': 0})
|
||||
|
||||
retval = self._view_builder.restore_summary(
|
||||
req, dict(new_restore.iteritems()))
|
||||
return retval
|
||||
|
||||
|
||||
class Backups(extensions.ExtensionDescriptor):
|
||||
"""Backups support."""
|
||||
|
||||
name = 'Backups'
|
||||
alias = 'backups'
|
||||
namespace = 'http://docs.openstack.org/volume/ext/backups/api/v1'
|
||||
updated = '2012-12-12T00:00:00+00:00'
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
res = extensions.ResourceExtension(
|
||||
Backups.alias, BackupsController(),
|
||||
collection_actions={'detail': 'GET'},
|
||||
member_actions={'restore': 'POST'})
|
||||
resources.append(res)
|
||||
return resources
|
@ -1,90 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 manila.api import common
|
||||
from manila.openstack.common import log as logging
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ViewBuilder(common.ViewBuilder):
|
||||
"""Model backup API responses as a python dictionary."""
|
||||
|
||||
_collection_name = "backups"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize view builder."""
|
||||
super(ViewBuilder, self).__init__()
|
||||
|
||||
def summary_list(self, request, backups):
|
||||
"""Show a list of backups without many details."""
|
||||
return self._list_view(self.summary, request, backups)
|
||||
|
||||
def detail_list(self, request, backups):
|
||||
"""Detailed view of a list of backups ."""
|
||||
return self._list_view(self.detail, request, backups)
|
||||
|
||||
def summary(self, request, backup):
|
||||
"""Generic, non-detailed view of a backup."""
|
||||
return {
|
||||
'backup': {
|
||||
'id': backup['id'],
|
||||
'name': backup['display_name'],
|
||||
'links': self._get_links(request,
|
||||
backup['id']),
|
||||
},
|
||||
}
|
||||
|
||||
def restore_summary(self, request, restore):
|
||||
"""Generic, non-detailed view of a restore."""
|
||||
return {
|
||||
'restore': {
|
||||
'backup_id': restore['backup_id'],
|
||||
'volume_id': restore['volume_id'],
|
||||
},
|
||||
}
|
||||
|
||||
def detail(self, request, backup):
|
||||
"""Detailed view of a single backup."""
|
||||
return {
|
||||
'backup': {
|
||||
'id': backup.get('id'),
|
||||
'status': backup.get('status'),
|
||||
'size': backup.get('size'),
|
||||
'object_count': backup.get('object_count'),
|
||||
'availability_zone': backup.get('availability_zone'),
|
||||
'container': backup.get('container'),
|
||||
'created_at': backup.get('created_at'),
|
||||
'name': backup.get('display_name'),
|
||||
'description': backup.get('display_description'),
|
||||
'fail_reason': backup.get('fail_reason'),
|
||||
'volume_id': backup.get('volume_id'),
|
||||
'links': self._get_links(request, backup['id'])
|
||||
}
|
||||
}
|
||||
|
||||
def _list_view(self, func, request, backups):
|
||||
"""Provide a view for a list of backups."""
|
||||
backups_list = [func(request, backup)['backup'] for backup in backups]
|
||||
backups_links = self._get_collection_links(request,
|
||||
backups,
|
||||
self._collection_name)
|
||||
backups_dict = dict(backups=backups_list)
|
||||
|
||||
if backups_links:
|
||||
backups_dict['backups_links'] = backups_links
|
||||
|
||||
return backups_dict
|
@ -1,23 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# Importing full names to not pollute the namespace and cause possible
|
||||
# collisions with use of 'from manila.backup import <foo>' elsewhere.
|
||||
|
||||
import manila.flags
|
||||
import manila.openstack.common.importutils
|
||||
|
||||
API = manila.openstack.common.importutils.import_class(
|
||||
manila.flags.FLAGS.backup_api_class)
|
@ -1,171 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Handles all requests relating to the volume backups service.
|
||||
"""
|
||||
|
||||
from eventlet import greenthread
|
||||
|
||||
from manila.backup import rpcapi as backup_rpcapi
|
||||
from manila.db import base
|
||||
from manila import exception
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
import manila.volume
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class API(base.Base):
|
||||
"""API for interacting with the volume backup manager."""
|
||||
|
||||
def __init__(self, db_driver=None):
|
||||
self.backup_rpcapi = backup_rpcapi.BackupAPI()
|
||||
self.volume_api = manila.volume.API()
|
||||
super(API, self).__init__(db_driver)
|
||||
|
||||
def get(self, context, backup_id):
|
||||
rv = self.db.backup_get(context, backup_id)
|
||||
return dict(rv.iteritems())
|
||||
|
||||
def delete(self, context, backup_id):
|
||||
"""
|
||||
Make the RPC call to delete a volume backup.
|
||||
"""
|
||||
backup = self.get(context, backup_id)
|
||||
if backup['status'] not in ['available', 'error']:
|
||||
msg = _('Backup status must be available or error')
|
||||
raise exception.InvalidBackup(reason=msg)
|
||||
|
||||
self.db.backup_update(context, backup_id, {'status': 'deleting'})
|
||||
self.backup_rpcapi.delete_backup(context,
|
||||
backup['host'],
|
||||
backup['id'])
|
||||
|
||||
# TODO(moorehef): Add support for search_opts, discarded atm
|
||||
def get_all(self, context, search_opts={}):
|
||||
if context.is_admin:
|
||||
backups = self.db.backup_get_all(context)
|
||||
else:
|
||||
backups = self.db.backup_get_all_by_project(context,
|
||||
context.project_id)
|
||||
|
||||
return backups
|
||||
|
||||
def create(self, context, name, description, volume_id,
|
||||
container, availability_zone=None):
|
||||
"""
|
||||
Make the RPC call to create a volume backup.
|
||||
"""
|
||||
volume = self.volume_api.get(context, volume_id)
|
||||
if volume['status'] != "available":
|
||||
msg = _('Volume to be backed up must be available')
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
self.db.volume_update(context, volume_id, {'status': 'backing-up'})
|
||||
|
||||
options = {'user_id': context.user_id,
|
||||
'project_id': context.project_id,
|
||||
'display_name': name,
|
||||
'display_description': description,
|
||||
'volume_id': volume_id,
|
||||
'status': 'creating',
|
||||
'container': container,
|
||||
'size': volume['size'],
|
||||
# TODO(DuncanT): This will need de-managling once
|
||||
# multi-backend lands
|
||||
'host': volume['host'], }
|
||||
|
||||
backup = self.db.backup_create(context, options)
|
||||
|
||||
#TODO(DuncanT): In future, when we have a generic local attach,
|
||||
# this can go via the scheduler, which enables
|
||||
# better load ballancing and isolation of services
|
||||
self.backup_rpcapi.create_backup(context,
|
||||
backup['host'],
|
||||
backup['id'],
|
||||
volume_id)
|
||||
|
||||
return backup
|
||||
|
||||
def restore(self, context, backup_id, volume_id=None):
|
||||
"""
|
||||
Make the RPC call to restore a volume backup.
|
||||
"""
|
||||
backup = self.get(context, backup_id)
|
||||
if backup['status'] != 'available':
|
||||
msg = _('Backup status must be available')
|
||||
raise exception.InvalidBackup(reason=msg)
|
||||
|
||||
size = backup['size']
|
||||
if size is None:
|
||||
msg = _('Backup to be restored has invalid size')
|
||||
raise exception.InvalidBackup(reason=msg)
|
||||
|
||||
# Create a volume if none specified. If a volume is specified check
|
||||
# it is large enough for the backup
|
||||
if volume_id is None:
|
||||
name = 'restore_backup_%s' % backup_id
|
||||
description = 'auto-created_from_restore_from_swift'
|
||||
|
||||
LOG.audit(_("Creating volume of %(size)s GB for restore of "
|
||||
"backup %(backup_id)s"), locals(), context=context)
|
||||
volume = self.volume_api.create(context, size, name, description)
|
||||
volume_id = volume['id']
|
||||
|
||||
while True:
|
||||
volume = self.volume_api.get(context, volume_id)
|
||||
if volume['status'] != 'creating':
|
||||
break
|
||||
greenthread.sleep(1)
|
||||
else:
|
||||
volume = self.volume_api.get(context, volume_id)
|
||||
volume_size = volume['size']
|
||||
if volume_size < size:
|
||||
err = _('volume size %(volume_size)d is too small to restore '
|
||||
'backup of size %(size)d.') % locals()
|
||||
raise exception.InvalidVolume(reason=err)
|
||||
|
||||
if volume['status'] != "available":
|
||||
msg = _('Volume to be restored to must be available')
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
|
||||
LOG.debug('Checking backup size %s against volume size %s',
|
||||
size, volume['size'])
|
||||
if size > volume['size']:
|
||||
msg = _('Volume to be restored to is smaller '
|
||||
'than the backup to be restored')
|
||||
raise exception.InvalidVolume(reason=msg)
|
||||
|
||||
LOG.audit(_("Overwriting volume %(volume_id)s with restore of "
|
||||
"backup %(backup_id)s"), locals(), context=context)
|
||||
|
||||
# Setting the status here rather than setting at start and unrolling
|
||||
# for each error condition, it should be a very small window
|
||||
self.db.backup_update(context, backup_id, {'status': 'restoring'})
|
||||
self.db.volume_update(context, volume_id, {'status':
|
||||
'restoring-backup'})
|
||||
self.backup_rpcapi.restore_backup(context,
|
||||
backup['host'],
|
||||
backup['id'],
|
||||
volume_id)
|
||||
|
||||
d = {'backup_id': backup_id,
|
||||
'volume_id': volume_id, }
|
||||
|
||||
return d
|
@ -1,264 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Backup manager manages volume backups.
|
||||
|
||||
Volume Backups are full copies of persistent volumes stored in Swift object
|
||||
storage. They are usable without the original object being available. A
|
||||
volume backup can be restored to the original volume it was created from or
|
||||
any other available volume with a minimum size of the original volume.
|
||||
Volume backups can be created, restored, deleted and listed.
|
||||
|
||||
**Related Flags**
|
||||
|
||||
:backup_topic: What :mod:`rpc` topic to listen to (default:
|
||||
`manila-backup`).
|
||||
:backup_manager: The module name of a class derived from
|
||||
:class:`manager.Manager` (default:
|
||||
:class:`manila.backup.manager.Manager`).
|
||||
|
||||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila import flags
|
||||
from manila import manager
|
||||
from manila.openstack.common import excutils
|
||||
from manila.openstack.common import importutils
|
||||
from manila.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
backup_manager_opts = [
|
||||
cfg.StrOpt('backup_service',
|
||||
default='manila.backup.services.swift',
|
||||
help='Service to use for backups.'),
|
||||
]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(backup_manager_opts)
|
||||
|
||||
|
||||
class BackupManager(manager.SchedulerDependentManager):
|
||||
"""Manages backup of block storage devices."""
|
||||
|
||||
RPC_API_VERSION = '1.0'
|
||||
|
||||
def __init__(self, service_name=None, *args, **kwargs):
|
||||
self.service = importutils.import_module(FLAGS.backup_service)
|
||||
self.az = FLAGS.storage_availability_zone
|
||||
self.volume_manager = importutils.import_object(FLAGS.volume_manager)
|
||||
self.driver = self.volume_manager.driver
|
||||
super(BackupManager, self).__init__(service_name='backup',
|
||||
*args, **kwargs)
|
||||
self.driver.db = self.db
|
||||
|
||||
def init_host(self):
|
||||
"""Do any initialization that needs to be run if this is a
|
||||
standalone service."""
|
||||
|
||||
ctxt = context.get_admin_context()
|
||||
self.driver.do_setup(ctxt)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
LOG.info(_("Cleaning up incomplete backup operations"))
|
||||
volumes = self.db.volume_get_all_by_host(ctxt, self.host)
|
||||
for volume in volumes:
|
||||
if volume['status'] == 'backing-up':
|
||||
LOG.info(_('Resetting volume %s to available '
|
||||
'(was backing-up)') % volume['id'])
|
||||
self.volume_manager.detach_volume(ctxt, volume['id'])
|
||||
if volume['status'] == 'restoring-backup':
|
||||
LOG.info(_('Resetting volume %s to error_restoring '
|
||||
'(was restoring-backup)') % volume['id'])
|
||||
self.volume_manager.detach_volume(ctxt, volume['id'])
|
||||
self.db.volume_update(ctxt, volume['id'],
|
||||
{'status': 'error_restoring'})
|
||||
|
||||
# TODO(smulcahy) implement full resume of backup and restore
|
||||
# operations on restart (rather than simply resetting)
|
||||
backups = self.db.backup_get_all_by_host(ctxt, self.host)
|
||||
for backup in backups:
|
||||
if backup['status'] == 'creating':
|
||||
LOG.info(_('Resetting backup %s to error '
|
||||
'(was creating)') % backup['id'])
|
||||
err = 'incomplete backup reset on manager restart'
|
||||
self.db.backup_update(ctxt, backup['id'], {'status': 'error',
|
||||
'fail_reason': err})
|
||||
if backup['status'] == 'restoring':
|
||||
LOG.info(_('Resetting backup %s to available '
|
||||
'(was restoring)') % backup['id'])
|
||||
self.db.backup_update(ctxt, backup['id'],
|
||||
{'status': 'available'})
|
||||
if backup['status'] == 'deleting':
|
||||
LOG.info(_('Resuming delete on backup: %s') % backup['id'])
|
||||
self.delete_backup(ctxt, backup['id'])
|
||||
|
||||
def create_backup(self, context, backup_id):
|
||||
"""
|
||||
Create volume backups using configured backup service.
|
||||
"""
|
||||
backup = self.db.backup_get(context, backup_id)
|
||||
volume_id = backup['volume_id']
|
||||
volume = self.db.volume_get(context, volume_id)
|
||||
LOG.info(_('create_backup started, backup: %(backup_id)s for '
|
||||
'volume: %(volume_id)s') % locals())
|
||||
self.db.backup_update(context, backup_id, {'host': self.host,
|
||||
'service':
|
||||
FLAGS.backup_service})
|
||||
|
||||
expected_status = 'backing-up'
|
||||
actual_status = volume['status']
|
||||
if actual_status != expected_status:
|
||||
err = _('create_backup aborted, expected volume status '
|
||||
'%(expected_status)s but got %(actual_status)s') % locals()
|
||||
self.db.backup_update(context, backup_id, {'status': 'error',
|
||||
'fail_reason': err})
|
||||
raise exception.InvalidVolume(reason=err)
|
||||
|
||||
expected_status = 'creating'
|
||||
actual_status = backup['status']
|
||||
if actual_status != expected_status:
|
||||
err = _('create_backup aborted, expected backup status '
|
||||
'%(expected_status)s but got %(actual_status)s') % locals()
|
||||
self.db.volume_update(context, volume_id, {'status': 'available'})
|
||||
self.db.backup_update(context, backup_id, {'status': 'error',
|
||||
'fail_reason': err})
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
try:
|
||||
backup_service = self.service.get_backup_service(context)
|
||||
self.driver.backup_volume(context, backup, backup_service)
|
||||
except Exception as err:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self.db.volume_update(context, volume_id,
|
||||
{'status': 'available'})
|
||||
self.db.backup_update(context, backup_id,
|
||||
{'status': 'error',
|
||||
'fail_reason': unicode(err)})
|
||||
|
||||
self.db.volume_update(context, volume_id, {'status': 'available'})
|
||||
self.db.backup_update(context, backup_id, {'status': 'available',
|
||||
'size': volume['size'],
|
||||
'availability_zone':
|
||||
self.az})
|
||||
LOG.info(_('create_backup finished. backup: %s'), backup_id)
|
||||
|
||||
def restore_backup(self, context, backup_id, volume_id):
|
||||
"""
|
||||
Restore volume backups from configured backup service.
|
||||
"""
|
||||
LOG.info(_('restore_backup started, restoring backup: %(backup_id)s'
|
||||
' to volume: %(volume_id)s') % locals())
|
||||
backup = self.db.backup_get(context, backup_id)
|
||||
volume = self.db.volume_get(context, volume_id)
|
||||
self.db.backup_update(context, backup_id, {'host': self.host})
|
||||
|
||||
expected_status = 'restoring-backup'
|
||||
actual_status = volume['status']
|
||||
if actual_status != expected_status:
|
||||
err = _('restore_backup aborted, expected volume status '
|
||||
'%(expected_status)s but got %(actual_status)s') % locals()
|
||||
self.db.backup_update(context, backup_id, {'status': 'available'})
|
||||
raise exception.InvalidVolume(reason=err)
|
||||
|
||||
expected_status = 'restoring'
|
||||
actual_status = backup['status']
|
||||
if actual_status != expected_status:
|
||||
err = _('restore_backup aborted, expected backup status '
|
||||
'%(expected_status)s but got %(actual_status)s') % locals()
|
||||
self.db.backup_update(context, backup_id, {'status': 'error',
|
||||
'fail_reason': err})
|
||||
self.db.volume_update(context, volume_id, {'status': 'error'})
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
if volume['size'] > backup['size']:
|
||||
LOG.warn('volume: %s, size: %d is larger than backup: %s, '
|
||||
'size: %d, continuing with restore',
|
||||
volume['id'], volume['size'],
|
||||
backup['id'], backup['size'])
|
||||
|
||||
backup_service = backup['service']
|
||||
configured_service = FLAGS.backup_service
|
||||
if backup_service != configured_service:
|
||||
err = _('restore_backup aborted, the backup service currently'
|
||||
' configured [%(configured_service)s] is not the'
|
||||
' backup service that was used to create this'
|
||||
' backup [%(backup_service)s]') % locals()
|
||||
self.db.backup_update(context, backup_id, {'status': 'available'})
|
||||
self.db.volume_update(context, volume_id, {'status': 'error'})
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
try:
|
||||
backup_service = self.service.get_backup_service(context)
|
||||
self.driver.restore_backup(context, backup, volume,
|
||||
backup_service)
|
||||
except Exception as err:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self.db.volume_update(context, volume_id,
|
||||
{'status': 'error_restoring'})
|
||||
self.db.backup_update(context, backup_id,
|
||||
{'status': 'available'})
|
||||
|
||||
self.db.volume_update(context, volume_id, {'status': 'available'})
|
||||
self.db.backup_update(context, backup_id, {'status': 'available'})
|
||||
LOG.info(_('restore_backup finished, backup: %(backup_id)s restored'
|
||||
' to volume: %(volume_id)s') % locals())
|
||||
|
||||
def delete_backup(self, context, backup_id):
|
||||
"""
|
||||
Delete volume backup from configured backup service.
|
||||
"""
|
||||
backup = self.db.backup_get(context, backup_id)
|
||||
LOG.info(_('delete_backup started, backup: %s'), backup_id)
|
||||
self.db.backup_update(context, backup_id, {'host': self.host})
|
||||
|
||||
expected_status = 'deleting'
|
||||
actual_status = backup['status']
|
||||
if actual_status != expected_status:
|
||||
err = _('delete_backup aborted, expected backup status '
|
||||
'%(expected_status)s but got %(actual_status)s') % locals()
|
||||
self.db.backup_update(context, backup_id, {'status': 'error',
|
||||
'fail_reason': err})
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
backup_service = backup['service']
|
||||
if backup_service is not None:
|
||||
configured_service = FLAGS.backup_service
|
||||
if backup_service != configured_service:
|
||||
err = _('delete_backup aborted, the backup service currently'
|
||||
' configured [%(configured_service)s] is not the'
|
||||
' backup service that was used to create this'
|
||||
' backup [%(backup_service)s]') % locals()
|
||||
self.db.backup_update(context, backup_id,
|
||||
{'status': 'error'})
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
try:
|
||||
backup_service = self.service.get_backup_service(context)
|
||||
backup_service.delete(backup)
|
||||
except Exception as err:
|
||||
with excutils.save_and_reraise_exception():
|
||||
self.db.backup_update(context, backup_id,
|
||||
{'status': 'error',
|
||||
'fail_reason':
|
||||
unicode(err)})
|
||||
|
||||
context = context.elevated()
|
||||
self.db.backup_destroy(context, backup_id)
|
||||
LOG.info(_('delete_backup finished, backup %s deleted'), backup_id)
|
@ -1,73 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Client side of the volume backup RPC API.
|
||||
"""
|
||||
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
from manila.openstack.common import rpc
|
||||
import manila.openstack.common.rpc.proxy
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class BackupAPI(manila.openstack.common.rpc.proxy.RpcProxy):
|
||||
'''Client side of the volume rpc API.
|
||||
|
||||
API version history:
|
||||
|
||||
1.0 - Initial version.
|
||||
'''
|
||||
|
||||
BASE_RPC_API_VERSION = '1.0'
|
||||
|
||||
def __init__(self):
|
||||
super(BackupAPI, self).__init__(
|
||||
topic=FLAGS.backup_topic,
|
||||
default_version=self.BASE_RPC_API_VERSION)
|
||||
|
||||
def create_backup(self, ctxt, host, backup_id, volume_id):
|
||||
LOG.debug("create_backup in rpcapi backup_id %s", backup_id)
|
||||
topic = rpc.queue_get_for(ctxt, self.topic, host)
|
||||
LOG.debug("create queue topic=%s", topic)
|
||||
self.cast(ctxt,
|
||||
self.make_msg('create_backup',
|
||||
backup_id=backup_id),
|
||||
topic=topic)
|
||||
|
||||
def restore_backup(self, ctxt, host, backup_id, volume_id):
|
||||
LOG.debug("restore_backup in rpcapi backup_id %s", backup_id)
|
||||
topic = rpc.queue_get_for(ctxt, self.topic, host)
|
||||
LOG.debug("restore queue topic=%s", topic)
|
||||
self.cast(ctxt,
|
||||
self.make_msg('restore_backup',
|
||||
backup_id=backup_id,
|
||||
volume_id=volume_id),
|
||||
topic=topic)
|
||||
|
||||
def delete_backup(self, ctxt, host, backup_id):
|
||||
LOG.debug("delete_backup rpcapi backup_id %s", backup_id)
|
||||
topic = rpc.queue_get_for(ctxt, self.topic, host)
|
||||
self.cast(ctxt,
|
||||
self.make_msg('delete_backup',
|
||||
backup_id=backup_id),
|
||||
topic=topic)
|
@ -1,14 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
@ -1,384 +0,0 @@
|
||||
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Implementation of a backup service that uses Swift as the backend
|
||||
|
||||
**Related Flags**
|
||||
|
||||
:backup_swift_url: The URL of the Swift endpoint (default:
|
||||
localhost:8080).
|
||||
:backup_swift_object_size: The size in bytes of the Swift objects used
|
||||
for volume backups (default: 52428800).
|
||||
:backup_swift_retry_attempts: The number of retries to make for Swift
|
||||
operations (default: 10).
|
||||
:backup_swift_retry_backoff: The backoff time in seconds between retrying
|
||||
failed Swift operations (default: 10).
|
||||
:backup_compression_algorithm: Compression algorithm to use for volume
|
||||
backups. Supported options are:
|
||||
None (to disable), zlib and bz2 (default: zlib)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import StringIO
|
||||
|
||||
import eventlet
|
||||
from oslo.config import cfg
|
||||
|
||||
from manila.db import base
|
||||
from manila import exception
|
||||
from manila import flags
|
||||
from manila.openstack.common import log as logging
|
||||
from manila.openstack.common import timeutils
|
||||
from swiftclient import client as swift
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
swiftbackup_service_opts = [
|
||||
cfg.StrOpt('backup_swift_url',
|
||||
default='http://localhost:8080/v1/AUTH_',
|
||||
help='The URL of the Swift endpoint'),
|
||||
cfg.StrOpt('backup_swift_container',
|
||||
default='volumebackups',
|
||||
help='The default Swift container to use'),
|
||||
cfg.IntOpt('backup_swift_object_size',
|
||||
default=52428800,
|
||||
help='The size in bytes of Swift backup objects'),
|
||||
cfg.IntOpt('backup_swift_retry_attempts',
|
||||
default=3,
|
||||
help='The number of retries to make for Swift operations'),
|
||||
cfg.IntOpt('backup_swift_retry_backoff',
|
||||
default=2,
|
||||
help='The backoff time in seconds between Swift retries'),
|
||||
cfg.StrOpt('backup_compression_algorithm',
|
||||
default='zlib',
|
||||
help='Compression algorithm (None to disable)'),
|
||||
]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(swiftbackup_service_opts)
|
||||
|
||||
|
||||
class SwiftBackupService(base.Base):
|
||||
"""Provides backup, restore and delete of backup objects within Swift."""
|
||||
|
||||
SERVICE_VERSION = '1.0.0'
|
||||
SERVICE_VERSION_MAPPING = {'1.0.0': '_restore_v1'}
|
||||
|
||||
def _get_compressor(self, algorithm):
|
||||
try:
|
||||
if algorithm.lower() in ('none', 'off', 'no'):
|
||||
return None
|
||||
elif algorithm.lower() in ('zlib', 'gzip'):
|
||||
import zlib as compressor
|
||||
return compressor
|
||||
elif algorithm.lower() in ('bz2', 'bzip2'):
|
||||
import bz2 as compressor
|
||||
return compressor
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
err = _('unsupported compression algorithm: %s') % algorithm
|
||||
raise ValueError(unicode(err))
|
||||
|
||||
def __init__(self, context, db_driver=None):
|
||||
self.context = context
|
||||
self.swift_url = '%s%s' % (FLAGS.backup_swift_url,
|
||||
self.context.project_id)
|
||||
self.az = FLAGS.storage_availability_zone
|
||||
self.data_block_size_bytes = FLAGS.backup_swift_object_size
|
||||
self.swift_attempts = FLAGS.backup_swift_retry_attempts
|
||||
self.swift_backoff = FLAGS.backup_swift_retry_backoff
|
||||
self.compressor = \
|
||||
self._get_compressor(FLAGS.backup_compression_algorithm)
|
||||
self.conn = swift.Connection(None, None, None,
|
||||
retries=self.swift_attempts,
|
||||
preauthurl=self.swift_url,
|
||||
preauthtoken=self.context.auth_token,
|
||||
starting_backoff=self.swift_backoff)
|
||||
super(SwiftBackupService, self).__init__(db_driver)
|
||||
|
||||
def _check_container_exists(self, container):
|
||||
LOG.debug(_('_check_container_exists: container: %s') % container)
|
||||
try:
|
||||
self.conn.head_container(container)
|
||||
except swift.ClientException as error:
|
||||
if error.http_status == httplib.NOT_FOUND:
|
||||
LOG.debug(_('container %s does not exist') % container)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
LOG.debug(_('container %s exists') % container)
|
||||
return True
|
||||
|
||||
def _create_container(self, context, backup):
|
||||
backup_id = backup['id']
|
||||
container = backup['container']
|
||||
LOG.debug(_('_create_container started, container: %(container)s,'
|
||||
'backup: %(backup_id)s') % locals())
|
||||
if container is None:
|
||||
container = FLAGS.backup_swift_container
|
||||
self.db.backup_update(context, backup_id, {'container': container})
|
||||
if not self._check_container_exists(container):
|
||||
self.conn.put_container(container)
|
||||
return container
|
||||
|
||||
def _generate_swift_object_name_prefix(self, backup):
|
||||
az = 'az_%s' % self.az
|
||||
backup_name = '%s_backup_%s' % (az, backup['id'])
|
||||
volume = 'volume_%s' % (backup['volume_id'])
|
||||
timestamp = timeutils.strtime(fmt="%Y%m%d%H%M%S")
|
||||
prefix = volume + '/' + timestamp + '/' + backup_name
|
||||
LOG.debug(_('_generate_swift_object_name_prefix: %s') % prefix)
|
||||
return prefix
|
||||
|
||||
def _generate_object_names(self, backup):
|
||||
prefix = backup['service_metadata']
|
||||
swift_objects = self.conn.get_container(backup['container'],
|
||||
prefix=prefix,
|
||||
full_listing=True)[1]
|
||||
swift_object_names = []
|
||||
for swift_object in swift_objects:
|
||||
swift_object_names.append(swift_object['name'])
|
||||
LOG.debug(_('generated object list: %s') % swift_object_names)
|
||||
return swift_object_names
|
||||
|
||||
def _metadata_filename(self, backup):
|
||||
swift_object_name = backup['service_metadata']
|
||||
filename = '%s_metadata' % swift_object_name
|
||||
return filename
|
||||
|
||||
def _write_metadata(self, backup, volume_id, container, object_list):
|
||||
filename = self._metadata_filename(backup)
|
||||
LOG.debug(_('_write_metadata started, container name: %(container)s,'
|
||||
' metadata filename: %(filename)s') % locals())
|
||||
metadata = {}
|
||||
metadata['version'] = self.SERVICE_VERSION
|
||||
metadata['backup_id'] = backup['id']
|
||||
metadata['volume_id'] = volume_id
|
||||
metadata['backup_name'] = backup['display_name']
|
||||
metadata['backup_description'] = backup['display_description']
|
||||
metadata['created_at'] = str(backup['created_at'])
|
||||
metadata['objects'] = object_list
|
||||
metadata_json = json.dumps(metadata, sort_keys=True, indent=2)
|
||||
reader = StringIO.StringIO(metadata_json)
|
||||
etag = self.conn.put_object(container, filename, reader)
|
||||
md5 = hashlib.md5(metadata_json).hexdigest()
|
||||
if etag != md5:
|
||||
err = _('error writing metadata file to swift, MD5 of metadata'
|
||||
' file in swift [%(etag)s] is not the same as MD5 of '
|
||||
'metadata file sent to swift [%(md5)s]') % locals()
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
LOG.debug(_('_write_metadata finished'))
|
||||
|
||||
def _read_metadata(self, backup):
|
||||
container = backup['container']
|
||||
filename = self._metadata_filename(backup)
|
||||
LOG.debug(_('_read_metadata started, container name: %(container)s, '
|
||||
'metadata filename: %(filename)s') % locals())
|
||||
(resp, body) = self.conn.get_object(container, filename)
|
||||
metadata = json.loads(body)
|
||||
LOG.debug(_('_read_metadata finished (%s)') % metadata)
|
||||
return metadata
|
||||
|
||||
def backup(self, backup, volume_file):
|
||||
"""Backup the given volume to swift using the given backup metadata."""
|
||||
backup_id = backup['id']
|
||||
volume_id = backup['volume_id']
|
||||
volume = self.db.volume_get(self.context, volume_id)
|
||||
|
||||
if volume['size'] <= 0:
|
||||
err = _('volume size %d is invalid.') % volume['size']
|
||||
raise exception.InvalidVolume(reason=err)
|
||||
|
||||
try:
|
||||
container = self._create_container(self.context, backup)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
|
||||
object_prefix = self._generate_swift_object_name_prefix(backup)
|
||||
backup['service_metadata'] = object_prefix
|
||||
self.db.backup_update(self.context, backup_id, {'service_metadata':
|
||||
object_prefix})
|
||||
volume_size_bytes = volume['size'] * 1024 * 1024 * 1024
|
||||
availability_zone = self.az
|
||||
LOG.debug(_('starting backup of volume: %(volume_id)s to swift,'
|
||||
' volume size: %(volume_size_bytes)d, swift object names'
|
||||
' prefix %(object_prefix)s, availability zone:'
|
||||
' %(availability_zone)s') % locals())
|
||||
object_id = 1
|
||||
object_list = []
|
||||
while True:
|
||||
data_block_size_bytes = self.data_block_size_bytes
|
||||
object_name = '%s-%05d' % (object_prefix, object_id)
|
||||
obj = {}
|
||||
obj[object_name] = {}
|
||||
obj[object_name]['offset'] = volume_file.tell()
|
||||
data = volume_file.read(data_block_size_bytes)
|
||||
obj[object_name]['length'] = len(data)
|
||||
if data == '':
|
||||
break
|
||||
LOG.debug(_('reading chunk of data from volume'))
|
||||
if self.compressor is not None:
|
||||
algorithm = FLAGS.backup_compression_algorithm.lower()
|
||||
obj[object_name]['compression'] = algorithm
|
||||
data_size_bytes = len(data)
|
||||
data = self.compressor.compress(data)
|
||||
comp_size_bytes = len(data)
|
||||
LOG.debug(_('compressed %(data_size_bytes)d bytes of data'
|
||||
' to %(comp_size_bytes)d bytes using '
|
||||
'%(algorithm)s') % locals())
|
||||
else:
|
||||
LOG.debug(_('not compressing data'))
|
||||
obj[object_name]['compression'] = 'none'
|
||||
|
||||
reader = StringIO.StringIO(data)
|
||||
LOG.debug(_('About to put_object'))
|
||||
try:
|
||||
etag = self.conn.put_object(container, object_name, reader)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
LOG.debug(_('swift MD5 for %(object_name)s: %(etag)s') % locals())
|
||||
md5 = hashlib.md5(data).hexdigest()
|
||||
obj[object_name]['md5'] = md5
|
||||
LOG.debug(_('backup MD5 for %(object_name)s: %(md5)s') % locals())
|
||||
if etag != md5:
|
||||
err = _('error writing object to swift, MD5 of object in '
|
||||
'swift %(etag)s is not the same as MD5 of object sent '
|
||||
'to swift %(md5)s') % locals()
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
object_list.append(obj)
|
||||
object_id += 1
|
||||
LOG.debug(_('Calling eventlet.sleep(0)'))
|
||||
eventlet.sleep(0)
|
||||
try:
|
||||
self._write_metadata(backup, volume_id, container, object_list)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
self.db.backup_update(self.context, backup_id, {'object_count':
|
||||
object_id})
|
||||
LOG.debug(_('backup %s finished.') % backup_id)
|
||||
|
||||
def _restore_v1(self, backup, volume_id, metadata, volume_file):
|
||||
"""Restore a v1 swift volume backup from swift."""
|
||||
backup_id = backup['id']
|
||||
LOG.debug(_('v1 swift volume backup restore of %s started'), backup_id)
|
||||
container = backup['container']
|
||||
metadata_objects = metadata['objects']
|
||||
metadata_object_names = []
|
||||
for metadata_object in metadata_objects:
|
||||
metadata_object_names.extend(metadata_object.keys())
|
||||
LOG.debug(_('metadata_object_names = %s') % metadata_object_names)
|
||||
prune_list = [self._metadata_filename(backup)]
|
||||
swift_object_names = [swift_object_name for swift_object_name in
|
||||
self._generate_object_names(backup)
|
||||
if swift_object_name not in prune_list]
|
||||
if sorted(swift_object_names) != sorted(metadata_object_names):
|
||||
err = _('restore_backup aborted, actual swift object list in '
|
||||
'swift does not match object list stored in metadata')
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
|
||||
for metadata_object in metadata_objects:
|
||||
object_name = metadata_object.keys()[0]
|
||||
LOG.debug(_('restoring object from swift. backup: %(backup_id)s, '
|
||||
'container: %(container)s, swift object name: '
|
||||
'%(object_name)s, volume: %(volume_id)s') % locals())
|
||||
try:
|
||||
(resp, body) = self.conn.get_object(container, object_name)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
compression_algorithm = metadata_object[object_name]['compression']
|
||||
decompressor = self._get_compressor(compression_algorithm)
|
||||
if decompressor is not None:
|
||||
LOG.debug(_('decompressing data using %s algorithm') %
|
||||
compression_algorithm)
|
||||
decompressed = decompressor.decompress(body)
|
||||
volume_file.write(decompressed)
|
||||
else:
|
||||
volume_file.write(body)
|
||||
|
||||
# force flush every write to avoid long blocking write on close
|
||||
volume_file.flush()
|
||||
os.fsync(volume_file.fileno())
|
||||
# Restoring a backup to a volume can take some time. Yield so other
|
||||
# threads can run, allowing for among other things the service
|
||||
# status to be updated
|
||||
eventlet.sleep(0)
|
||||
LOG.debug(_('v1 swift volume backup restore of %s finished'),
|
||||
backup_id)
|
||||
|
||||
def restore(self, backup, volume_id, volume_file):
|
||||
"""Restore the given volume backup from swift."""
|
||||
backup_id = backup['id']
|
||||
container = backup['container']
|
||||
object_prefix = backup['service_metadata']
|
||||
LOG.debug(_('starting restore of backup %(object_prefix)s from swift'
|
||||
' container: %(container)s, to volume %(volume_id)s, '
|
||||
'backup: %(backup_id)s') % locals())
|
||||
try:
|
||||
metadata = self._read_metadata(backup)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
metadata_version = metadata['version']
|
||||
LOG.debug(_('Restoring swift backup version %s'), metadata_version)
|
||||
try:
|
||||
restore_func = getattr(self, self.SERVICE_VERSION_MAPPING.get(
|
||||
metadata_version))
|
||||
except TypeError:
|
||||
err = (_('No support to restore swift backup version %s')
|
||||
% metadata_version)
|
||||
raise exception.InvalidBackup(reason=err)
|
||||
restore_func(backup, volume_id, metadata, volume_file)
|
||||
LOG.debug(_('restore %(backup_id)s to %(volume_id)s finished.') %
|
||||
locals())
|
||||
|
||||
def delete(self, backup):
|
||||
"""Delete the given backup from swift."""
|
||||
container = backup['container']
|
||||
LOG.debug('delete started, backup: %s, container: %s, prefix: %s',
|
||||
backup['id'], container, backup['service_metadata'])
|
||||
|
||||
if container is not None:
|
||||
swift_object_names = []
|
||||
try:
|
||||
swift_object_names = self._generate_object_names(backup)
|
||||
except Exception:
|
||||
LOG.warn(_('swift error while listing objects, continuing'
|
||||
' with delete'))
|
||||
|
||||
for swift_object_name in swift_object_names:
|
||||
try:
|
||||
self.conn.delete_object(container, swift_object_name)
|
||||
except socket.error as err:
|
||||
raise exception.SwiftConnectionFailed(reason=str(err))
|
||||
except Exception:
|
||||
LOG.warn(_('swift error while deleting object %s, '
|
||||
'continuing with delete') % swift_object_name)
|
||||
else:
|
||||
LOG.debug(_('deleted swift object: %(swift_object_name)s'
|
||||
' in container: %(container)s') % locals())
|
||||
# Deleting a backup's objects from swift can take some time.
|
||||
# Yield so other threads can run
|
||||
eventlet.sleep(0)
|
||||
|
||||
LOG.debug(_('delete %s finished') % backup['id'])
|
||||
|
||||
|
||||
def get_backup_service(context):
|
||||
return SwiftBackupService(context)
|
@ -65,10 +65,7 @@ db_opts = [
|
||||
'names'),
|
||||
cfg.StrOpt('snapshot_name_template',
|
||||
default='snapshot-%s',
|
||||
help='Template string to be used to generate snapshot names'),
|
||||
cfg.StrOpt('backup_name_template',
|
||||
default='backup-%s',
|
||||
help='Template string to be used to generate backup names'), ]
|
||||
help='Template string to be used to generate snapshot names'), ]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(db_opts)
|
||||
|
@ -291,8 +291,7 @@ def register_models():
|
||||
connection is lost and needs to be reestablished.
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
models = (Backup,
|
||||
Migration,
|
||||
models = (Migration,
|
||||
Service,
|
||||
Share,
|
||||
ShareAccessMapping,
|
||||
|
@ -565,14 +565,6 @@ class ImageCopyFailure(Invalid):
|
||||
message = _("Failed to copy image to volume")
|
||||
|
||||
|
||||
class BackupNotFound(NotFound):
|
||||
message = _("Backup %(backup_id)s could not be found.")
|
||||
|
||||
|
||||
class InvalidBackup(Invalid):
|
||||
message = _("Invalid backup: %(reason)s")
|
||||
|
||||
|
||||
class InvalidShare(CinderException):
|
||||
message = _("Invalid share: %(reason)s")
|
||||
|
||||
|
@ -134,12 +134,6 @@ global_opts = [
|
||||
cfg.StrOpt('scheduler_topic',
|
||||
default='manila-scheduler',
|
||||
help='the topic scheduler nodes listen on'),
|
||||
cfg.StrOpt('volume_topic',
|
||||
default='manila-volume',
|
||||
help='the topic volume nodes listen on'),
|
||||
cfg.StrOpt('backup_topic',
|
||||
default='manila-backup',
|
||||
help='the topic volume backup nodes listen on'),
|
||||
cfg.StrOpt('share_topic',
|
||||
default='manila-share',
|
||||
help='the topic share nodes listen on'),
|
||||
@ -188,9 +182,6 @@ global_opts = [
|
||||
cfg.StrOpt('volume_manager',
|
||||
default='manila.volume.manager.VolumeManager',
|
||||
help='full class name for the Manager for volume'),
|
||||
cfg.StrOpt('backup_manager',
|
||||
default='manila.backup.manager.BackupManager',
|
||||
help='full class name for the Manager for volume backup'),
|
||||
cfg.StrOpt('scheduler_manager',
|
||||
default='manila.scheduler.manager.SchedulerManager',
|
||||
help='full class name for the Manager for scheduler'),
|
||||
@ -234,9 +225,6 @@ global_opts = [
|
||||
cfg.StrOpt('volume_api_class',
|
||||
default='manila.volume.api.API',
|
||||
help='The full class name of the volume API class to use'),
|
||||
cfg.StrOpt('backup_api_class',
|
||||
default='manila.backup.api.API',
|
||||
help='The full class name of the volume backup API class'),
|
||||
cfg.StrOpt('share_api_class',
|
||||
default='manila.share.api.API',
|
||||
help='The full class name of the share API class to use'),
|
||||
|
@ -28,15 +28,12 @@ packages =
|
||||
scripts =
|
||||
bin/manila-all
|
||||
bin/manila-api
|
||||
bin/manila-backup
|
||||
bin/manila-clear-rabbit-queues
|
||||
bin/manila-manage
|
||||
bin/manila-rootwrap
|
||||
bin/manila-rpc-zmq-receiver
|
||||
bin/manila-scheduler
|
||||
bin/manila-share
|
||||
bin/manila-volume
|
||||
bin/manila-volume-usage-audit
|
||||
|
||||
[entry_points]
|
||||
manila.scheduler.filters =
|
||||
|
Loading…
x
Reference in New Issue
Block a user