Add backup update function (microversion)
Add update interface so that users can update name and description of a backup. This new API endpoint is added in microversion 3.9. APIImpact Add PUT to /backups/<id>. DocImpact Change-Id: If592b53c7e1dcdc36dbcaa89425b8e44a51684c3
This commit is contained in:
parent
eaac65bb48
commit
c5ebe48b8e
@ -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"
|
||||
|
||||
|
@ -152,3 +152,16 @@ user documentation.
|
||||
Adds the following resources that were previously in extensions:
|
||||
- os-volume-manage => /v3/<project_id>/manageable_volumes
|
||||
- os-snapshot-manage => /v3/<project_id>/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",
|
||||
}
|
||||
|
56
cinder/api/v3/backups.py
Normal file
56
cinder/api/v3/backups.py
Normal file
@ -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())
|
@ -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'})
|
||||
|
@ -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
|
||||
|
104
cinder/tests/unit/api/v3/test_backups.py
Normal file
104
cinder/tests/unit/api/v3/test_backups.py
Normal file
@ -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)
|
@ -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",
|
||||
|
@ -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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.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):
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.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,
|
||||
backup_obj = tests_utils.create_backup(self.context,
|
||||
vol['id'])
|
||||
backup_obj = objects.Backup.get_by_id(self.context, backup.id)
|
||||
properties = {}
|
||||
attach_info = {'device': {'path': '/dev/null'}}
|
||||
backup_service = mock.Mock()
|
||||
|
@ -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",
|
||||
|
3
releasenotes/notes/backup-update-d0b0db6a7b1c2a5b.yaml
Normal file
3
releasenotes/notes/backup-update-d0b0db6a7b1c2a5b.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added REST API to update backup name and description.
|
Loading…
Reference in New Issue
Block a user