Use PATCH method for flavor update

This gets rid of the flavor extraspec endpoint, and change to use
PATCH method for flavor update which also include flavor extra
specs update.

Change-Id: I86c291111ebdc1c8824031a590d3d5db4db294ed
Closes-Bug: #1696632
This commit is contained in:
Zhenguo Niu 2017-06-08 16:47:19 +08:00
parent 58dea8a58f
commit 6673dbb88f
4 changed files with 69 additions and 241 deletions

View File

@ -96,7 +96,7 @@ Response
Update Flavor
=============
.. rest_method:: PUT /flavors/{flavor_uuid}
.. rest_method:: PATCH /flavors/{flavor_uuid}
Updates a flavor.
@ -111,14 +111,14 @@ conflict(409)
Request
-------
The BODY of the PATCH request must be a JSON PATCH document, adhering to
`RFC 6902 <https://tools.ietf.org/html/rfc6902>`_.
.. rest_parameters:: parameters.yaml
- flavor_uuid: flavor_uuid_path
- name: flavor_name
- description: flavor_description
- is_public: flavor_is_public_not_required
**Example Update Flavor**
**Example Update Flavor: JSON request**
.. literalinclude:: samples/flavors/flavor-update-put-req.json
:language: javascript
@ -206,97 +206,3 @@ Response
--------
No body content is returned on a successful DELETE.
List Extra Specs
================
.. rest_method:: GET /flavors/{flavor_uuid}/extraspecs
Lists all extra specs related to the given flavor.
Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403)
Request
-------
.. rest_parameters:: parameters.yaml
- flavor_uuid: flavor_uuid_path
Response
--------
.. rest_parameters:: parameters.yaml
- extra_specs: flavor_extra_specs
**Example List Extra Specs**
.. literalinclude:: samples/flavors/flavor-extra-specs-list-resp.json
:language: javascript
Create/Update Extra Spec
========================
.. rest_method:: PATCH /flavors/{flavor_uuid}/extraspecs
Create/update extra specs to the given flavor.
Normal response codes: 201
Error response codes: unauthorized(401), forbidden(403)
Request
-------
.. rest_parameters:: parameters.yaml
- flavor_uuid: flavor_uuid_path
- extra_specs: flavor_extra_specs
**Example Create Extra Specs**
.. literalinclude:: samples/flavors/flavor-extra-specs-patch-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- extra_specs: flavor_extra_specs
**Example Create Extra Specs**
.. literalinclude:: samples/flavors/flavor-extra-specs-patch-resp.json
:language: javascript
Delete Extra Spec
=================
.. rest_method:: DELETE /flavors/{flavor_uuid}/extraspecs/key
Deletes an extra spec related to the specific flavor.
Normal response codes: 204
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- flavor_uuid: flavor_uuid_path
- key: spec_key_path
Response
--------
No body content is returned on a successful DELETE.

View File

@ -1,5 +1,17 @@
{
"name": "updated_flavor",
"description": "this is a flavor to be updated",
"is_public": false
}
[
{
"op": "replace",
"path": "/name",
"value": "updated_flavor"
},
{
"op": "replace",
"path": "/is_public",
"value": false
},
{
"op": "add",
"path": "/extra_specs/k1",
"value": "v1"
}
]

View File

