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:
lisali 2016-06-27 16:01:25 +08:00 committed by Michal Dulko
parent eaac65bb48
commit c5ebe48b8e
12 changed files with 236 additions and 45 deletions

View File

@ -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"

View File

@ -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
View 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())

View File

@ -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'})

View File

@ -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

View 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)

View File

@ -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",

View File

@ -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):

View File

@ -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,

View File

@ -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()

View File

@ -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",

View File

@ -0,0 +1,3 @@
---
features:
- Added REST API to update backup name and description.