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:
parent
58dea8a58f
commit
6673dbb88f
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue