From 5feaf74ccf10148859e206ce21bfd54dec2c1c16 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Thu, 8 Feb 2018 15:23:47 +0100 Subject: [PATCH] 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 --- api-ref/source/v3/ext-backups.inc | 1 + api-ref/source/v3/parameters.yaml | 7 ++++ .../v3/samples/backup-create-request.json | 1 + cinder/api/contrib/backups.py | 19 ++++++++--- cinder/api/microversions.py | 2 ++ cinder/api/openstack/api_version_request.py | 3 +- .../openstack/rest_api_version_history.rst | 4 +++ cinder/api/schemas/backups.py | 5 +++ cinder/api/validation/parameter_types.py | 5 +++ cinder/backup/api.py | 6 ++-- cinder/tests/unit/api/contrib/test_backups.py | 32 +++++++++++++++++++ ...ure-cross-az-backups-6b68c4c4456f2fd7.yaml | 5 +++ 12 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/feature-cross-az-backups-6b68c4c4456f2fd7.yaml diff --git a/api-ref/source/v3/ext-backups.inc b/api-ref/source/v3/ext-backups.inc index 05a8305e159..2004978aef6 100644 --- a/api-ref/source/v3/ext-backups.inc +++ b/api-ref/source/v3/ext-backups.inc @@ -252,6 +252,7 @@ Request - name: name_optional - snapshot_id: snapshot_id_3 - metadata: metadata_9 + - availability_zone: availability_zone_4 Request Example --------------- diff --git a/api-ref/source/v3/parameters.yaml b/api-ref/source/v3/parameters.yaml index fdd78c603c6..97885e53c31 100644 --- a/api-ref/source/v3/parameters.yaml +++ b/api-ref/source/v3/parameters.yaml @@ -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``. diff --git a/api-ref/source/v3/samples/backup-create-request.json b/api-ref/source/v3/samples/backup-create-request.json index 5b3326f75d0..00ad677cd6e 100644 --- a/api-ref/source/v3/samples/backup-create-request.json +++ b/api-ref/source/v3/samples/backup-create-request.json @@ -5,6 +5,7 @@ "name": "backup001", "volume_id": "64f5d2fb-d836-4063-b7e2-544d5c1ff607", "incremental": true, + "availability_zone": "AZ2", "metadata": null } } diff --git a/cinder/api/contrib/backups.py b/cinder/api/contrib/backups.py index acb9c460582..efaedf16d11 100644 --- a/cinder/api/contrib/backups.py +++ b/cinder/api/contrib/backups.py @@ -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, diff --git a/cinder/api/microversions.py b/cinder/api/microversions.py index 6f9958e6a6b..7624e4e995c 100644 --- a/cinder/api/microversions.py +++ b/cinder/api/microversions.py @@ -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. diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index a4389cff0ad..3756cae01ee 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -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" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 714f16db8e2..6d9394291ee 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/cinder/api/schemas/backups.py b/cinder/api/schemas/backups.py index 8a84c7aef1f..4276a6ef169 100644 --- a/cinder/api/schemas/backups.py +++ b/cinder/api/schemas/backups.py @@ -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': { diff --git a/cinder/api/validation/parameter_types.py b/cinder/api/validation/parameter_types.py index 437583b120f..9b9d8fb3139 100644 --- a/cinder/api/validation/parameter_types.py +++ b/cinder/api/validation/parameter_types.py @@ -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 +} diff --git a/cinder/backup/api.py b/cinder/backup/api.py index 707ed8b03bd..3b71ea47358 100644 --- a/cinder/backup/api.py +++ b/cinder/backup/api.py @@ -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 {} diff --git a/cinder/tests/unit/api/contrib/test_backups.py b/cinder/tests/unit/api/contrib/test_backups.py index c554639f1b6..b71597d16e9 100644 --- a/cinder/tests/unit/api/contrib/test_backups.py +++ b/cinder/tests/unit/api/contrib/test_backups.py @@ -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): diff --git a/releasenotes/notes/feature-cross-az-backups-6b68c4c4456f2fd7.yaml b/releasenotes/notes/feature-cross-az-backups-6b68c4c4456f2fd7.yaml new file mode 100644 index 00000000000..7fc24afc533 --- /dev/null +++ b/releasenotes/notes/feature-cross-az-backups-6b68c4c4456f2fd7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Cinder backup creation can now (since microversion 3.51) receive the + availability zone where the backup should be stored.