diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 64e767cb0f7..04d5805e6f7 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -56,6 +56,7 @@ REST_API_VERSION_HISTORY = """ group in consisgroup-update operation. * 3.7 - Add cluster API and cluster_name field to service list API * 3.8 - Adds resources from volume_manage and snapshot_manage extensions. + * 3.9 - Add backup update interface. """ @@ -64,7 +65,7 @@ REST_API_VERSION_HISTORY = """ # minimum version of the API supported. # Explicitly using /v1 or /v2 enpoints will still work _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.8" +_MAX_API_VERSION = "3.9" _LEGACY_API_VERSION1 = "1.0" _LEGACY_API_VERSION2 = "2.0" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 42930c6cd6f..6d9267ea5e5 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -152,3 +152,16 @@ user documentation. Adds the following resources that were previously in extensions: - os-volume-manage => /v3//manageable_volumes - os-snapshot-manage => /v3//manageable_snapshots + +3.9 +--- + Added backup update interface to change name and description. + Returns: + + .. code-block:: json + + "backup": { + "id": "backup_id", + "name": "backup_name", + "links": "backup_link", + } diff --git a/cinder/api/v3/backups.py b/cinder/api/v3/backups.py new file mode 100644 index 00000000000..7db051217f7 --- /dev/null +++ b/cinder/api/v3/backups.py @@ -0,0 +1,56 @@ +# Copyright (c) 2016 Intel, Inc. +# 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 V3 api.""" + +from webob import exc + +from cinder.api.contrib import backups as backups_v2 +from cinder.api.openstack import wsgi +from cinder.i18n import _ + +BACKUP_UPDATE_MICRO_VERSION = '3.9' + + +class BackupsController(backups_v2.BackupsController): + """The backups API controller for the Openstack API V3.""" + + @wsgi.Controller.api_version(BACKUP_UPDATE_MICRO_VERSION) + def update(self, req, id, body): + """Update a backup.""" + context = req.environ['cinder.context'] + self.assert_valid_body(body, 'backup') + + backup_update = body['backup'] + + self.validate_name_and_description(backup_update) + update_dict = {} + if 'name' in backup_update: + update_dict['display_name'] = backup_update.pop('name') + if 'description' in backup_update: + update_dict['display_description'] = ( + backup_update.pop('description')) + # Check no unsupported fields. + if backup_update: + msg = _("Unsupported fields %s.") % (", ".join(backup_update)) + raise exc.HTTPBadRequest(explanation=msg) + + new_backup = self.backup_api.update(context, id, update_dict) + + return self._view_builder.summary(req, new_backup) + + +def create_resource(): + return wsgi.Resource(BackupsController()) diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 71706203f76..8638d9e7588 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -26,6 +26,7 @@ from cinder.api.v2 import snapshot_metadata from cinder.api.v2 import snapshots from cinder.api.v2 import types from cinder.api.v2 import volume_metadata +from cinder.api.v3 import backups from cinder.api.v3 import clusters from cinder.api.v3 import consistencygroups from cinder.api.v3 import messages @@ -125,3 +126,9 @@ class APIRouter(cinder.api.openstack.APIRouter): mapper.resource("manageable_snapshot", "manageable_snapshots", controller=self.resources['manageable_snapshots'], collection={'detail': 'GET'}) + + self.resources['backups'] = ( + backups.create_resource()) + mapper.resource("backup", "backups", + controller=self.resources['backups'], + collection={'detail': 'GET'}) diff --git a/cinder/backup/api.py b/cinder/backup/api.py index f89cf48b8c2..2d98a7f20dd 100644 --- a/cinder/backup/api.py +++ b/cinder/backup/api.py @@ -589,3 +589,10 @@ class API(base.Base): hosts) return backup + + def update(self, context, backup_id, fields): + check_policy(context, 'update') + backup = self.get(context, backup_id) + backup.update(fields) + backup.save() + return backup diff --git a/cinder/tests/unit/api/v3/test_backups.py b/cinder/tests/unit/api/v3/test_backups.py new file mode 100644 index 00000000000..37feebdfed8 --- /dev/null +++ b/cinder/tests/unit/api/v3/test_backups.py @@ -0,0 +1,104 @@ +# Copyright (c) 2016 Intel, Inc. +# 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 V3 api.""" + +import webob + +from cinder.api.openstack import api_version_request as api_version +from cinder.api.v3 import backups +import cinder.backup +from cinder import context +from cinder import exception +from cinder.objects import fields +from cinder import test +from cinder.tests.unit.api import fakes +from cinder.tests.unit import fake_constants as fake +from cinder.tests.unit import utils as test_utils + + +class BackupsControllerAPITestCase(test.TestCase): + """Test cases for backups API.""" + + def setUp(self): + super(BackupsControllerAPITestCase, self).setUp() + self.backup_api = cinder.backup.API() + self.ctxt = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, + auth_token=True, + is_admin=True) + self.controller = backups.BackupsController() + + def _fake_update_request(self, backup_id, version='3.9'): + req = fakes.HTTPRequest.blank('/v3/%s/backups/%s/update' % + (fake.PROJECT_ID, backup_id)) + req.environ['cinder.context'].is_admin = True + req.headers['Content-Type'] = 'application/json' + req.headers['OpenStack-API-Version'] = 'volume ' + version + req.api_version_request = api_version.APIVersionRequest(version) + return req + + def test_update_wrong_version(self): + req = self._fake_update_request(fake.BACKUP_ID, version='3.6') + body = {"backup": {"name": "Updated Test Name", }} + self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.update, req, fake.BACKUP_ID, + body) + + def test_backup_update_with_no_body(self): + # omit body from the request + req = self._fake_update_request(fake.BACKUP_ID) + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, + req, fake.BACKUP_ID, None) + + def test_backup_update_with_unsupported_field(self): + req = self._fake_update_request(fake.BACKUP_ID) + body = {"backup": {"id": fake.BACKUP2_ID, + "description": "", }} + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, + req, fake.BACKUP_ID, body) + + def test_backup_update_with_backup_not_found(self): + req = self._fake_update_request(fake.BACKUP_ID) + updates = { + "name": "Updated Test Name", + "description": "Updated Test description.", + } + body = {"backup": updates} + self.assertRaises(exception.NotFound, + self.controller.update, + req, fake.BACKUP_ID, body) + + def test_backup_update(self): + backup = test_utils.create_backup( + self.ctxt, + status=fields.BackupStatus.AVAILABLE) + req = self._fake_update_request(fake.BACKUP_ID) + new_name = "updated_test_name" + new_description = "Updated Test description." + updates = { + "name": new_name, + "description": new_description, + } + body = {"backup": updates} + self.controller.update(req, + backup.id, + body) + + backup.refresh() + self.assertEqual(new_name, backup.display_name) + self.assertEqual(new_description, + backup.display_description) diff --git a/cinder/tests/unit/policy.json b/cinder/tests/unit/policy.json index a8fb7292878..dbe04c1b581 100644 --- a/cinder/tests/unit/policy.json +++ b/cinder/tests/unit/policy.json @@ -97,6 +97,7 @@ "backup:restore": "", "backup:backup-import": "rule:admin_api", "backup:backup-export": "rule:admin_api", + "backup:update": "rule:admin_or_owner", "volume_extension:replication:promote": "rule:admin_api", "volume_extension:replication:reenable": "rule:admin_api", diff --git a/cinder/tests/unit/test_volume.py b/cinder/tests/unit/test_volume.py index 2b225e8a6de..5f8460a7784 100644 --- a/cinder/tests/unit/test_volume.py +++ b/cinder/tests/unit/test_volume.py @@ -5935,9 +5935,8 @@ class GenericVolumeDriverTestCase(DriverTestCase): vol = tests_utils.create_volume(self.context) self.context.user_id = fake.USER_ID self.context.project_id = fake.PROJECT_ID - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) properties = {} attach_info = {'device': {'path': '/dev/null'}} backup_service = mock.Mock() @@ -5995,9 +5994,8 @@ class GenericVolumeDriverTestCase(DriverTestCase): temp_vol = tests_utils.create_volume(self.context) self.context.user_id = fake.USER_ID self.context.project_id = fake.PROJECT_ID - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) properties = {} attach_info = {'device': {'path': '/dev/null'}} backup_service = mock.Mock() @@ -6088,16 +6086,15 @@ class GenericVolumeDriverTestCase(DriverTestCase): vol = tests_utils.create_volume(self.context) self.context.user_id = fake.USER_ID self.context.project_id = fake.PROJECT_ID - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) (backup_device, is_snapshot) = self.volume.driver.get_backup_device( self.context, backup_obj) volume = objects.Volume.get_by_id(self.context, vol.id) self.assertEqual(volume, backup_device) self.assertFalse(is_snapshot) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) - self.assertIsNone(backup.temp_volume_id) + backup_obj.refresh() + self.assertIsNone(backup_obj.temp_volume_id) def test_get_backup_device_in_use(self): vol = tests_utils.create_volume(self.context, @@ -6106,9 +6103,8 @@ class GenericVolumeDriverTestCase(DriverTestCase): temp_vol = tests_utils.create_volume(self.context) self.context.user_id = fake.USER_ID self.context.project_id = fake.PROJECT_ID - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) with mock.patch.object( self.volume.driver, '_create_temp_cloned_volume') as mock_create_temp: @@ -6118,7 +6114,7 @@ class GenericVolumeDriverTestCase(DriverTestCase): backup_obj)) self.assertEqual(temp_vol, backup_device) self.assertFalse(is_snapshot) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj.refresh() self.assertEqual(temp_vol.id, backup_obj.temp_volume_id) def test__create_temp_volume_from_snapshot(self): diff --git a/cinder/tests/unit/utils.py b/cinder/tests/unit/utils.py index 5149e0d4a54..b58f9cbb86d 100644 --- a/cinder/tests/unit/utils.py +++ b/cinder/tests/unit/utils.py @@ -208,7 +208,7 @@ def create_cgsnapshot(ctxt, def create_backup(ctxt, - volume_id, + volume_id=fake.VOLUME_ID, display_name='test_backup', display_description='This is a test backup', status=fields.BackupStatus.CREATING, @@ -216,27 +216,32 @@ def create_backup(ctxt, temp_volume_id=None, temp_snapshot_id=None, snapshot_id=None, - data_timestamp=None): - backup = {} - backup['volume_id'] = volume_id - backup['user_id'] = ctxt.user_id - backup['project_id'] = ctxt.project_id - backup['host'] = socket.gethostname() - backup['availability_zone'] = '1' - backup['display_name'] = display_name - backup['display_description'] = display_description - backup['container'] = 'fake' - backup['status'] = status - backup['fail_reason'] = '' - backup['service'] = 'fake' - backup['parent_id'] = parent_id - backup['size'] = 5 * 1024 * 1024 - backup['object_count'] = 22 - backup['temp_volume_id'] = temp_volume_id - backup['temp_snapshot_id'] = temp_snapshot_id - backup['snapshot_id'] = snapshot_id - backup['data_timestamp'] = data_timestamp - return db.backup_create(ctxt, backup) + data_timestamp=None, + **kwargs): + """Create a backup object.""" + values = { + 'user_id': ctxt.user_id or fake.USER_ID, + 'project_id': ctxt.project_id or fake.PROJECT_ID, + 'volume_id': volume_id, + 'status': status, + 'display_name': display_name, + 'display_description': display_description, + 'container': 'fake', + 'availability_zone': 'fake', + 'service': 'fake', + 'size': 5 * 1024 * 1024, + 'object_count': 22, + 'host': socket.gethostname(), + 'parent_id': parent_id, + 'temp_volume_id': temp_volume_id, + 'temp_snapshot_id': temp_snapshot_id, + 'snapshot_id': snapshot_id, + 'data_timestamp': data_timestamp, } + + values.update(kwargs) + backup = objects.Backup(ctxt, **values) + backup.create() + return backup def create_message(ctxt, diff --git a/cinder/tests/unit/volume/drivers/test_lvm_driver.py b/cinder/tests/unit/volume/drivers/test_lvm_driver.py index 2a57014ecd9..f0fbf5f315e 100644 --- a/cinder/tests/unit/volume/drivers/test_lvm_driver.py +++ b/cinder/tests/unit/volume/drivers/test_lvm_driver.py @@ -22,7 +22,6 @@ from oslo_config import cfg from cinder.brick.local_dev import lvm as brick_lvm from cinder import db from cinder import exception -from cinder import objects from cinder.objects import fields from cinder.tests import fake_driver from cinder.tests.unit.brick import fake_lvm @@ -145,9 +144,8 @@ class LVMVolumeDriverTestCase(DriverTestCase): vol = tests_utils.create_volume(self.context) self.context.user_id = fake.USER_ID self.context.project_id = fake.PROJECT_ID - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) properties = {} attach_info = {'device': {'path': '/dev/null'}} @@ -233,9 +231,8 @@ class LVMVolumeDriverTestCase(DriverTestCase): mock_volume_get.return_value = vol temp_snapshot = tests_utils.create_snapshot(self.context, vol['id']) - backup = tests_utils.create_backup(self.context, - vol['id']) - backup_obj = objects.Backup.get_by_id(self.context, backup.id) + backup_obj = tests_utils.create_backup(self.context, + vol['id']) properties = {} attach_info = {'device': {'path': '/dev/null'}} backup_service = mock.Mock() diff --git a/etc/cinder/policy.json b/etc/cinder/policy.json index e820e9aff89..b1c5138d1be 100644 --- a/etc/cinder/policy.json +++ b/etc/cinder/policy.json @@ -91,6 +91,7 @@ "backup:restore": "rule:admin_or_owner", "backup:backup-import": "rule:admin_api", "backup:backup-export": "rule:admin_api", + "backup:update": "rule:admin_or_owner", "snapshot_extension:snapshot_actions:update_snapshot_status": "", "snapshot_extension:snapshot_manage": "rule:admin_api", diff --git a/releasenotes/notes/backup-update-d0b0db6a7b1c2a5b.yaml b/releasenotes/notes/backup-update-d0b0db6a7b1c2a5b.yaml new file mode 100644 index 00000000000..3abca4d2562 --- /dev/null +++ b/releasenotes/notes/backup-update-d0b0db6a7b1c2a5b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added REST API to update backup name and description.