Support cross AZ backups

We currently limit backups to the same AZ of the volume while allowing
volume restoration to any AZ.

When having multiple AZs it is ideal to store your backups in a
different AZ than the one where the source volumes are stored.

This patch adds microversion 3.51 where we allow requesting the AZ where
we want to store backups which will allow us to have as many backup AZs
as volume AZs and do cross AZ backups for increased security, this
feature also supports having an additional AZ just for backups or the
less desirable solution where you store all your backups in a single AZ
shared with some of the volumes.

Change-Id: I595932276088d25abd464025c99dce33a2cc502b
This commit is contained in:
Gorka Eguileor 2018-02-08 15:23:47 +01:00
parent 015b105399
commit 5feaf74ccf
12 changed files with 82 additions and 8 deletions

View File

@ -252,6 +252,7 @@ Request
- name: name_optional
- snapshot_id: snapshot_id_3
- metadata: metadata_9
- availability_zone: availability_zone_4
Request Example
---------------

View File

@ -512,6 +512,13 @@ availability_zone_3:
in: body
required: true
type: string
availability_zone_4:
description: |
The backup availability zone key value pair.
in: body
required: false
type: string
min_version: 3.51
backend_id:
description: |
ID of backend to failover to. Default is ``None``.

View File

@ -5,6 +5,7 @@
"name": "backup001",
"volume_id": "64f5d2fb-d836-4063-b7e2-544d5c1ff607",
"incremental": true,
"availability_zone": "AZ2",
"metadata": null
}
}

View File

@ -144,7 +144,8 @@ class BackupsController(wsgi.Controller):
# - maybe also do validation of swift container name
@wsgi.response(http_client.ACCEPTED)
@validation.schema(backup.create, '2.0', '3.42')
@validation.schema(backup.create_backup_v343, '3.43')
@validation.schema(backup.create_backup_v343, '3.43', '3.50')
@validation.schema(backup.create_backup_v351, mv.BACKUP_AZ)
def create(self, req, body):
"""Create a new backup."""
LOG.debug('Creating new backup %s', body)
@ -166,16 +167,24 @@ class BackupsController(wsgi.Controller):
snapshot_id = backup.get('snapshot_id', None)
metadata = backup.get('metadata', None) if req_version.matches(
mv.BACKUP_METADATA) else None
if req_version.matches(mv.BACKUP_AZ):
availability_zone = backup.get('availability_zone', None)
else:
availability_zone = None
az_text = ' in az %s' % availability_zone if availability_zone else ''
LOG.info("Creating backup of volume %(volume_id)s in container"
" %(container)s",
{'volume_id': volume_id, 'container': container},
" %(container)s%(az)s",
{'volume_id': volume_id, 'container': container,
'az': az_text},
context=context)
try:
new_backup = self.backup_api.create(context, name, description,
volume_id, container,
incremental, None, force,
snapshot_id, metadata)
incremental, availability_zone,
force, snapshot_id, metadata)
except (exception.InvalidVolume,
exception.InvalidSnapshot,
exception.InvalidVolumeMetadata,

View File

@ -139,6 +139,8 @@ BACKEND_STATE_REPORT = '3.49'
MULTIATTACH_VOLUMES = '3.50'
BACKUP_AZ = '3.51'
def get_mv_header(version):
"""Gets a formatted HTTP microversion header.

View File

@ -114,6 +114,7 @@ REST_API_VERSION_HISTORY = """
* 3.48 - Add ``shared_targets`` and ``service_uuid`` fields to volume.
* 3.49 - Support report backend storage state in service list.
* 3.50 - Add multiattach capability
* 3.51 - Add support for cross AZ backups.
"""
# The minimum and maximum versions of the API supported
@ -121,7 +122,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v2 endpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.50"
_MAX_API_VERSION = "3.51"
_LEGACY_API_VERSION2 = "2.0"
UPDATED = "2017-09-19T20:18:14Z"

View File

@ -400,3 +400,7 @@ Support report backend storage state in service list.
Services supporting this microversion are capable of volume multiattach.
This version does not need to be requested when creating the volume, but can
be used as a way to query if the capability exists in the Cinder service.
3.51
----
Add support for cross AZ backups.

View File

@ -51,6 +51,11 @@ create_backup_v343['properties']['backup']['properties'][
'metadata'] = parameter_types.metadata_allows_null
create_backup_v351 = copy.deepcopy(create_backup_v343)
create_backup_v351['properties']['backup']['properties'][
'availability_zone'] = parameter_types.nullable_string
update = {
'type': 'object',
'properties': {

View File

@ -185,3 +185,8 @@ backup_url = {'type': 'string', 'minLength': 1, 'format': 'base64'}
backup_service = {'type': 'string', 'minLength': 0, 'maxLength': 255}
nullable_string = {
'type': ('string', 'null'), 'minLength': 0, 'maxLength': 255
}

View File

@ -227,8 +227,9 @@ class API(base.Base):
previous_status = volume['status']
volume_host = volume_utils.extract_host(volume.host, 'host')
host = self._get_available_backup_service_host(
volume_host, volume.availability_zone)
availability_zone = availability_zone or volume.availability_zone
host = self._get_available_backup_service_host(volume_host,
availability_zone)
# Reserve a quota before setting volume status and backup status
try:
@ -307,6 +308,7 @@ class API(base.Base):
'parent_id': parent_id,
'size': volume['size'],
'host': host,
'availability_zone': availability_zone,
'snapshot_id': snapshot_id,
'data_timestamp': data_timestamp,
'metadata': metadata or {}

View File

@ -595,6 +595,38 @@ class BackupsAPITestCase(test.TestCase):
volume.destroy()
@mock.patch('cinder.objects.Service.is_up', mock.Mock(return_value=True))
@mock.patch('cinder.db.service_get_all')
def test_create_backup_with_availability_zone(self, _mock_service_get_all):
vol_az = 'az1'
backup_svc_az = 'az2'
_mock_service_get_all.return_value = [
{'availability_zone': backup_svc_az, 'host': 'testhost',
'disabled': 0, 'updated_at': timeutils.utcnow(),
'uuid': 'a3a593da-7f8d-4bb7-8b4c-f2bc1e0b4824'}]
volume = utils.create_volume(self.context, availability_zone=vol_az,
size=1)
# Create a backup with metadata
body = {'backup': {'name': 'nightly001',
'volume_id': volume.id,
'container': 'nightlybackups',
'availability_zone': backup_svc_az}}
req = webob.Request.blank('/v3/%s/backups' % fake.PROJECT_ID)
req.method = 'POST'
req.headers = mv.get_mv_header(mv.BACKUP_AZ)
req.headers['Content-Type'] = 'application/json'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(fakes.wsgi_app(
fake_auth_context=self.user_context))
self.assertEqual(202, res.status_code)
res_dict = jsonutils.loads(res.body)
backup = objects.Backup.get_by_id(self.context,
res_dict['backup']['id'])
self.assertEqual(backup_svc_az, backup.availability_zone)
@mock.patch('cinder.db.service_get_all')
def test_create_backup_inuse_no_force(self,
_mock_service_get_all):

View File

@ -0,0 +1,5 @@
---
features:
- |
Cinder backup creation can now (since microversion 3.51) receive the
availability zone where the backup should be stored.