@ -24,6 +24,7 @@ from mogan.api.controllers import link
from mogan.api.controllers.v1.schemas import flavor as flavor_schema
from mogan.api.controllers.v1.schemas import flavor_access
from mogan.api.controllers.v1 import types
from mogan.api.controllers.v1 import utils as api_utils
from mogan.api import expose
from mogan.api import validation
from mogan.common import exception
@ -102,6 +103,16 @@ class Flavor(base.APIBase):
return flavor
class FlavorPatchType(types.JsonPatchType):
_api_base = Flavor
@staticmethod
def internal_attrs():
defaults = types.JsonPatchType.internal_attrs()
return defaults + ['/cpus', '/memory', '/nics', '/disks']
class FlavorCollection(base.APIBase):
"""API representation of a collection of flavor."""
@ -116,39 +127,6 @@ class FlavorCollection(base.APIBase):
return collection
class FlavorExtraSpecsController(rest.RestController):
"""REST controller for flavor extra specs."""
@policy.authorize_wsgi("mogan:flavor_extra_specs", "get_all")
@expose.expose(wtypes.text, types.uuid)
def get_all(self, flavor_uuid):
"""Retrieve a list of extra specs of the queried flavor."""
flavor = objects.Flavor.get(pecan.request.context, flavor_uuid)
return dict(extra_specs=flavor.extra_specs)
@policy.authorize_wsgi("mogan:flavor_extra_specs", "patch")
@expose.expose(types.jsontype, types.uuid, body=types.jsontype,
status_code=http_client.ACCEPTED)
def patch(self, flavor_uuid, extra_spec):
"""Create/update extra specs for the given flavor."""
flavor = objects.Flavor.get(pecan.request.context, flavor_uuid)
flavor.extra_specs = dict(flavor.extra_specs, **extra_spec)
flavor.save()
return dict(extra_specs=flavor.extra_specs)
@policy.authorize_wsgi("mogan:flavor_extra_specs", "delete")
@expose.expose(None, types.uuid, wtypes.text,
status_code=http_client.NO_CONTENT)
def delete(self, flavor_uuid, spec_name):
"""Delete an extra specs for the given flavor."""
flavor = objects.Flavor.get(pecan.request.context, flavor_uuid)
del flavor.extra_specs[spec_name]
flavor.save()
class FlavorAccessController(rest.RestController):
"""REST controller for flavor access."""
@ -218,7 +196,6 @@ class FlavorAccessController(rest.RestController):
class FlavorsController(rest.RestController):
"""REST controller for Flavors."""
extraspecs = FlavorExtraSpecsController()
access = FlavorAccessController()
@policy.authorize_wsgi("mogan:flavor", "get_all")
@ -256,33 +233,45 @@ class FlavorsController(rest.RestController):
return Flavor.convert_with_links(new_flavor)
@policy.authorize_wsgi("mogan:flavor", "update")
@expose.expose(Flavor, types.uuid, body=Flavor)
def put(self, flavor_uuid, flavor):
@wsme.validate(types.uuid, [FlavorPatchType])
@expose.expose(Flavor, types.uuid, body=[FlavorPatchType])
def patch(self, flavor_uuid, patch):
"""Update a flavor.
:param flavor_uuid: the uuid of the flavor to be updated.
:param flavor: a flavor within the request body.
:param flavor: a json PATCH document to apply to this flavor.
"""
try:
flavor_in_db = objects.Flavor.get(
db_flavor = objects.Flavor.get(
pecan.request.context, flavor_uuid)
except exception.FlavorTypeNotFound:
msg = (_("Flavor %s could not be found") %
flavor_uuid)
raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST)
need_to_update = False
for attr in ('name', 'description', 'is_public'):
if getattr(flavor, attr) != wtypes.Unset:
need_to_update = True
setattr(flavor_in_db, attr, getattr(flavor, attr))
# don't need to call db_api if no update
if need_to_update:
flavor_in_db.save()
# Set the HTTP Location Header
pecan.response.location = link.build_url('flavor',
flavor_in_db.uuid)
return Flavor.convert_with_links(flavor_in_db)
try:
flavor = Flavor(
**api_utils.apply_jsonpatch(db_flavor.as_dict(), patch))
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
# Update only the fields that have changed
for field in objects.Flavor.fields:
try:
patch_val = getattr(flavor, field)
except AttributeError:
# Ignore fields that aren't exposed in the API
continue
if patch_val == wtypes.Unset:
patch_val = None
if db_flavor[field] != patch_val:
db_flavor[field] = patch_val
db_flavor.save()
return Flavor.convert_with_links(db_flavor)
@policy.authorize_wsgi("mogan:flavor", "delete")
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)

