Add microversion to allow setting flavor description

This adds the new microversion to allow providing
a description when creating a flavor, returning a
flavor description when showing flavor details, and
updating the description on an existing flavor.

Implements blueprint flavor-description

Change-Id: Ib16b0de82f9f9492f5cacf646dc3165a0849d75e
This commit is contained in:
Matt Riedemann 2017-10-25 16:59:31 -04:00
parent f93f10e7b6
commit 034d7f3795
39 changed files with 1253 additions and 52 deletions

View File

@ -42,14 +42,12 @@ Response
- flavors: flavors
- id: flavor_id_body
- name: flavor_name
- description: flavor_description_resp
- links: links
**Example List Flavors**
**Example List Flavors (v2.55)**
Showing all the default flavors of a Liberty era Nova installation
that was not customized by the site operators.
.. literalinclude:: ../../doc/api_samples/flavors/flavors-list-resp.json
.. literalinclude:: ../../doc/api_samples/flavors/v2.55/flavors-list-resp.json
:language: javascript
Create Flavor
@ -74,6 +72,7 @@ Request
- flavor: flavor
- name: flavor_name
- description: flavor_description
- id: flavor_id_body_create
- ram: flavor_ram
- disk: flavor_disk
@ -83,9 +82,9 @@ Request
- rxtx_factor: flavor_rxtx_factor_in
- os-flavor-access:is_public: flavor_is_public_in
**Example Create Flavor**
**Example Create Flavor (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavor-manage/flavor-create-post-req.json
.. literalinclude:: ../../doc/api_samples/flavor-manage/v2.55/flavor-create-post-req.json
:language: javascript
Response
@ -95,6 +94,7 @@ Response
- flavor: flavor
- name: flavor_name
- description: flavor_description_resp
- id: flavor_id_body
- ram: flavor_ram
- disk: flavor_disk
@ -107,9 +107,9 @@ Response
- os-flavor-access:is_public: flavor_is_public
**Example Create Flavor**
**Example Create Flavor (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavor-manage/flavor-create-post-resp.json
.. literalinclude:: ../../doc/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json
:language: javascript
List Flavors With Details
@ -144,6 +144,7 @@ Response
- flavors: flavors
- name: flavor_name
- description: flavor_description_resp
- id: flavor_id_body
- ram: flavor_ram
- disk: flavor_disk
@ -155,9 +156,9 @@ Response
- rxtx_factor: flavor_rxtx_factor
- os-flavor-access:is_public: flavor_is_public
**Example List Flavors With Details**
**Example List Flavors With Details (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavors/flavors-detail-resp.json
.. literalinclude:: ../../doc/api_samples/flavors/v2.55/flavors-detail-resp.json
:language: javascript
Show Flavor Details
@ -185,6 +186,7 @@ Response
- flavor: flavor
- name: flavor_name
- description: flavor_description_resp
- id: flavor_id_body
- ram: flavor_ram
- disk: flavor_disk
@ -196,9 +198,65 @@ Response
- rxtx_factor: flavor_rxtx_factor
- os-flavor-access:is_public: flavor_is_public
**Example Show Flavor Details**
**Example Show Flavor Details (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavors/flavor-get-resp.json
.. literalinclude:: ../../doc/api_samples/flavors/v2.55/flavor-get-resp.json
:language: javascript
Update Flavor Description
=========================
.. rest_method:: PUT /flavors/{flavor_id}
Updates a flavor description.
This API is available starting with microversion 2.55.
Policy defaults enable only users with the administrative role to
perform this operation. Cloud providers can change these permissions
through the ``policy.json`` file.
Normal response codes: 200
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- flavor_id: flavor_id
- flavor: flavor
- description: flavor_description_required
**Example Update Flavor Description (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavor-manage/v2.55/flavor-update-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- flavor: flavor
- name: flavor_name
- description: flavor_description_resp
- id: flavor_id_body
- ram: flavor_ram
- disk: flavor_disk
- vcpus: flavor_cpus
- links: links
- OS-FLV-EXT-DATA:ephemeral: flavor_ephem_disk
- OS-FLV-DISABLED:disabled: flavor_disabled
- swap: flavor_swap
- rxtx_factor: flavor_rxtx_factor
- os-flavor-access:is_public: flavor_is_public
**Example Update Flavor Description (v2.55)**
.. literalinclude:: ../../doc/api_samples/flavor-manage/v2.55/flavor-update-resp.json
:language: javascript
Delete Flavor

View File

@ -2365,6 +2365,29 @@ flavor_cpus_2_47:
type: integer
description: |
The number of virtual CPUs that were allocated to the server.
flavor_description:
type: string
in: body
required: false
min_version: 2.55
description: |
A free form description of the flavor. Limited to 65535 characters
in length. Only printable characters are allowed.
flavor_description_required:
type: string
in: body
required: true
min_version: 2.55
description: |
A free form description of the flavor. Limited to 65535 characters
in length. Only printable characters are allowed.
flavor_description_resp:
description: |
The description of the flavor.
in: body
required: true
type: string
min_version: 2.55
flavor_disabled:
in: body
required: false

View File

@ -0,0 +1,11 @@
{
"flavor": {
"name": "test_flavor",
"ram": 1024,
"vcpus": 2,
"disk": 10,
"id": "10",
"rxtx_factor": 2.0,
"description": "test description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"OS-FLV-DISABLED:disabled": false,
"disk": 10,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "10",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/10",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/10",
"rel": "bookmark"
}
],
"name": "test_flavor",
"ram": 1024,
"swap": "",
"rxtx_factor": 2.0,
"vcpus": 2,
"description": "test description"
}
}

View File

@ -0,0 +1,5 @@
{
"flavor": {
"description": "updated description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/flavors/1",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "updated description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "7",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/7",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/7",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "test description"
}
}

View File

@ -0,0 +1,165 @@
{
"flavors": [
{
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/1",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "2",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/2",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/2",
"rel": "bookmark"
}
],
"name": "m1.small",
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 40,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "3",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/3",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/3",
"rel": "bookmark"
}
],
"name": "m1.medium",
"ram": 4096,
"swap": "",
"vcpus": 2,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 80,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "4",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/4",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/4",
"rel": "bookmark"
}
],
"name": "m1.large",
"ram": 8192,
"swap": "",
"vcpus": 4,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 160,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "5",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/5",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/5",
"rel": "bookmark"
}
],
"name": "m1.xlarge",
"ram": 16384,
"swap": "",
"vcpus": 8,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "6",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/6",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/6",
"rel": "bookmark"
}
],
"name": "m1.tiny.specs",
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "7",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/7",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/7",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "test description"
}
]
}

