From 034d7f37954e8bb46bb18df261adea51a0e3981f Mon Sep 17 00:00:00 2001 From: Matt Riedemann <mriedem.os@gmail.com> Date: Wed, 25 Oct 2017 16:59:31 -0400 Subject: [PATCH] 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 --- api-ref/source/flavors.inc | 84 +++++++-- api-ref/source/parameters.yaml | 23 +++ .../v2.55/flavor-create-post-req.json | 11 ++ .../v2.55/flavor-create-post-resp.json | 25 +++ .../v2.55/flavor-update-req.json | 5 + .../v2.55/flavor-update-resp.json | 25 +++ .../flavors/v2.55/flavor-get-resp.json | 25 +++ .../flavors/v2.55/flavors-detail-resp.json | 165 ++++++++++++++++++ .../flavors/v2.55/flavors-list-resp.json | 109 ++++++++++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 3 +- nova/api/openstack/compute/flavor_access.py | 7 + nova/api/openstack/compute/flavor_manage.py | 38 +++- nova/api/openstack/compute/flavor_rxtx.py | 4 + nova/api/openstack/compute/flavors.py | 5 +- .../compute/rest_api_version_history.rst | 13 ++ nova/api/openstack/compute/routes.py | 1 + .../compute/schemas/flavor_manage.py | 34 ++++ nova/api/openstack/compute/views/flavors.py | 35 +++- nova/compute/flavors.py | 3 +- nova/policies/flavor_access.py | 4 + nova/policies/flavor_manage.py | 10 ++ nova/policies/flavor_rxtx.py | 4 + .../v2.55/flavor-create-post-req.json.tpl | 11 ++ .../v2.55/flavor-create-post-resp.json.tpl | 25 +++ .../v2.55/flavor-update-req.json.tpl | 5 + .../v2.55/flavor-update-resp.json.tpl | 25 +++ .../flavors/v2.55/flavor-get-resp.json.tpl | 25 +++ .../v2.55/flavors-detail-resp.json.tpl | 165 ++++++++++++++++++ .../flavors/v2.55/flavors-list-resp.json.tpl | 109 ++++++++++++ .../api_sample_tests/test_flavor_manage.py | 9 + .../api_sample_tests/test_flavors.py | 35 +++- .../notification_sample_tests/test_flavor.py | 44 +++++ .../openstack/compute/test_flavor_manage.py | 128 +++++++++++++- .../api/openstack/compute/test_flavors.py | 67 +++++-- nova/tests/unit/api/openstack/fakes.py | 2 + nova/tests/unit/test_policy.py | 1 + .../flavor-description-02f8b8626da71a25.yaml | 17 ++ 39 files changed, 1253 insertions(+), 52 deletions(-) create mode 100644 doc/api_samples/flavor-manage/v2.55/flavor-create-post-req.json create mode 100644 doc/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json create mode 100644 doc/api_samples/flavor-manage/v2.55/flavor-update-req.json create mode 100644 doc/api_samples/flavor-manage/v2.55/flavor-update-resp.json create mode 100644 doc/api_samples/flavors/v2.55/flavor-get-resp.json create mode 100644 doc/api_samples/flavors/v2.55/flavors-detail-resp.json create mode 100644 doc/api_samples/flavors/v2.55/flavors-list-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavor-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-detail-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-list-resp.json.tpl create mode 100644 releasenotes/notes/flavor-description-02f8b8626da71a25.yaml diff --git a/api-ref/source/flavors.inc b/api-ref/source/flavors.inc index 6ef3feef1c7e..941e54783bab 100644 --- a/api-ref/source/flavors.inc +++ b/api-ref/source/flavors.inc @@ -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 diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 774506e730ad..68b20fe20119 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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 diff --git a/doc/api_samples/flavor-manage/v2.55/flavor-create-post-req.json b/doc/api_samples/flavor-manage/v2.55/flavor-create-post-req.json new file mode 100644 index 000000000000..0d9926d72027 --- /dev/null +++ b/doc/api_samples/flavor-manage/v2.55/flavor-create-post-req.json @@ -0,0 +1,11 @@ +{ + "flavor": { + "name": "test_flavor", + "ram": 1024, + "vcpus": 2, + "disk": 10, + "id": "10", + "rxtx_factor": 2.0, + "description": "test description" + } +} diff --git a/doc/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json b/doc/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json new file mode 100644 index 000000000000..18ff7727cbae --- /dev/null +++ b/doc/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json @@ -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" + } +} diff --git a/doc/api_samples/flavor-manage/v2.55/flavor-update-req.json b/doc/api_samples/flavor-manage/v2.55/flavor-update-req.json new file mode 100644 index 000000000000..93c8e1e8ab23 --- /dev/null +++ b/doc/api_samples/flavor-manage/v2.55/flavor-update-req.json @@ -0,0 +1,5 @@ +{ + "flavor": { + "description": "updated description" + } +} diff --git a/doc/api_samples/flavor-manage/v2.55/flavor-update-resp.json b/doc/api_samples/flavor-manage/v2.55/flavor-update-resp.json new file mode 100644 index 000000000000..27c903d934cb --- /dev/null +++ b/doc/api_samples/flavor-manage/v2.55/flavor-update-resp.json @@ -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" + } +} diff --git a/doc/api_samples/flavors/v2.55/flavor-get-resp.json b/doc/api_samples/flavors/v2.55/flavor-get-resp.json new file mode 100644 index 000000000000..575777240945 --- /dev/null +++ b/doc/api_samples/flavors/v2.55/flavor-get-resp.json @@ -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" + } +} diff --git a/doc/api_samples/flavors/v2.55/flavors-detail-resp.json b/doc/api_samples/flavors/v2.55/flavors-detail-resp.json new file mode 100644 index 000000000000..2f181cca91fe --- /dev/null +++ b/doc/api_samples/flavors/v2.55/flavors-detail-resp.json @@ -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" + } + ] +} diff --git a/doc/api_samples/flavors/v2.55/flavors-list-resp.json b/doc/api_samples/flavors/v2.55/flavors-list-resp.json new file mode 100644 index 000000000000..f368ed5c66fd --- /dev/null +++ b/doc/api_samples/flavors/v2.55/flavors-list-resp.json @@ -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" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 92d5a3e2eff7..f0bf9cd8a149 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.54", + "version": "2.55", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 43956e0fb4b6..30fa5b2cc6c2 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.54", + "version": "2.55", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index b72586934068..c3444df22804 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/compute/flavor_access.py b/nova/api/openstack/compute/flavor_access.py index b591212d6919..7ad7ff1cc38a 100644 --- a/nova/api/openstack/compute/flavor_access.py +++ b/nova/api/openstack/compute/flavor_access.py @@ -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) diff --git a/nova/api/openstack/compute/flavor_manage.py b/nova/api/openstack/compute/flavor_manage.py index b55da6a2ec8a..58ad59743006 100644 --- a/nova/api/openstack/compute/flavor_manage.py +++ b/nova/api/openstack/compute/flavor_manage.py @@ -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) diff --git a/nova/api/openstack/compute/flavor_rxtx.py b/nova/api/openstack/compute/flavor_rxtx.py index 374a561fd9a6..497490f30cf9 100644 --- a/nova/api/openstack/compute/flavor_rxtx.py +++ b/nova/api/openstack/compute/flavor_rxtx.py @@ -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'] diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py index f9b7e8bee732..d0f31f6eae7a 100644 --- a/nova/api/openstack/compute/flavors.py +++ b/nova/api/openstack/compute/flavors.py @@ -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.""" diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 8990fd7e0275..3f76b1683d2d 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -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. diff --git a/nova/api/openstack/compute/routes.py b/nova/api/openstack/compute/routes.py index 7b7d75cca694..929f825a8f19 100644 --- a/nova/api/openstack/compute/routes.py +++ b/nova/api/openstack/compute/routes.py @@ -429,6 +429,7 @@ ROUTE_LIST = ( }), ('/flavors/{id}', { 'GET': [flavor_controller, 'show'], + 'PUT': [flavor_controller, 'update'], 'DELETE': [flavor_controller, 'delete'] }), ('/flavors/{id}/action', { diff --git a/nova/api/openstack/compute/schemas/flavor_manage.py b/nova/api/openstack/compute/schemas/flavor_manage.py index 5eb53263e9cd..347f557efce3 100644 --- a/nova/api/openstack/compute/schemas/flavor_manage.py +++ b/nova/api/openstack/compute/schemas/flavor_manage.py @@ -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, +} diff --git a/nova/api/openstack/compute/views/flavors.py b/nova/api/openstack/compute/views/flavors.py index 0295929cf788..04f10831eff8 100644 --- a/nova/api/openstack/compute/views/flavors.py +++ b/nova/api/openstack/compute/views/flavors.py @@ -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, diff --git a/nova/compute/flavors.py b/nova/compute/flavors.py index 281d12b3f44b..5bc08e4f74ee 100644 --- a/nova/compute/flavors.py +++ b/nova/compute/flavors.py @@ -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): diff --git a/nova/policies/flavor_access.py b/nova/policies/flavor_access.py index 49f605c78439..5a780b293a44 100644 --- a/nova/policies/flavor_access.py +++ b/nova/policies/flavor_access.py @@ -71,6 +71,10 @@ to a flavor via an os-flavor-access API. 'method': 'POST', 'path': '/flavors' }, + { + 'method': 'PUT', + 'path': '/flavors/{flavor_id}' + }, ]), ] diff --git a/nova/policies/flavor_manage.py b/nova/policies/flavor_manage.py index ba12143e58ce..f91f26e71573 100644 --- a/nova/policies/flavor_manage.py +++ b/nova/policies/flavor_manage.py @@ -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, diff --git a/nova/policies/flavor_rxtx.py b/nova/policies/flavor_rxtx.py index 8ad443a25a93..8a654a867bfd 100644 --- a/nova/policies/flavor_rxtx.py +++ b/nova/policies/flavor_rxtx.py @@ -40,6 +40,10 @@ flavor_rxtx_policies = [ 'method': 'POST', 'path': '/flavors' }, + { + 'method': 'PUT', + 'path': '/flavors/{flavor_id}' + }, ]), ] diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-req.json.tpl new file mode 100644 index 000000000000..2065b445cf3e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-req.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json.tpl new file mode 100644 index 000000000000..2fddca9b4cfd --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-create-post-resp.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-req.json.tpl new file mode 100644 index 000000000000..93c8e1e8ab23 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-req.json.tpl @@ -0,0 +1,5 @@ +{ + "flavor": { + "description": "updated description" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-resp.json.tpl new file mode 100644 index 000000000000..27c903d934cb --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavor-manage/v2.55/flavor-update-resp.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavor-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavor-get-resp.json.tpl new file mode 100644 index 000000000000..1de13dd0e23d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavor-get-resp.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-detail-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-detail-resp.json.tpl new file mode 100644 index 000000000000..164520a63d0c --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-detail-resp.json.tpl @@ -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" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-list-resp.json.tpl new file mode 100644 index 000000000000..79a23e8760ba --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/flavors/v2.55/flavors-list-resp.json.tpl @@ -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" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_flavor_manage.py b/nova/tests/functional/api_sample_tests/test_flavor_manage.py index 7b3e6149957a..6ecb1ddb4186 100644 --- a/nova/tests/functional/api_sample_tests/test_flavor_manage.py +++ b/nova/tests/functional/api_sample_tests/test_flavor_manage.py @@ -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) diff --git a/nova/tests/functional/api_sample_tests/test_flavors.py b/nova/tests/functional/api_sample_tests/test_flavors.py index 4745c8a52b5d..b2b42975b924 100644 --- a/nova/tests/functional/api_sample_tests/test_flavors.py +++ b/nova/tests/functional/api_sample_tests/test_flavors.py @@ -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} diff --git a/nova/tests/functional/notification_sample_tests/test_flavor.py b/nova/tests/functional/notification_sample_tests/test_flavor.py index b4011fa6b77e..e10e8df76007 100644 --- a/nova/tests/functional/notification_sample_tests/test_flavor.py +++ b/nova/tests/functional/notification_sample_tests/test_flavor.py @@ -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]) diff --git a/nova/tests/unit/api/openstack/compute/test_flavor_manage.py b/nova/tests/unit/api/openstack/compute/test_flavor_manage.py index c4aae37bc649..f5f68f45ceab 100644 --- a/nova/tests/unit/api/openstack/compute/test_flavor_manage.py +++ b/nova/tests/unit/api/openstack/compute/test_flavor_manage.py @@ -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()) diff --git a/nova/tests/unit/api/openstack/compute/test_flavors.py b/nova/tests/unit/api/openstack/compute/test_flavors.py index b842dc6a9dcf..77b233f1a51b 100644 --- a/nova/tests/unit/api/openstack/compute/test_flavors.py +++ b/nova/tests/unit/api/openstack/compute/test_flavors.py @@ -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 diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py index 200d1335e9aa..5ccd3f1be68e 100644 --- a/nova/tests/unit/api/openstack/fakes.py +++ b/nova/tests/unit/api/openstack/fakes.py @@ -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' ), } diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index 0d2914fcc44b..f0f7af35b4cf 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -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", diff --git a/releasenotes/notes/flavor-description-02f8b8626da71a25.yaml b/releasenotes/notes/flavor-description-02f8b8626da71a25.yaml new file mode 100644 index 000000000000..cebffa2c8288 --- /dev/null +++ b/releasenotes/notes/flavor-description-02f8b8626da71a25.yaml @@ -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.