View File

@ -15,8 +15,6 @@
import mock
import six
from oslo_serialization import jsonutils
from mogan.tests.functional.api import v1 as v1_test
@ -77,90 +75,13 @@ class TestFlavor(v1_test.APITestV1):
resp = self.get_json('/flavors/' + self.FLAVOR_UUIDS[0],
headers=self.headers)
self.assertEqual('test0', resp['name'])
self.assertEqual('just test0', resp['description'])
values = {"name": "update_name", "description": "updated_description",
"is_public": False}
self.put_json('/flavors/' + self.FLAVOR_UUIDS[0], values,
headers=self.headers, status=200)
self.patch_json('/flavors/' + self.FLAVOR_UUIDS[0],
[{'path': '/name', 'value': 'updated_name',
'op': 'replace'},
{'path': '/extra_specs/k1', 'value': 'v1',
'op': 'add'}],
headers=self.headers, status=200)
resp = self.get_json('/flavors/' + self.FLAVOR_UUIDS[0],
headers=self.headers)
self.assertEqual('update_name', resp['name'])
self.assertEqual('updated_description', resp['description'])
self.assertEqual(False, resp['is_public'])
class TestFlavorExtra(v1_test.APITestV1):
FLAVOR_UUID = 'ff28b5a2-73e5-431c-b4b7-1b96b74bca7b'
def setUp(self):
super(TestFlavorExtra, self).setUp()
self.headers = self.gen_headers(self.context, roles="admin")
self._prepare_flavor()
@mock.patch('oslo_utils.uuidutils.generate_uuid')
def _prepare_flavor(self, mocked):
mocked.return_value = self.FLAVOR_UUID
body = {"name": "test_flavor_extra",
"description": "just test flavor extra"}
self.post_json('/flavors', body, headers=self.headers, status=201)
def test_list_extra_empty(self):
resp = self.get_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
headers=self.headers)
self.assertEqual({}, resp['extra_specs'])
def test_add_extra(self):
resp = self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key': 'test_value'},
headers=self.headers)
resp = resp.json
self.assertEqual({'extra_specs': {'test_key': 'test_value'}}, resp)
def test_update_extra(self):
resp = self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key': 'test_value1'},
headers=self.headers)
resp = resp.json
self.assertEqual({'extra_specs': {'test_key': 'test_value1'}}, resp)
resp = self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key': 'test_value2'},
headers=self.headers)
resp = resp.json
self.assertEqual({'extra_specs': {'test_key': 'test_value2'}}, resp)
def test_list_extra(self):
resp = self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key1': 'test_value1',
'test_key2': 'test_value2'},
headers=self.headers)
resp = resp.json
self.assertEqual(
'{"test_key1": "test_value1", "test_key2": "test_value2"}',
jsonutils.dumps(resp['extra_specs'], sort_keys=True))
self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key3': 'test_value3'},
headers=self.headers)
resp = self.get_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
headers=self.headers)
self.assertEqual(
'{"test_key1": "test_value1", "test_key2": "test_value2", '
'"test_key3": "test_value3"}',
jsonutils.dumps(resp['extra_specs'], sort_keys=True))
def test_delete_extra(self):
resp = self.patch_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
{'test_key1': 'test_value1',
'test_key2': 'test_value2'},
headers=self.headers)
resp = resp.json
self.assertEqual(
'{"test_key1": "test_value1", "test_key2": "test_value2"}',
jsonutils.dumps(resp['extra_specs'], sort_keys=True))
self.delete('/flavors/%s/extraspecs/test_key1' % self.FLAVOR_UUID,
headers=self.headers)
resp = self.get_json('/flavors/%s/extraspecs' % self.FLAVOR_UUID,
headers=self.headers)
self.assertEqual({'test_key2': 'test_value2'}, resp['extra_specs'])
self.assertEqual('updated_name', resp['name'])
self.assertEqual({'k1': 'v1'}, resp['extra_specs'])