View File

@ -0,0 +1,109 @@
{
"flavors": [
{
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/1",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"description": null
},
{
"id": "2",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/2",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/2",
"rel": "bookmark"
}
],
"name": "m1.small",
"description": null
},
{
"id": "3",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/3",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/3",
"rel": "bookmark"
}
],
"name": "m1.medium",
"description": null
},
{
"id": "4",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/4",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/4",
"rel": "bookmark"
}
],
"name": "m1.large",
"description": null
},
{
"id": "5",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/5",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/5",
"rel": "bookmark"
}
],
"name": "m1.xlarge",
"description": null
},
{
"id": "6",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/6",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/6",
"rel": "bookmark"
}
],
"name": "m1.tiny.specs",
"description": null
},
{
"id": "7",
"links": [
{
"href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/flavors/7",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/7",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"description": "test description"
}
]
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.54",
"version": "2.55",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.54",
"version": "2.55",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -129,6 +129,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
id field, and takes a uuid in requests. PUT and GET requests
and responses are also changed.
* 2.54 - Enable reset key pair while rebuilding instance.
* 2.55 - Added flavor.description to GET/POST/PUT flavors APIs.
"""
# The minimum and maximum versions of the API supported
@ -137,7 +138,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.54"
_MAX_API_VERSION = "2.55"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@ -87,6 +87,13 @@ class FlavorActionController(wsgi.Controller):
self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
@wsgi.extends(action='update')
def update(self, req, id, body, resp_obj):
context = req.environ['nova.context']
if context.can(fa_policies.BASE_POLICY_NAME, fatal=False):
db_flavor = req.get_db_flavor(resp_obj.obj['flavor']['id'])
self._extend_flavor(resp_obj.obj['flavor'], db_flavor)
@extensions.expected_errors((400, 403, 404, 409))
@wsgi.action("addTenantAccess")
@validation.schema(flavor_access.add_tenant_access)

View File

@ -14,6 +14,7 @@ import webob
from oslo_log import log as logging
from nova.api.openstack import api_version_request
from nova.api.openstack.compute.schemas import flavor_manage
from nova.api.openstack.compute.views import flavors as flavors_view
from nova.api.openstack import extensions
@ -67,7 +68,9 @@ class FlavorManageController(wsgi.Controller):
@wsgi.action("create")
@extensions.expected_errors((400, 409))
@validation.schema(flavor_manage.create_v20, '2.0', '2.0')
@validation.schema(flavor_manage.create, '2.1')
@validation.schema(flavor_manage.create, '2.1', '2.54')
@validation.schema(flavor_manage.create_v2_55,
flavors_view.FLAVOR_DESCRIPTION_MICROVERSION)
def _create(self, req, body):
context = req.environ['nova.context']
# TODO(rb560u): remove this check in future release
@ -92,12 +95,18 @@ class FlavorManageController(wsgi.Controller):
rxtx_factor = vals.get('rxtx_factor', 1.0)
is_public = vals.get('os-flavor-access:is_public', True)
# The user can specify a description starting with microversion 2.55.
include_description = api_version_request.is_supported(
req, flavors_view.FLAVOR_DESCRIPTION_MICROVERSION)
description = vals.get('description') if include_description else None
try:
flavor = flavors.create(name, memory, vcpus, root_gb,
ephemeral_gb=ephemeral_gb,
flavorid=flavorid, swap=swap,
rxtx_factor=rxtx_factor,
is_public=is_public)
is_public=is_public,
description=description)
# NOTE(gmann): For backward compatibility, non public flavor
# access is not being added for created tenant. Ref -bug/1209101
req.cache_db_flavor(flavor)
@ -105,4 +114,27 @@ class FlavorManageController(wsgi.Controller):
exception.FlavorIdExists) as err:
raise webob.exc.HTTPConflict(explanation=err.format_message())
return self._view_builder.show(req, flavor)
return self._view_builder.show(req, flavor, include_description)
@wsgi.Controller.api_version(flavors_view.FLAVOR_DESCRIPTION_MICROVERSION)
@wsgi.action('update')
@extensions.expected_errors((400, 404))
@validation.schema(flavor_manage.update_v2_55,
flavors_view.FLAVOR_DESCRIPTION_MICROVERSION)
def _update(self, req, id, body):
# Validate the policy.
context = req.environ['nova.context']
context.can(fm_policies.POLICY_ROOT % 'update')
# Get the flavor and update the description.
try:
flavor = objects.Flavor.get_by_flavor_id(context, id)
flavor.description = body['flavor']['description']
flavor.save()
except exception.FlavorNotFound as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
# Cache the flavor so the flavor_access and flavor_rxtx extensions
# can add stuff to the response.
req.cache_db_flavor(flavor)
return self._view_builder.show(req, flavor, include_description=True)

View File

@ -42,6 +42,10 @@ class FlavorRxtxController(wsgi.Controller):
def create(self, req, resp_obj, body):
return self._show(req, resp_obj)
@wsgi.extends(action='update')
def update(self, req, id, body, resp_obj):
return self._show(req, resp_obj)
@wsgi.extends
def detail(self, req, resp_obj):
context = req.environ['nova.context']

View File

@ -16,6 +16,7 @@
from oslo_utils import strutils
import webob
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.views import flavors as flavors_view
from nova.api.openstack import extensions
@ -57,7 +58,9 @@ class FlavorsController(wsgi.Controller):
except exception.FlavorNotFound as e:
raise webob.exc.HTTPNotFound(explanation=e.format_message())
return self._view_builder.show(req, flavor)
include_description = api_version_request.is_supported(
req, flavors_view.FLAVOR_DESCRIPTION_MICROVERSION)
return self._view_builder.show(req, flavor, include_description)
def _parse_is_public(self, is_public):
"""Parse is_public into something usable."""

View File

@ -688,3 +688,16 @@ uniqueness across cells. This microversion brings the following changes:
----
Allow the user to set the server key pair while rebuilding.
2.55
----
Adds a ``description`` field to the flavor resource in the following APIs:
* ``GET /flavors``
* ``GET /flavors/detail``
* ``GET /flavors/{flavor_id}``
* ``POST /flavors``
* ``PUT /flavors/{flavor_id}``
The embedded flavor description will not be included in server representations.

View File

@ -429,6 +429,7 @@ ROUTE_LIST = (
}),
('/flavors/{id}', {
'GET': [flavor_controller, 'show'],
'PUT': [flavor_controller, 'update'],
'DELETE': [flavor_controller, 'delete']
}),
('/flavors/{id}/action', {

View File

@ -65,3 +65,37 @@ create = {
create_v20 = copy.deepcopy(create)
create_v20['properties']['flavor']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)
# 2.55 adds an optional description field with a max length of 65535 since the
# backing database column is a TEXT column which is 64KiB.
flavor_description = {
'type': ['string', 'null'], 'minLength': 0, 'maxLength': 65535,
'pattern': parameter_types.valid_description_regex,
}
create_v2_55 = copy.deepcopy(create)
create_v2_55['properties']['flavor']['properties']['description'] = (
flavor_description)
update_v2_55 = {
'type': 'object',
'properties': {
'flavor': {
'type': 'object',
'properties': {
'description': flavor_description
},
# Since the only property that can be specified on update is the
# description field, it is required. If we allow updating other
# flavor attributes in a later microversion, we should reconsider
# what is required.
'required': ['description'],
'additionalProperties': False,
},
},
'required': ['flavor'],
'additionalProperties': False,
}

View File

@ -13,15 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.openstack import api_version_request
from nova.api.openstack import common
FLAVOR_DESCRIPTION_MICROVERSION = '2.55'
class ViewBuilder(common.ViewBuilder):
_collection_name = "flavors"
def basic(self, request, flavor):
return {
def basic(self, request, flavor, include_description=False):
flavor_dict = {
"flavor": {
"id": flavor["flavorid"],
"name": flavor["name"],
@ -31,7 +34,12 @@ class ViewBuilder(common.ViewBuilder):
},
}
def show(self, request, flavor):
if include_description:
flavor_dict['flavor']['description'] = flavor.description
return flavor_dict
def show(self, request, flavor, include_description=False):
flavor_dict = {
"flavor": {
"id": flavor["flavorid"],
@ -48,19 +56,29 @@ class ViewBuilder(common.ViewBuilder):
},
}
if include_description:
flavor_dict['flavor']['description'] = flavor.description
return flavor_dict
def index(self, request, flavors):
"""Return the 'index' view of flavors."""
coll_name = self._collection_name
return self._list_view(self.basic, request, flavors, coll_name)
include_description = api_version_request.is_supported(
request, FLAVOR_DESCRIPTION_MICROVERSION)
return self._list_view(self.basic, request, flavors, coll_name,
include_description=include_description)
def detail(self, request, flavors):
"""Return the 'detail' view of flavors."""
coll_name = self._collection_name + '/detail'
return self._list_view(self.show, request, flavors, coll_name)
include_description = api_version_request.is_supported(
request, FLAVOR_DESCRIPTION_MICROVERSION)
return self._list_view(self.show, request, flavors, coll_name,
include_description=include_description)
def _list_view(self, func, request, flavors, coll_name):
def _list_view(self, func, request, flavors, coll_name,
include_description=False):
"""Provide a view for a list of flavors.
:param func: Function used to format the flavor data
@ -68,10 +86,13 @@ class ViewBuilder(common.ViewBuilder):
:param flavors: List of flavors in dictionary format
:param coll_name: Name of collection, used to generate the next link
for a pagination query
:param include_description: If the flavor.description should be
included in the response dict.
:returns: Flavor reply data in dictionary format
"""
flavor_list = [func(request, flavor)["flavor"] for flavor in flavors]
flavor_list = [func(request, flavor, include_description)["flavor"]
for flavor in flavors]
flavors_links = self._get_collection_links(request,
flavors,
coll_name,

View File

@ -69,7 +69,7 @@ system_metadata_flavor_extra_props = [
def create(name, memory, vcpus, root_gb, ephemeral_gb=0, flavorid=None,
swap=0, rxtx_factor=1.0, is_public=True):
swap=0, rxtx_factor=1.0, is_public=True, description=None):
"""Creates flavors."""
if not flavorid:
flavorid = uuidutils.generate_uuid()
@ -81,6 +81,7 @@ def create(name, memory, vcpus, root_gb, ephemeral_gb=0, flavorid=None,
'ephemeral_gb': ephemeral_gb,
'swap': swap,
'rxtx_factor': rxtx_factor,
'description': description
}
if isinstance(name, six.string_types):

View File

@ -71,6 +71,10 @@ to a flavor via an os-flavor-access API.
'method': 'POST',
'path': '/flavors'
},
{
'method': 'PUT',
'path': '/flavors/{flavor_id}'
},
]),
]

View File

@ -52,6 +52,16 @@ flavor_manage_policies = [
'path': '/flavors'
}
]),
policy.DocumentedRuleDefault(
POLICY_ROOT % 'update',
base.RULE_ADMIN_API,
"Update a flavor",
[
{
'method': 'PUT',
'path': '/flavors/{flavor_id}'
}
]),
policy.DocumentedRuleDefault(
POLICY_ROOT % 'delete',
BASE_POLICY_RULE,

View File

@ -40,6 +40,10 @@ flavor_rxtx_policies = [
'method': 'POST',
'path': '/flavors'
},
{
'method': 'PUT',
'path': '/flavors/{flavor_id}'
},
]),
]

View File

@ -0,0 +1,11 @@
{
"flavor": {
"name": "%(flavor_name)s",
"ram": 1024,
"vcpus": 2,
"disk": 10,
"id": "%(flavor_id)s",
"rxtx_factor": 2.0,
"description": "test description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"disk": 10,
"id": "%(flavor_id)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/%(flavor_id)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/%(flavor_id)s",
"rel": "bookmark"
}
],
"name": "%(flavor_name)s",
"os-flavor-access:is_public": true,
"ram": 1024,
"vcpus": 2,
"OS-FLV-DISABLED:disabled": false,
"OS-FLV-EXT-DATA:ephemeral": 0,
"swap": "",
"rxtx_factor": 2.0,
"description": "test description"
}
}

View File

@ -0,0 +1,5 @@
{
"flavor": {
"description": "updated description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"os-flavor-access:is_public": true,
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/flavors/1",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "updated description"
}
}

View File

@ -0,0 +1,25 @@
{
"flavor": {
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "%(flavorid)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/%(flavorid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/%(flavorid)s",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"os-flavor-access:is_public": true,
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "test description"
}
}

View File

@ -0,0 +1,165 @@
{
"flavors": [
{
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "1",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/1",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"os-flavor-access:is_public": true,
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "2",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/2",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/2",
"rel": "bookmark"
}
],
"name": "m1.small",
"os-flavor-access:is_public": true,
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 40,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "3",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/3",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/3",
"rel": "bookmark"
}
],
"name": "m1.medium",
"os-flavor-access:is_public": true,
"ram": 4096,
"swap": "",
"vcpus": 2,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 80,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "4",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/4",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/4",
"rel": "bookmark"
}
],
"name": "m1.large",
"os-flavor-access:is_public": true,
"ram": 8192,
"swap": "",
"vcpus": 4,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 160,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "5",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/5",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/5",
"rel": "bookmark"
}
],
"name": "m1.xlarge",
"os-flavor-access:is_public": true,
"ram": 16384,
"swap": "",
"vcpus": 8,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 1,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "6",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/6",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/6",
"rel": "bookmark"
}
],
"name": "m1.tiny.specs",
"os-flavor-access:is_public": true,
"ram": 512,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": null
},
{
"OS-FLV-DISABLED:disabled": false,
"disk": 20,
"OS-FLV-EXT-DATA:ephemeral": 0,
"id": "%(flavorid)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/%(flavorid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/%(flavorid)s",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"os-flavor-access:is_public": true,
"ram": 2048,
"swap": "",
"vcpus": 1,
"rxtx_factor": 1.0,
"description": "test description"
}
]
}

View File

@ -0,0 +1,109 @@
{
"flavors": [
{
"id": "1",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/1",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
],
"name": "m1.tiny",
"description": null
},
{
"id": "2",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/2",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/2",
"rel": "bookmark"
}
],
"name": "m1.small",
"description": null
},
{
"id": "3",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/3",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/3",
"rel": "bookmark"
}
],
"name": "m1.medium",
"description": null
},
{
"id": "4",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/4",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/4",
"rel": "bookmark"
}
],
"name": "m1.large",
"description": null
},
{
"id": "5",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/5",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/5",
"rel": "bookmark"
}
],
"name": "m1.xlarge",
"description": null
},
{
"id": "6",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/6",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/6",
"rel": "bookmark"
}
],
"name": "m1.tiny.specs",
"description": null
},
{
"id": "%(flavorid)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/flavors/%(flavorid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/flavors/%(flavorid)s",
"rel": "bookmark"
}
],
"name": "m1.small.description",
"description": "test description"
}
]
}

View File

@ -37,3 +37,12 @@ class FlavorManageSampleJsonTests(api_sample_base.ApiSampleTestBaseV21):
response = self._do_delete("flavors/10")
self.assertEqual(202, response.status_code)
self.assertEqual('', response.text)
class FlavorManageSampleJsonTests2_55(FlavorManageSampleJsonTests):
microversion = '2.55'
scenarios = [('v2_55', {'api_major_version': 'v2.1'})]
def test_update_flavor_description(self):
response = self._do_put("flavors/1", "flavor-update-req", {})
self._verify_response("flavor-update-resp", {}, response, 200)

View File

@ -13,20 +13,47 @@
# License for the specific language governing permissions and limitations
# under the License.
from nova import context as nova_context
from nova import objects
from nova.tests.functional.api_sample_tests import api_sample_base
class FlavorsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
sample_dir = 'flavors'
flavor_show_id = '1'
subs = {}
def test_flavors_get(self):
response = self._do_get('flavors/1')
self._verify_response('flavor-get-resp', {}, response, 200)
response = self._do_get('flavors/%s' % self.flavor_show_id)
self._verify_response('flavor-get-resp', self.subs, response, 200)
def test_flavors_list(self):
response = self._do_get('flavors')
self._verify_response('flavors-list-resp', {}, response, 200)
self._verify_response('flavors-list-resp', self.subs, response, 200)
def test_flavors_detail(self):
response = self._do_get('flavors/detail')
self._verify_response('flavors-detail-resp', {}, response, 200)
self._verify_response('flavors-detail-resp', self.subs, response,
200)
class FlavorsSampleJsonTest2_55(FlavorsSampleJsonTest):
microversion = '2.55'
scenarios = [('v2_55', {'api_major_version': 'v2.1'})]
def setUp(self):
super(FlavorsSampleJsonTest2_55, self).setUp()
# Get the existing flavors created by DefaultFlavorsFixture.
ctxt = nova_context.get_admin_context()
flavors = objects.FlavorList.get_all(ctxt)
# Flavors are sorted by flavorid in ascending order by default, so
# get the last flavor in the list and create a new flavor with an
# incremental flavorid so we have a predictable sort order for the
# sample response.
new_flavor_id = int(flavors[-1].flavorid) + 1
new_flavor = objects.Flavor(
ctxt, memory_mb=2048, vcpus=1, root_gb=20, flavorid=new_flavor_id,
name='m1.small.description', description='test description')
new_flavor.create()
self.flavor_show_id = new_flavor_id
self.subs = {'flavorid': new_flavor_id}

View File

@ -86,3 +86,47 @@ class TestFlavorNotificationSample(
self._verify_notification(
'flavor-update', actual=fake_notifier.VERSIONED_NOTIFICATIONS[2])
class TestFlavorNotificationSamplev2_55(
notification_sample_base.NotificationSampleTestBase):
"""Tests PUT /flavors/{flavor_id} with a description."""
MAX_MICROVERSION = '2.55'
def test_flavor_udpate_with_description(self):
# First create a flavor without a description.
body = {
"flavor": {
"name": "test_flavor",
"ram": 1024,
"vcpus": 2,
"disk": 10,
"id": "a22d5517-147c-4147-a0d1-e698df5cd4e3",
"os-flavor-access:is_public": False,
"rxtx_factor": 2.0
}
}
# Create a flavor.
flavor = self.admin_api.api_post('flavors', body).body['flavor']
# Check the notification; should be the same as the sample where there
# is no description set.
self.assertEqual(1, len(fake_notifier.VERSIONED_NOTIFICATIONS))
self._verify_notification(
'flavor-create',
replacements={'is_public': False},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[0])
# Update and set the flavor description.
self.admin_api.api_put(
'flavors/%s' % flavor['id'],
{'flavor': {'description': 'test description'}}).body['flavor']
# Assert the notifications, one for create and one for update.
self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS))
self._verify_notification(
'flavor-update',
replacements={'description': 'test description',
'extra_specs': {},
'projects': []},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[1])

View File

@ -18,11 +18,13 @@ from oslo_serialization import jsonutils
import six
import webob
from nova.api.openstack import api_version_request
from nova.api.openstack.compute import flavor_access as flavor_access_v21
from nova.api.openstack.compute import flavor_manage as flavormanage_v21
from nova.compute import flavors
from nova import db
from nova import exception
from nova import objects
from nova import policy
from nova import test
from nova.tests.unit.api.openstack import fakes
@ -45,6 +47,7 @@ class FlavorManageTestV21(test.NoDBTestCase):
controller = flavormanage_v21.FlavorManageController()
validation_error = exception.ValidationError
base_url = '/v2/fake/flavors'
microversion = '2.1'
def setUp(self):
super(FlavorManageTestV21, self).setUp()
@ -66,7 +69,8 @@ class FlavorManageTestV21(test.NoDBTestCase):
self.expected_flavor = self.request_body
def _get_http_request(self, url=''):
return fakes.HTTPRequest.blank(url)
return fakes.HTTPRequest.blank(url, version=self.microversion,
use_admin_context=True)
@property
def app(self):
@ -126,6 +130,7 @@ class FlavorManageTestV21(test.NoDBTestCase):
def _create_flavor_success_case(self, body, req=None):
req = req if req else self._get_http_request(url=self.base_url)
req.headers['Content-Type'] = 'application/json'
req.headers['X-OpenStack-Nova-API-Version'] = self.microversion
req.method = 'POST'
req.body = jsonutils.dump_as_bytes(body)
res = req.get_response(self.app)
@ -293,7 +298,7 @@ class FlavorManageTestV21(test.NoDBTestCase):
}
def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb,
flavorid, swap, rxtx_factor, is_public):
flavorid, swap, rxtx_factor, is_public, description):
raise exception.FlavorExists(name=name)
self.stub_out('nova.compute.flavors.create', fake_create)
@ -313,6 +318,111 @@ class FlavorManageTestV21(test.NoDBTestCase):
self.assertRaises(exception.InvalidInput, flavors.create, "abcdef",
"test_memory_mb", 2, None, 1, 1234, 512, 1, True)
def test_create_with_description(self):
"""With microversion <2.55 this should return a failure."""
self.request_body['flavor']['description'] = 'invalid'
ex = self.assertRaises(
self.validation_error, self.controller._create,
self._get_http_request(), body=self.request_body)
self.assertIn('description', six.text_type(ex))
def test_flavor_update_description(self):
"""With microversion <2.55 this should return a failure."""
flavor = self._create_flavor_success_case(self.request_body)['flavor']
self.assertRaises(
exception.VersionNotFoundForAPIMethod, self.controller._update,
self._get_http_request(), flavor['id'],
body={'flavor': {'description': 'nope'}})
class FlavorManageTestV2_55(FlavorManageTestV21):
microversion = '2.55'
def setUp(self):
super(FlavorManageTestV2_55, self).setUp()
# Send a description in POST /flavors requests.
self.request_body['flavor']['description'] = 'test description'
def test_create_with_description(self):
# test_create already tests this.
pass
@mock.patch('nova.objects.Flavor.get_by_flavor_id')
@mock.patch('nova.objects.Flavor.save')
def test_flavor_update_description(self, mock_flavor_save, mock_get):
"""Tests updating a flavor description."""
# First create a flavor.
flavor = self._create_flavor_success_case(self.request_body)['flavor']
self.assertEqual('test description', flavor['description'])
mock_get.return_value = objects.Flavor(
flavorid=flavor['id'], name=flavor['name'],
memory_mb=flavor['ram'], vcpus=flavor['vcpus'],
root_gb=flavor['disk'], swap=flavor['swap'],
ephemeral_gb=flavor['OS-FLV-EXT-DATA:ephemeral'],
disabled=flavor['OS-FLV-DISABLED:disabled'],
is_public=flavor['os-flavor-access:is_public'],
description=flavor['description'])
# Now null out the flavor description.
flavor = self.controller._update(
self._get_http_request(), flavor['id'],
body={'flavor': {'description': None}})['flavor']
self.assertIsNone(flavor['description'])
mock_get.assert_called_once_with(
test.MatchType(fakes.FakeRequestContext), flavor['id'])
mock_flavor_save.assert_called_once_with()
@mock.patch('nova.objects.Flavor.get_by_flavor_id',
side_effect=exception.FlavorNotFound(flavor_id='notfound'))
def test_flavor_update_not_found(self, mock_get):
"""Tests that a 404 is returned if the flavor is not found."""
self.assertRaises(webob.exc.HTTPNotFound,
self.controller._update,
self._get_http_request(), 'notfound',
body={'flavor': {'description': None}})
def test_flavor_update_missing_description(self):
"""Tests that a schema validation error is raised if no description
is provided in the update request body.
"""
self.assertRaises(self.validation_error,
self.controller._update,
self._get_http_request(), 'invalid',
body={'flavor': {}})
def test_create_with_invalid_description(self):
# NOTE(mriedem): Intentionally not using ddt for this since ddt will
# create a test name that has 65536 'a's in the name which blows up
# the console output.
for description in ('bad !@#!$%\x00 description', # printable chars
'a' * 65536): # maxLength
self.request_body['flavor']['description'] = description
self.assertRaises(self.validation_error, self.controller._create,
self._get_http_request(), body=self.request_body)
@mock.patch('nova.objects.Flavor.get_by_flavor_id')
@mock.patch('nova.objects.Flavor.save')
def test_update_with_invalid_description(self, mock_flavor_save, mock_get):
# First create a flavor.
flavor = self._create_flavor_success_case(self.request_body)['flavor']
self.assertEqual('test description', flavor['description'])
mock_get.return_value = objects.Flavor(
flavorid=flavor['id'], name=flavor['name'],
memory_mb=flavor['ram'], vcpus=flavor['vcpus'],
root_gb=flavor['disk'], swap=flavor['swap'],
ephemeral_gb=flavor['OS-FLV-EXT-DATA:ephemeral'],
disabled=flavor['OS-FLV-DISABLED:disabled'],
is_public=flavor['os-flavor-access:is_public'],
description=flavor['description'])
# NOTE(mriedem): Intentionally not using ddt for this since ddt will
# create a test name that has 65536 'a's in the name which blows up
# the console output.
for description in ('bad !@#!$%\x00 description', # printable chars
'a' * 65536): # maxLength
self.request_body['flavor']['description'] = description
self.assertRaises(self.validation_error, self.controller._update,
self._get_http_request(), flavor['id'],
body={'flavor': {'description': description}})
class PrivateFlavorManageTestV21(test.TestCase):
controller = flavormanage_v21.FlavorManageController()
@ -574,3 +684,17 @@ class FlavorManagerPolicyEnforcementV21(test.TestCase):
self.assertEqual(
"Policy doesn't allow %s to be performed." % delete_flavor_policy,
exc.format_message())
def test_flavor_update_non_admin_fails(self):
"""Tests that trying to update a flavor as a non-admin fails due
to the default policy.
"""
self.req.api_version_request = api_version_request.APIVersionRequest(
'2.55')
exc = self.assertRaises(
exception.PolicyNotAuthorized,
self.controller._update, self.req, 'fake_id',
body={"flavor": {"description": "not authorized"}})
self.assertEqual(
"Policy doesn't allow os_compute_api:os-flavor-manage:update to "
"be performed.", exc.format_message())

View File

@ -49,6 +49,9 @@ class FlavorsTestV21(test.TestCase):
fake_request = fakes.HTTPRequestV21
_rspv = "v2/fake"
_fake = "/fake"
microversion = '2.1'
# Flag to tell the test if a description should be expected in a response.
expect_description = False
def setUp(self):
super(FlavorsTestV21, self).setUp()
@ -57,6 +60,10 @@ class FlavorsTestV21(test.TestCase):
fakes.stub_out_flavor_get_by_flavor_id(self)
self.controller = self.Controller()
def _build_request(self, url):
return self.fake_request.blank(
self._prefix + url, version=self.microversion)
def _set_expected_body(self, expected, flavor):
# NOTE(oomichi): On v2.1 API, some extensions of v2.0 are merged
# as core features and we can get the following parameters as the
@ -64,16 +71,18 @@ class FlavorsTestV21(test.TestCase):
expected['OS-FLV-EXT-DATA:ephemeral'] = flavor.ephemeral_gb
expected['OS-FLV-DISABLED:disabled'] = flavor.disabled
expected['swap'] = flavor.swap
if self.expect_description:
expected['description'] = flavor.description
@mock.patch('nova.objects.Flavor.get_by_flavor_id',
side_effect=return_flavor_not_found)
def test_get_flavor_by_invalid_id(self, mock_get):
req = self.fake_request.blank(self._prefix + '/flavors/asdf')
req = self._build_request('/flavors/asdf')
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show, req, 'asdf')
def test_get_flavor_by_id(self):
req = self.fake_request.blank(self._prefix + '/flavors/1')
req = self._build_request('/flavors/1')
flavor = self.controller.show(req, '1')
expected = {
"flavor": {
@ -103,7 +112,7 @@ class FlavorsTestV21(test.TestCase):
self.flags(compute_link_prefix='http://zoo.com:42',
glance_link_prefix='http://circus.com:34',
group='api')
req = self.fake_request.blank(self._prefix + '/flavors/1')
req = self._build_request('/flavors/1')
flavor = self.controller.show(req, '1')
expected = {
"flavor": {
@ -130,7 +139,7 @@ class FlavorsTestV21(test.TestCase):
self.assertEqual(expected, flavor)
def test_get_flavor_list(self):
req = self.fake_request.blank(self._prefix + '/flavors')
req = self._build_request('/flavors')
flavor = self.controller.index(req)
expected = {
"flavors": [
@ -168,12 +177,16 @@ class FlavorsTestV21(test.TestCase):
},
],
}
if self.expect_description:
for idx, _flavor in enumerate(expected['flavors']):
expected['flavors'][idx]['description'] = (
fakes.FLAVORS[_flavor['id']].description)
self.assertEqual(flavor, expected)
def test_get_flavor_list_with_marker(self):
self.maxDiff = None
url = self._prefix + '/flavors?limit=1&marker=1'
req = self.fake_request.blank(url)
url = '/flavors?limit=1&marker=1'
req = self._build_request(url)
flavor = self.controller.index(req)
expected = {
"flavors": [
@ -200,16 +213,19 @@ class FlavorsTestV21(test.TestCase):
'rel': 'next'}
]
}
if self.expect_description:
expected['flavors'][0]['description'] = (
fakes.FLAVORS['2'].description)
self.assertThat(flavor, matchers.DictMatches(expected))
def test_get_flavor_list_with_invalid_marker(self):
req = self.fake_request.blank(self._prefix + '/flavors?marker=99999')
req = self._build_request('/flavors?marker=99999')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
def test_get_flavor_detail_with_limit(self):
url = self._prefix + '/flavors/detail?limit=1'
req = self.fake_request.blank(url)
url = '/flavors/detail?limit=1'
req = self._build_request(url)
response = self.controller.detail(req)
response_list = response["flavors"]
response_links = response["flavors_links"]
@ -247,7 +263,7 @@ class FlavorsTestV21(test.TestCase):
matchers.DictMatches(params))
def test_get_flavor_with_limit(self):
req = self.fake_request.blank(self._prefix + '/flavors?limit=2')
req = self._build_request('/flavors?limit=2')
response = self.controller.index(req)
response_list = response["flavors"]
response_links = response["flavors_links"]
@ -286,6 +302,10 @@ class FlavorsTestV21(test.TestCase):
],
}
]
if self.expect_description:
for idx, _flavor in enumerate(expected_flavors):
expected_flavors[idx]['description'] = (
fakes.FLAVORS[_flavor['id']].description)
self.assertEqual(response_list, expected_flavors)
self.assertEqual(response_links[0]['rel'], 'next')
@ -330,7 +350,7 @@ class FlavorsTestV21(test.TestCase):
matchers.DictMatches(params))
def test_get_flavor_list_detail(self):
req = self.fake_request.blank(self._prefix + '/flavors/detail')
req = self._build_request('/flavors/detail')
flavor = self.controller.detail(req)
expected = {
"flavors": [
@ -381,14 +401,14 @@ class FlavorsTestV21(test.TestCase):
@mock.patch('nova.objects.FlavorList.get_all',
return_value=objects.FlavorList())
def test_get_empty_flavor_list(self, mock_get):
req = self.fake_request.blank(self._prefix + '/flavors')
req = self._build_request('/flavors')
flavors = self.controller.index(req)
expected = {'flavors': []}
self.assertEqual(flavors, expected)
def test_get_flavor_list_filter_min_ram(self):
# Flavor lists may be filtered by minRam.
req = self.fake_request.blank(self._prefix + '/flavors?minRam=512')
req = self._build_request('/flavors?minRam=512')
flavor = self.controller.index(req)
expected = {
"flavors": [
@ -410,17 +430,20 @@ class FlavorsTestV21(test.TestCase):
},
],
}
if self.expect_description:
expected['flavors'][0]['description'] = (
fakes.FLAVORS['2'].description)
self.assertEqual(flavor, expected)
def test_get_flavor_list_filter_invalid_min_ram(self):
# Ensure you cannot list flavors with invalid minRam param.
req = self.fake_request.blank(self._prefix + '/flavors?minRam=NaN')
req = self._build_request('/flavors?minRam=NaN')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
def test_get_flavor_list_filter_min_disk(self):
# Flavor lists may be filtered by minDisk.
req = self.fake_request.blank(self._prefix + '/flavors?minDisk=20')
req = self._build_request('/flavors?minDisk=20')
flavor = self.controller.index(req)
expected = {
"flavors": [
@ -442,11 +465,14 @@ class FlavorsTestV21(test.TestCase):
},
],
}
if self.expect_description:
expected['flavors'][0]['description'] = (
fakes.FLAVORS['2'].description)
self.assertEqual(flavor, expected)
def test_get_flavor_list_filter_invalid_min_disk(self):
# Ensure you cannot list flavors with invalid minDisk param.
req = self.fake_request.blank(self._prefix + '/flavors?minDisk=NaN')
req = self._build_request('/flavors?minDisk=NaN')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
@ -454,8 +480,7 @@ class FlavorsTestV21(test.TestCase):
"""Tests that filtering work on flavor details and that minRam and
minDisk filters can be combined
"""
req = self.fake_request.blank(self._prefix + '/flavors/detail'
'?minRam=256&minDisk=20')
req = self._build_request('/flavors/detail?minRam=256&minDisk=20')
flavor = self.controller.detail(req)
expected = {
"flavors": [
@ -484,6 +509,12 @@ class FlavorsTestV21(test.TestCase):
self.assertEqual(expected, flavor)
class FlavorsTestV2_55(FlavorsTestV21):
"""Run the same tests as we would for v2.1 but with a description."""
microversion = '2.55'
expect_description = True
class DisabledFlavorsWithRealDBTestV21(test.TestCase):
"""Tests that disabled flavors should not be shown nor listed."""
Controller = flavors_v21.FlavorsController

View File

@ -716,6 +716,7 @@ FLAVORS = {
vcpu_weight=None,
disabled=False,
is_public=True,
description=None
),
'2': objects.Flavor(
id=2,
@ -730,6 +731,7 @@ FLAVORS = {
vcpu_weight=None,
disabled=True,
is_public=True,
description='flavor 2 description'
),
}

View File

@ -313,6 +313,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:os-flavor-extra-specs:delete",
"os_compute_api:os-flavor-manage",
"os_compute_api:os-flavor-manage:create",
"os_compute_api:os-flavor-manage:update",
"os_compute_api:os-flavor-manage:delete",
"os_compute_api:os-floating-ips-bulk",
"os_compute_api:os-floating-ip-dns:domain:delete",

View File

@ -0,0 +1,17 @@
---
features:
- |
Microversion 2.55 adds a ``description`` field to the flavor resource in
the following APIs:
* ``GET /flavors``
* ``GET /flavors/detail``
* ``GET /flavors/{flavor_id}``
* ``POST /flavors``
* ``PUT /flavors/{flavor_id}``
The embedded flavor description will not be included in server
representations.
A new policy rule ``os_compute_api:os-flavor-manage:update`` is added
to control access to the ``PUT /flavors/{flavor_id}`` API.