From 8c7ca368b190f0fd3c097531e2cf52fe4dc20c69 Mon Sep 17 00:00:00 2001 From: Brianna Poulos Date: Wed, 6 Jun 2018 16:43:56 -0400 Subject: [PATCH] Add trusted_image_certificates to REST API This change adds support for the trusted_image_certificates parameter, which is used to define a list of trusted certificate IDs that can be used during image signature verification and certificate validation. The parameter may contain a list of strings, each string representing the ID of a trusted certificate. The list is restricted to a maximum of 50 IDs. The list of certificate IDs will be stored in the trusted_certs field of the instance InstanceExtra and will be used to verify the validity of the signing certificate of a signed instance image. The trusted_image_certificates request parameter can be passed to the server create and rebuild APIs (if allowed by policy): * POST /servers * POST /servers/{server_id}/action (rebuild) The following policy rules were added to restrict the usage of the ``trusted_image_certificates`` request parameter in the server create and rebuild APIs: * os_compute_api:servers:create:trusted_certs * os_compute_api:servers:rebuild:trusted_certs The trusted_image_certificates parameter will be in the response body of the following APIs (not restricted by policy): * GET /servers/detail * GET /servers/{server_id} * PUT /servers/{server_id} * POST /servers/{server_id}/action (rebuild) APIImpact Implements blueprint: nova-validate-certificates Change-Id: Iedd3fea0e86648fae364f075915555dcb2c4f199 --- api-ref/source/parameters.yaml | 34 ++ api-ref/source/servers-actions.inc | 10 +- api-ref/source/servers.inc | 21 +- .../v2.63/server-action-rebuild-resp.json | 71 ++++ .../servers/v2.63/server-action-rebuild.json | 20 ++ .../servers/v2.63/server-create-req.json | 28 ++ .../servers/v2.63/server-create-resp.json | 22 ++ .../servers/v2.63/server-get-resp.json | 93 +++++ .../servers/v2.63/server-update-req.json | 8 + .../servers/v2.63/server-update-resp.json | 66 ++++ .../servers/v2.63/servers-details-resp.json | 95 +++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 4 +- .../compute/rest_api_version_history.rst | 23 ++ nova/api/openstack/compute/schemas/servers.py | 13 + nova/api/openstack/compute/servers.py | 53 ++- nova/api/openstack/compute/views/servers.py | 6 + nova/api/validation/parameter_types.py | 13 + nova/compute/api.py | 104 +++++- nova/exception.py | 6 + nova/policies/servers.py | 20 ++ .../v2.63/server-action-rebuild-resp.json.tpl | 69 ++++ .../v2.63/server-action-rebuild.json.tpl | 20 ++ .../servers/v2.63/server-create-req.json.tpl | 28 ++ .../servers/v2.63/server-create-resp.json.tpl | 22 ++ .../servers/v2.63/server-get-resp.json.tpl | 93 +++++ .../servers/v2.63/server-update-req.json.tpl | 8 + .../servers/v2.63/server-update-resp.json.tpl | 66 ++++ .../v2.63/servers-details-resp.json.tpl | 95 +++++ .../api_sample_tests/test_servers.py | 72 ++++ .../api/openstack/compute/test_serversV21.py | 325 ++++++++++++++++++ nova/tests/unit/api/openstack/fakes.py | 32 +- nova/tests/unit/compute/test_compute_api.py | 265 +++++++++++++- nova/tests/unit/test_policy.py | 2 + ...d-certs-microversion-589b75f0180d4d51.yaml | 30 ++ 36 files changed, 1803 insertions(+), 38 deletions(-) create mode 100644 doc/api_samples/servers/v2.63/server-action-rebuild-resp.json create mode 100644 doc/api_samples/servers/v2.63/server-action-rebuild.json create mode 100644 doc/api_samples/servers/v2.63/server-create-req.json create mode 100644 doc/api_samples/servers/v2.63/server-create-resp.json create mode 100644 doc/api_samples/servers/v2.63/server-get-resp.json create mode 100644 doc/api_samples/servers/v2.63/server-update-req.json create mode 100644 doc/api_samples/servers/v2.63/server-update-resp.json create mode 100644 doc/api_samples/servers/v2.63/servers-details-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/servers-details-resp.json.tpl create mode 100644 releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index b6e3ecdb7863..58f558d01e0e 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -5706,6 +5706,40 @@ server_tags_create: required: false type: array min_version: 2.52 +server_trusted_image_certificates_create_req: + description: | + A list of trusted certificate IDs, which are used during image + signature verification to verify the signing certificate. The list is + restricted to a maximum of 50 IDs. This parameter is optional in server + create requests if allowed by policy, and is not supported for + volume-backed instances. + in: body + required: false + type: array + min_version: 2.63 +server_trusted_image_certificates_rebuild_req: + description: | + A list of trusted certificate IDs, which are used during image + signature verification to verify the signing certificate. The list is + restricted to a maximum of 50 IDs. This parameter is optional in server + rebuild requests if allowed by policy, and is not supported + for volume-backed instances. + + If ``null`` is specified, the existing trusted certificate IDs are either + unset or reset to the configured defaults. + in: body + required: false + type: array + min_version: 2.63 +server_trusted_image_certificates_resp: + description: | + A list of trusted certificate IDs, that were used during image signature + verification to verify the signing certificate. The list is restricted + to a maximum of 50 IDs. + in: body + required: true + type: array + min_version: 2.63 server_usages: description: | A list of the server usage objects. diff --git a/api-ref/source/servers-actions.inc b/api-ref/source/servers-actions.inc index 037c273645c3..1ff5cd631d27 100644 --- a/api-ref/source/servers-actions.inc +++ b/api-ref/source/servers-actions.inc @@ -488,10 +488,11 @@ Request - description: server_description - key_name: key_name_rebuild_req - user_data: user_data_rebuild_req + - trusted_image_certificates: server_trusted_image_certificates_rebuild_req -**Example Rebuild Server (rebuild Action) (v2.54)** +**Example Rebuild Server (rebuild Action) (v2.63)** -.. literalinclude:: ../../doc/api_samples/servers/v2.54/server-action-rebuild.json +.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-action-rebuild.json :language: javascript Response @@ -537,10 +538,11 @@ Response - tags: tags - key_name: key_name_rebuild_resp - user_data: user_data_rebuild_resp + - trusted_image_certificates: server_trusted_image_certificates_resp -**Example Rebuild Server (rebuild Action) (v2.54)** +**Example Rebuild Server (rebuild Action) (v2.63)** -.. literalinclude:: ../../doc/api_samples/servers/v2.54/server-action-rebuild-resp.json +.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-action-rebuild-resp.json :language: javascript Remove (Disassociate) Floating Ip (removeFloatingIp Action) (DEPRECATED) diff --git a/api-ref/source/servers.inc b/api-ref/source/servers.inc index 9548fea81d40..493f575a69ea 100644 --- a/api-ref/source/servers.inc +++ b/api-ref/source/servers.inc @@ -381,6 +381,7 @@ Request - os:scheduler_hints.query: os:scheduler_hints_query - os:scheduler_hints.same_host: os:scheduler_hints_same_host - os:scheduler_hints.target_cell: os:scheduler_hints_target_cell + - trusted_image_certificates: server_trusted_image_certificates_create_req **Example Create Server** @@ -392,6 +393,11 @@ Request .. literalinclude:: ../../doc/api_samples/servers/v2.37/server-create-req.json :language: javascript +**Example Create Server With Trusted Image Certificates (v2.63)** + +.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-create-req.json + :language: javascript + Response -------- @@ -610,10 +616,11 @@ Response - host_status: host_status - description: server_description_resp - tags: tags + - trusted_image_certificates: server_trusted_image_certificates_resp -**Example List Servers Detailed (2.47)** +**Example List Servers Detailed (2.63)** -.. literalinclude:: /../../doc/api_samples/servers/v2.47/servers-details-resp.json +.. literalinclude:: /../../doc/api_samples/servers/v2.63/servers-details-resp.json :language: javascript @@ -716,10 +723,11 @@ Response - host_status: host_status - description: server_description_resp - tags: tags + - trusted_image_certificates: server_trusted_image_certificates_resp -**Example Show Server Details (2.47)** +**Example Show Server Details (2.63)** -.. literalinclude:: ../../doc/api_samples/servers/v2.47/server-get-resp.json +.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-get-resp.json :language: javascript Update Server @@ -808,10 +816,11 @@ Response - locked: locked - description: server_description_resp - tags: tags + - trusted_image_certificates: server_trusted_image_certificates_resp -**Example Update server name (2.47)** +**Example Update server name (2.63)** -.. literalinclude:: ../../doc/api_samples/servers/v2.47/server-update-resp.json +.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-update-resp.json :language: javascript Delete Server diff --git a/doc/api_samples/servers/v2.63/server-action-rebuild-resp.json b/doc/api_samples/servers/v2.63/server-action-rebuild-resp.json new file mode 100644 index 000000000000..251634920c36 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-action-rebuild-resp.json @@ -0,0 +1,71 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "adminPass": "seekr3t", + "created": "2017-10-10T16:06:02Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:cpu_policy": "dedicated", + "hw:mem_page_size": "2048" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "28d8d56f0e3a77e20891f455721cbb68032e017045e20aa5dfc6cb66", + "id": "a0a80a94-3d81-4a10-822a-daa0cf9e870b", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/a4baaf2a-3768-4e45-8847-13becef6bc5e", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/a4baaf2a-3768-4e45-8847-13becef6bc5e", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "meta_var": "meta_val" + }, + "name": "foobar", + "key_name": "new-key", + "description" : "description of foobar", + "progress": 0, + "status": "ACTIVE", + "tags": [], + "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "2017-10-10T16:06:03Z", + "user_id": "fake" + } +} + diff --git a/doc/api_samples/servers/v2.63/server-action-rebuild.json b/doc/api_samples/servers/v2.63/server-action-rebuild.json new file mode 100644 index 000000000000..54c3fca5c956 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-action-rebuild.json @@ -0,0 +1,20 @@ +{ + "rebuild" : { + "accessIPv4" : "1.2.3.4", + "accessIPv6" : "80fe::", + "OS-DCF:diskConfig": "AUTO", + "imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b", + "name" : "foobar", + "key_name": "new-key", + "description" : "description of foobar", + "adminPass" : "seekr3t", + "metadata" : { + "meta_var" : "meta_val" + }, + "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ] + } +} diff --git a/doc/api_samples/servers/v2.63/server-create-req.json b/doc/api_samples/servers/v2.63/server-create-req.json new file mode 100644 index 000000000000..5523ce8d349e --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-create-req.json @@ -0,0 +1,28 @@ +{ + "server" : { + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "name" : "new-server-test", + "imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef" : "6", + "availability_zone": "nova", + "OS-DCF:diskConfig": "AUTO", + "metadata" : { + "My Server Name" : "Apache1" + }, + "security_groups": [ + { + "name": "default" + } + ], + "user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "networks": "auto", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ] + }, + "OS-SCH-HNT:scheduler_hints": { + "same_host": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.63/server-create-resp.json b/doc/api_samples/servers/v2.63/server-create-resp.json new file mode 100644 index 000000000000..7400eb332720 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-create-resp.json @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "wKLKinb9u7GM", + "id": "aab35fd0-b459-4b59-9308-5a23147f3165", + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/aab35fd0-b459-4b59-9308-5a23147f3165", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/aab35fd0-b459-4b59-9308-5a23147f3165", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.63/server-get-resp.json b/doc/api_samples/servers/v2.63/server-get-resp.json new file mode 100644 index 000000000000..85eabf8727e3 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-get-resp.json @@ -0,0 +1,93 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "compute", + "OS-EXT-SRV-ATTR:hostname": "new-server-test", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "fake-mini", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000001", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "r-ov3q80zj", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-14T19:23:59.895661", + "OS-SRV-USG:terminated_at": null, + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "config_drive": "", + "created": "2017-02-14T19:23:58Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:cpu_policy": "dedicated", + "hw:mem_page_size": "2048" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6", + "host_status": "UP", + "id": "9168b536-cd40-4630-b43f-b259807c6e87", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/9168b536-cd40-4630-b43f-b259807c6e87", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/9168b536-cd40-4630-b43f-b259807c6e87", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "os-extended-volumes:volumes_attached": [], + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tags": [], + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "2017-02-14T19:24:00Z", + "user_id": "fake" + } +} diff --git a/doc/api_samples/servers/v2.63/server-update-req.json b/doc/api_samples/servers/v2.63/server-update-req.json new file mode 100644 index 000000000000..56e1a8c56a65 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-update-req.json @@ -0,0 +1,8 @@ +{ + "server": { + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "OS-DCF:diskConfig": "AUTO", + "name" : "new-server-test" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.63/server-update-resp.json b/doc/api_samples/servers/v2.63/server-update-resp.json new file mode 100644 index 000000000000..c4c769e16486 --- /dev/null +++ b/doc/api_samples/servers/v2.63/server-update-resp.json @@ -0,0 +1,66 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "created": "2012-12-02T02:11:57Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:cpu_policy": "dedicated", + "hw:mem_page_size": "2048" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "6e84af987b4e7ec1c039b16d21f508f4a505672bd94fb0218b668d07", + "id": "324dfb7d-f4a9-419a-9a19-237df04b443b", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/324dfb7d-f4a9-419a-9a19-237df04b443b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/324dfb7d-f4a9-419a-9a19-237df04b443b", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tags": [], + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "2012-12-02T02:11:58Z", + "user_id": "fake" + } +} diff --git a/doc/api_samples/servers/v2.63/servers-details-resp.json b/doc/api_samples/servers/v2.63/servers-details-resp.json new file mode 100644 index 000000000000..3cad50bbfe68 --- /dev/null +++ b/doc/api_samples/servers/v2.63/servers-details-resp.json @@ -0,0 +1,95 @@ +{ + "servers": [ + { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "compute", + "OS-EXT-SRV-ATTR:hostname": "new-server-test", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "fake-mini", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000001", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "r-y0w4v32k", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-10-10T15:49:09.516729", + "OS-SRV-USG:terminated_at": null, + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "config_drive": "", + "created": "2017-10-10T15:49:08Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:cpu_policy": "dedicated", + "hw:mem_page_size": "2048" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6", + "host_status": "UP", + "id": "569f39f9-7c76-42a1-9c2d-8394e2638a6d", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/569f39f9-7c76-42a1-9c2d-8394e2638a6d", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/569f39f9-7c76-42a1-9c2d-8394e2638a6d", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "os-extended-volumes:volumes_attached": [], + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tags": [], + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "2017-10-10T15:49:09Z", + "user_id": "fake" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index a25c2b0a4e25..308f43aaf31b 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.62", + "version": "2.63", "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 d4a531ca3503..ec1ecbe1c59d 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.62", + "version": "2.63", "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 12929d4ccc4c..8ffa6ec2fe5f 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -148,6 +148,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: /flavors APIs. * 2.62 - Add ``host`` and ``hostId`` fields to instance action detail API responses. + * 2.63 - Add support for applying trusted certificates when creating or + rebuilding a server. """ # The minimum and maximum versions of the API supported @@ -156,7 +158,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.62" +_MAX_API_VERSION = "2.63" 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/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 9a4038995c60..629fcb891125 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -795,3 +795,26 @@ the newly added ``host`` field will be controlled via policy rule ``os_compute_api:os-instance-actions:events``, which is the same policy used for the ``events.traceback`` field. If the user is prevented by policy, only ``hostId`` will be displayed. + +2.63 +---- + +Adds support for the ``trusted_image_certificates`` parameter, which is used to +define a list of trusted certificate IDs that can be used during image +signature verification and certificate validation. The list is restricted to +a maximum of 50 IDs. Note that ``trusted_image_certificates`` is not supported +with volume-backed servers. + +The ``trusted_image_certificates`` request parameter can be passed to +the server create and rebuild APIs: + +* ``POST /servers`` +* ``POST /servers/{server_id}/action (rebuild)`` + +The ``trusted_image_certificates`` parameter will be in the response body of +the following APIs: + +* ``GET /servers/detail`` +* ``GET /servers/{server_id}`` +* ``PUT /servers/{server_id}`` +* ``POST /servers/{server_id}/action (rebuild)`` diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 06b6a098d721..e77a56c8614c 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -148,6 +148,13 @@ base_create_v257 = copy.deepcopy(base_create_v252) base_create_v257['properties']['server']['properties'].pop('personality') +# 2.63 builds on 2.57 and makes the following changes: +# Allowing adding trusted certificates to instances when booting +base_create_v263 = copy.deepcopy(base_create_v257) +base_create_v263['properties']['server']['properties'][ + 'trusted_image_certificates'] = parameter_types.trusted_certs + + base_update = { 'type': 'object', 'properties': { @@ -224,6 +231,12 @@ base_rebuild_v257['properties']['rebuild']['properties']['user_data'] = ({ ] }) +# 2.63 builds on 2.57 and makes the following changes: +# Allowing adding trusted certificates to instances when rebuilding +base_rebuild_v263 = copy.deepcopy(base_rebuild_v257) +base_rebuild_v263['properties']['rebuild']['properties'][ + 'trusted_image_certificates'] = parameter_types.trusted_certs + resize = { 'type': 'object', 'properties': { diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 9f13c2e58d55..5500af4c4491 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -86,6 +86,9 @@ class ServersController(wsgi.Controller): schema_server_create_v252 = schema_servers.base_create_v252 schema_server_create_v257 = schema_servers.base_create_v257 + schema_server_create_v263 = schema_servers.base_create_v263 + schema_server_rebuild_v263 = schema_servers.base_rebuild_v263 + # NOTE(alex_xu): Please do not add more items into this list. This list # should be removed in the future. schema_func_list = [ @@ -132,6 +135,7 @@ class ServersController(wsgi.Controller): # TODO(alex_xu): The final goal is that merging all of # extended json-schema into server main json-schema. + self._create_schema(self.schema_server_create_v263, '2.63') self._create_schema(self.schema_server_create_v257, '2.57') self._create_schema(self.schema_server_create_v252, '2.52') self._create_schema(self.schema_server_create_v242, '2.42') @@ -304,6 +308,8 @@ class ServersController(wsgi.Controller): expected_attrs.append('services') if api_version_request.is_supported(req, '2.26'): expected_attrs.append("tags") + if api_version_request.is_supported(req, '2.63'): + expected_attrs.append("trusted_certs") # merge our expected attrs with what the view builder needs for # showing details @@ -345,6 +351,8 @@ class ServersController(wsgi.Controller): if is_detail: if api_version_request.is_supported(req, '2.26'): expected_attrs.append("tags") + if api_version_request.is_supported(req, '2.63'): + expected_attrs.append("trusted_certs") expected_attrs = self._view_builder.get_show_expected_attrs( expected_attrs) instance = common.get_instance(self.compute_api, context, @@ -456,7 +464,8 @@ class ServersController(wsgi.Controller): @validation.schema(schema_server_create_v237, '2.37', '2.41') @validation.schema(schema_server_create_v242, '2.42', '2.51') @validation.schema(schema_server_create_v252, '2.52', '2.56') - @validation.schema(schema_server_create_v257, '2.57') + @validation.schema(schema_server_create_v257, '2.57', '2.62') + @validation.schema(schema_server_create_v263, '2.63') def create(self, req, body): """Creates a new server for a given user.""" context = req.environ['nova.context'] @@ -489,6 +498,14 @@ class ServersController(wsgi.Controller): 'availability_zone': availability_zone} context.can(server_policies.SERVERS % 'create', target) + # Skip policy check for 'create:trusted_certs' if no trusted + # certificate IDs were provided. + trusted_certs = server_dict.get('trusted_image_certificates', None) + if trusted_certs: + create_kwargs['trusted_certs'] = trusted_certs + context.can(server_policies.SERVERS % 'create:trusted_certs', + target=target) + # TODO(Shao He, Feng) move this policy check to os-availability-zone # extension after refactor it. parse_az = self.compute_api.parse_availability_zone @@ -634,13 +651,15 @@ class ServersController(wsgi.Controller): exception.RealtimeMaskNotFoundOrInvalid, exception.SnapshotNotFound, exception.UnableToAutoAllocateNetwork, - exception.MultiattachNotSupportedOldMicroversion) as error: + exception.MultiattachNotSupportedOldMicroversion, + exception.CertificateValidationFailed) as error: raise exc.HTTPBadRequest(explanation=error.format_message()) except (exception.PortInUse, exception.InstanceExists, exception.NetworkAmbiguous, exception.NoUniqueMatch, - exception.MultiattachSupportNotYetAvailable) as error: + exception.MultiattachSupportNotYetAvailable, + exception.CertificateValidationNotYetAvailable) as error: raise exc.HTTPConflict(explanation=error.format_message()) # If the caller wanted a reservation_id, return it @@ -895,7 +914,8 @@ class ServersController(wsgi.Controller): @validation.schema(schema_server_rebuild, '2.1', '2.18') @validation.schema(schema_server_rebuild_v219, '2.19', '2.53') @validation.schema(schema_server_rebuild_v254, '2.54', '2.56') - @validation.schema(schema_server_rebuild_v257, '2.57') + @validation.schema(schema_server_rebuild_v257, '2.57', '2.62') + @validation.schema(schema_server_rebuild_v263, '2.63') def _action_rebuild(self, req, id, body): """Rebuild an instance with the given attributes.""" rebuild_dict = body['rebuild'] @@ -906,9 +926,9 @@ class ServersController(wsgi.Controller): context = req.environ['nova.context'] instance = self._get_server(context, req, id) - context.can(server_policies.SERVERS % 'rebuild', - target={'user_id': instance.user_id, - 'project_id': instance.project_id}) + target = {'user_id': instance.user_id, + 'project_id': instance.project_id} + context.can(server_policies.SERVERS % 'rebuild', target=target) attr_map = { 'name': 'display_name', 'description': 'display_description', @@ -930,6 +950,19 @@ class ServersController(wsgi.Controller): if include_user_data and 'user_data' in rebuild_dict: kwargs['user_data'] = rebuild_dict['user_data'] + # Skip policy check for 'rebuild:trusted_certs' if no trusted + # certificate IDs were provided. + if ((api_version_request.is_supported(req, min_version='2.63')) + # Note that this is different from server create since with + # rebuild a user can unset/reset the trusted certs by + # specifying trusted_image_certificates=None, similar to + # key_name. + and ('trusted_image_certificates' in rebuild_dict)): + kwargs['trusted_certs'] = rebuild_dict.get( + 'trusted_image_certificates') + context.can(server_policies.SERVERS % 'rebuild:trusted_certs', + target=target) + for request_attribute, instance_attribute in attr_map.items(): try: if request_attribute == 'name': @@ -947,7 +980,8 @@ class ServersController(wsgi.Controller): image_href, password, **kwargs) - except exception.InstanceIsLocked as e: + except (exception.InstanceIsLocked, + exception.CertificateValidationNotYetAvailable) as e: raise exc.HTTPConflict(explanation=e.format_message()) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, @@ -970,7 +1004,8 @@ class ServersController(wsgi.Controller): exception.FlavorDiskTooSmall, exception.FlavorMemoryTooSmall, exception.InvalidMetadata, - exception.AutoDiskConfigDisabledByImage) as error: + exception.AutoDiskConfigDisabledByImage, + exception.CertificateValidationFailed) as error: raise exc.HTTPBadRequest(explanation=error.format_message()) instance = self._get_server(context, req, id, is_detail=True) diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 2c60fdeda6d1..e7d339e12d31 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -171,6 +171,12 @@ class ViewBuilder(common.ViewBuilder): if api_version_request.is_supported(request, min_version="2.26"): server["server"]["tags"] = [t.tag for t in instance.tags] + if api_version_request.is_supported(request, min_version="2.63"): + trusted_certs = None + if instance.trusted_certs: + trusted_certs = instance.trusted_certs.ids + server["server"]["trusted_image_certificates"] = trusted_certs + return server def index(self, request, instances): diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index f36b18a667ee..edd01070ac31 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -476,3 +476,16 @@ pagination_parameters = { 'limit': multi_params(non_negative_integer), 'marker': multi_params({'type': 'string'}) } + +# The trusted_certs list is restricted to a maximum of 50 IDs. +# "null" is allowed to unset/reset trusted certs during rebuild. +trusted_certs = { + "type": ["array", "null"], + "minItems": 1, + "maxItems": 50, + "uniqueItems": True, + "items": { + "type": "string", + "minLength": 1, + } +} diff --git a/nova/compute/api.py b/nova/compute/api.py index dce517688c8d..0e28a9a342eb 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -104,6 +104,7 @@ AGGREGATE_ACTION_ADD = 'Add' BFV_RESERVE_MIN_COMPUTE_VERSION = 17 CINDER_V3_ATTACH_MIN_COMPUTE_VERSION = 24 MIN_COMPUTE_MULTIATTACH = 27 +MIN_COMPUTE_TRUSTED_CERTS = 31 # FIXME(danms): Keep a global cache of the cells we find the # first time we look. This needs to be refreshed on a timer or @@ -853,7 +854,7 @@ class API(base.Base): max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - key_pair, tags, supports_multiattach=False): + key_pair, tags, trusted_certs, supports_multiattach=False): # Check quotas num_instances = compute_utils.check_num_instances_quota( context, instance_type, min_count, max_count) @@ -887,6 +888,10 @@ class API(base.Base): instance.keypairs = objects.KeyPairList(objects=[]) if key_pair: instance.keypairs.objects.append(key_pair) + + instance.trusted_certs = self._retrieve_trusted_certs_object( + context, trusted_certs) + instance = self.create_db_entry_for_new_instance(context, instance_type, boot_meta, instance, security_groups, block_device_mapping, num_instances, i, @@ -961,6 +966,65 @@ class API(base.Base): return instances_to_build + @staticmethod + def _retrieve_trusted_certs_object(context, trusted_certs, rebuild=False): + """Convert user-requested trusted cert IDs to TrustedCerts object + + Also validates that the deployment is new enough to support trusted + image certification validation. + + :param context: The user request auth context + :param trusted_certs: list of user-specified trusted cert string IDs, + may be None + :param rebuild: True if rebuilding the server, False if creating a + new server + :returns: nova.objects.TrustedCerts object or None if no user-specified + trusted cert IDs were given and nova is not configured with + default trusted cert IDs + :raises: nova.exception.CertificateValidationNotYetAvailable: If + rebuilding a server with trusted certs on a compute host that is + too old to supported trusted image cert validation, or if creating + a server with trusted certs and there are no compute hosts in the + deployment that are new enough to support trusted image cert + validation + """ + # Retrieve trusted_certs parameter, or use CONF value if certificate + # validation is enabled + if trusted_certs: + certs_to_return = objects.TrustedCerts(ids=trusted_certs) + elif (CONF.glance.verify_glance_signatures and + CONF.glance.enable_certificate_validation and + CONF.glance.default_trusted_certificate_ids): + certs_to_return = objects.TrustedCerts( + ids=CONF.glance.default_trusted_certificate_ids) + else: + return None + + # Confirm trusted_certs are supported by the minimum nova + # compute service version + # TODO(mriedem): This minimum version compat code can be dropped in the + # 19.0.0 Stein release when all computes must be at a minimum running + # Rocky code. + if rebuild: + # we only care about the current cell since this is + # a rebuild + min_compute_version = objects.Service.get_minimum_version( + context, 'nova-compute') + else: + # we don't know which cell it's going to get scheduled + # to, so check all cells + # NOTE(mriedem): For multi-create server requests, we're hitting + # this for each instance since it's not cached; we could likely + # optimize this. + min_compute_version = \ + objects.service.get_minimum_version_all_cells( + context, ['nova-compute']) + + if min_compute_version < MIN_COMPUTE_TRUSTED_CERTS: + raise exception.CertificateValidationNotYetAvailable() + + return certs_to_return + def _get_bdm_image_metadata(self, context, block_device_mapping, legacy_bdm=True): """If we are booting from a volume, we need to get the @@ -1031,7 +1095,7 @@ class API(base.Base): block_device_mapping, auto_disk_config, filter_properties, reservation_id=None, legacy_bdm=True, shutdown_terminate=False, check_server_group_quota=False, tags=None, - supports_multiattach=False): + supports_multiattach=False, trusted_certs=None): """Verify all the input parameters regardless of the provisioning strategy being performed and schedule the instance(s) for creation. @@ -1049,6 +1113,14 @@ class API(base.Base): if image_href: image_id, boot_meta = self._get_image(context, image_href) else: + # This is similar to the logic in _retrieve_trusted_certs_object. + if (trusted_certs or + (CONF.glance.verify_glance_signatures and + CONF.glance.enable_certificate_validation and + CONF.glance.default_trusted_certificate_ids)): + msg = _("Image certificate validation is not supported " + "when booting from volume") + raise exception.CertificateValidationFailed(message=msg) image_id = None boot_meta = self._get_bdm_image_metadata( context, block_device_mapping, legacy_bdm) @@ -1096,7 +1168,8 @@ class API(base.Base): context, instance_type, min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, - filter_properties, key_pair, tags, supports_multiattach) + filter_properties, key_pair, tags, trusted_certs, + supports_multiattach) instances = [] request_specs = [] @@ -1577,7 +1650,7 @@ class API(base.Base): config_drive=None, auto_disk_config=None, scheduler_hints=None, legacy_bdm=True, shutdown_terminate=False, check_server_group_quota=False, tags=None, - supports_multiattach=False): + supports_multiattach=False, trusted_certs=None): """Provision instances, sending instance information to the scheduler. The scheduler will determine where the instance(s) go and will handle creating the DB entries. @@ -1617,7 +1690,8 @@ class API(base.Base): legacy_bdm=legacy_bdm, shutdown_terminate=shutdown_terminate, check_server_group_quota=check_server_group_quota, - tags=tags, supports_multiattach=supports_multiattach) + tags=tags, supports_multiattach=supports_multiattach, + trusted_certs=trusted_certs) def _check_auto_disk_config(self, instance=None, image=None, **extra_instance_updates): @@ -2998,6 +3072,18 @@ class API(base.Base): instance.key_data = None instance.keypairs = objects.KeyPairList(objects=[]) + # Use trusted_certs value from kwargs to create TrustedCerts object + trusted_certs = None + if 'trusted_certs' in kwargs: + # Note that the user can set, change, or unset / reset trusted + # certs. If they are explicitly specifying + # trusted_image_certificates=None, that means we'll either unset + # them on the instance *or* reset to use the defaults (if defaults + # are configured). + trusted_certs = kwargs.pop('trusted_certs') + instance.trusted_certs = self._retrieve_trusted_certs_object( + context, trusted_certs, rebuild=True) + image_id, image = self._get_image(context, image_href) self._check_auto_disk_config(image=image, **kwargs) @@ -3012,6 +3098,14 @@ class API(base.Base): is_volume_backed = compute_utils.is_volume_backed_instance( context, instance, bdms) if is_volume_backed: + if trusted_certs: + # The only way we can get here is if the user tried to set + # trusted certs or specified trusted_image_certificates=None + # and default_trusted_certificate_ids is configured. + msg = _("Image certificate validation is not supported " + "for volume-backed servers.") + raise exception.CertificateValidationFailed(message=msg) + # For boot from volume, instance.image_ref is empty, so we need to # query the image from the volume. if root_bdm is None: diff --git a/nova/exception.py b/nova/exception.py index 3adba4d2d692..d5311f10f11d 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2297,3 +2297,9 @@ class AllocationCreateFailed(NovaException): class CertificateValidationFailed(NovaException): msg_fmt = _("Image signature certificate validation failed for " "certificate: %(cert_uuid)s. %(reason)s") + + +class CertificateValidationNotYetAvailable(NovaException): + msg_fmt = _("Image signature certificate validation support is " + "not yet available.") + code = 409 diff --git a/nova/policies/servers.py b/nova/policies/servers.py index 070de165f5d7..512ebf3c6cc5 100644 --- a/nova/policies/servers.py +++ b/nova/policies/servers.py @@ -127,6 +127,16 @@ rules = [ 'path': '/servers' } ]), + policy.DocumentedRuleDefault( + SERVERS % 'create:trusted_certs', + RULE_AOO, + "Create a server with trusted image certificate IDs", + [ + { + 'method': 'POST', + 'path': '/servers' + } + ]), policy.DocumentedRuleDefault( NETWORK_ATTACH_EXTERNAL, 'is_admin:True', @@ -213,6 +223,16 @@ rules = [ 'path': '/servers/{server_id}/action (rebuild)' } ]), + policy.DocumentedRuleDefault( + SERVERS % 'rebuild:trusted_certs', + RULE_AOO, + "Rebuild a server with trusted image certificate IDs", + [ + { + 'method': 'POST', + 'path': '/servers/{server_id}/action (rebuild)' + } + ]), policy.DocumentedRuleDefault( SERVERS % 'create_image', RULE_AOO, diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild-resp.json.tpl new file mode 100644 index 000000000000..1df36b257d5d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild-resp.json.tpl @@ -0,0 +1,69 @@ +{ + "server": { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "version": 4 + } + ] + }, + "adminPass": "%(password)s", + "created": "%(isotime)s", + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:mem_page_size": "2048", + "hw:cpu_policy": "dedicated" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "%(hostid)s", + "id": "%(uuid)s", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(compute_endpoint)s/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "meta_var": "meta_val" + }, + "name": "%(name)s", + "key_name": "%(key_name)s", + "description": "%(description)s", + "progress": 0, + "OS-DCF:diskConfig": "AUTO", + "status": "ACTIVE", + "tags": [], + "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "%(isotime)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild.json.tpl new file mode 100644 index 000000000000..bce68ba837f6 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-action-rebuild.json.tpl @@ -0,0 +1,20 @@ +{ + "rebuild" : { + "accessIPv4" : "%(access_ip_v4)s", + "accessIPv6" : "%(access_ip_v6)s", + "OS-DCF:diskConfig": "AUTO", + "imageRef" : "%(uuid)s", + "name" : "%(name)s", + "key_name" : "%(key_name)s", + "description" : "%(description)s", + "adminPass" : "%(pass)s", + "metadata" : { + "meta_var" : "meta_val" + }, + "user_data": "ZWNobyAiaGVsbG8gd29ybGQi", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-req.json.tpl new file mode 100644 index 000000000000..7e2addb0fb5a --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-req.json.tpl @@ -0,0 +1,28 @@ +{ + "server" : { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "name" : "%(name)s", + "imageRef" : "%(image_id)s", + "flavorRef" : "6", + "availability_zone": "nova", + "OS-DCF:diskConfig": "AUTO", + "metadata" : { + "My Server Name" : "Apache1" + }, + "security_groups": [ + { + "name": "default" + } + ], + "user_data" : "%(user_data)s", + "networks": "auto", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ] + }, + "OS-SCH-HNT:scheduler_hints": { + "same_host": "%(uuid)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-resp.json.tpl new file mode 100644 index 000000000000..4b30e0cfbdb8 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-create-resp.json.tpl @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-get-resp.json.tpl new file mode 100644 index 000000000000..ca5d5bb1eff2 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-get-resp.json.tpl @@ -0,0 +1,93 @@ +{ + "server": { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "version": 4 + } + ] + }, + "created": "%(isotime)s", + "description": null, + "host_status": "UP", + "locked": false, + "tags": [], + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:cpu_policy": "dedicated", + "hw:mem_page_size": "2048" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "%(hostid)s", + "id": "%(id)s", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(compute_endpoint)s/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "config_drive": "%(cdrive)s", + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "%(compute_host)s", + "OS-EXT-SRV-ATTR:hostname": "%(hostname)s", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "%(hypervisor_hostname)s", + "OS-EXT-SRV-ATTR:instance_name": "%(instance_name)s", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "%(reservation_id)s", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "%(user_data)s", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "os-extended-volumes:volumes_attached": [], + "OS-SRV-USG:launched_at": "%(strtime)s", + "OS-SRV-USG:terminated_at": null, + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "%(isotime)s", + "user_id": "fake", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-req.json.tpl new file mode 100644 index 000000000000..e34896621d2e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-req.json.tpl @@ -0,0 +1,8 @@ +{ + "server": { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "OS-DCF:diskConfig": "AUTO", + "name" : "new-server-test" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-resp.json.tpl new file mode 100644 index 000000000000..fed78fd555d7 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/server-update-resp.json.tpl @@ -0,0 +1,66 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "version": 4 + } + ] + }, + "created": "%(isotime)s", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:mem_page_size": "2048", + "hw:cpu_policy": "dedicated" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "%(hostid)s", + "id": "%(id)s", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(compute_endpoint)s/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(id)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(id)s", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tags": [], + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "%(isotime)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/servers-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/servers-details-resp.json.tpl new file mode 100644 index 000000000000..9a8872d36a38 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.63/servers-details-resp.json.tpl @@ -0,0 +1,95 @@ +{ + "servers": [ + { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "addresses": { + "private": [ + { + "addr": "%(ip)s", + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "version": 4 + } + ] + }, + "created": "%(isotime)s", + "description": null, + "host_status": "UP", + "locked": false, + "tags": [], + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": { + "hw:cpu_model": "SandyBridge", + "hw:mem_page_size": "2048", + "hw:cpu_policy": "dedicated" + }, + "original_name": "m1.tiny.specs", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "%(hostid)s", + "id": "%(id)s", + "image": { + "id": "%(uuid)s", + "links": [ + { + "href": "%(compute_endpoint)s/images/%(uuid)s", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(id)s", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "config_drive": "%(cdrive)s", + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "%(compute_host)s", + "OS-EXT-SRV-ATTR:hostname": "%(hostname)s", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "%(hypervisor_hostname)s", + "OS-EXT-SRV-ATTR:instance_name": "%(instance_name)s", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "%(reservation_id)s", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "%(user_data)s", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "os-extended-volumes:volumes_attached": [], + "OS-SRV-USG:launched_at": "%(strtime)s", + "OS-SRV-USG:terminated_at": null, + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "trusted_image_certificates": [ + "0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8", + "674736e3-f25c-405c-8362-bbf991e0ce0a" + ], + "updated": "%(isotime)s", + "user_id": "fake" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index 2d6882642ae4..1d1bb8ca814a 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -19,10 +19,13 @@ import time import six from nova.api.openstack import api_version_request as avr +import nova.conf from nova.tests.functional.api_sample_tests import api_sample_base from nova.tests.unit.api.openstack import fakes from nova.tests.unit.image import fake +CONF = nova.conf.CONF + class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21): microversion = None @@ -237,6 +240,75 @@ class ServersSampleJson252Test(ServersSampleJsonTest): use_common_server_post = False +class ServersSampleJson263Test(ServersSampleBase): + microversion = '2.63' + scenarios = [('v2_63', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(ServersSampleJson263Test, self).setUp() + self.common_subs = { + 'hostid': '[a-f0-9]+', + 'instance_name': 'instance-\d{8}', + 'hypervisor_hostname': r'[\w\.\-]+', + 'hostname': r'[\w\.\-]+', + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': '80fe::', + 'user_data': (self.user_data if six.PY2 + else self.user_data.decode('utf-8')), + 'cdrive': '.*', + } + + def test_servers_post(self): + self._post_server(use_common_server_api_samples=False) + + def test_server_rebuild(self): + uuid = self._post_server(use_common_server_api_samples=False) + fakes.stub_out_key_pair_funcs(self) + image = fake.get_valid_image_id() + + params = { + 'uuid': image, + 'name': 'foobar', + 'key_name': 'new-key', + 'description': 'description of foobar', + 'pass': 'seekr3t', + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': '80fe::', + } + + resp = self._do_post('servers/%s/action' % uuid, + 'server-action-rebuild', params) + + exp_resp = params.copy() + del exp_resp['uuid'] + exp_resp['hostid'] = '[a-f0-9]+' + + self._verify_response('server-action-rebuild-resp', + exp_resp, resp, 202) + + def test_servers_details(self): + uuid = self._post_server(use_common_server_api_samples=False) + response = self._do_get('servers/detail') + subs = self.common_subs.copy() + subs['id'] = uuid + self._verify_response('servers-details-resp', subs, response, 200) + + def test_server_get(self): + uuid = self._post_server(use_common_server_api_samples=False) + response = self._do_get('servers/%s' % uuid) + subs = self.common_subs.copy() + subs['id'] = uuid + self._verify_response('server-get-resp', subs, response, 200) + + def test_server_update(self): + uuid = self._post_server(use_common_server_api_samples=False) + subs = self.common_subs.copy() + subs['id'] = uuid + response = self._do_put('servers/%s' % uuid, + 'server-update-req', subs) + self._verify_response('server-update-resp', subs, response, 200) + + class ServersUpdateSampleJsonTest(ServersSampleBase): def test_update_server(self): diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 77a019d316fa..a17e34caadd7 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -2406,6 +2406,200 @@ class ServersControllerRebuildTestV219(ServersControllerRebuildInstanceTest): self.req, FAKE_UUID, body=self.body) +# NOTE(jaypipes): Not based from ServersControllerRebuildInstanceTest because +# that test case's setUp is completely b0rked +class ServersControllerRebuildTestV263(ControllerTest): + + image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + + def setUp(self): + super(ServersControllerRebuildTestV263, self).setUp() + self.req = fakes.HTTPRequest.blank('/fake/servers/a/action') + self.req.method = 'POST' + self.req.headers["content-type"] = "application/json" + self.req_user_id = self.req.environ['nova.context'].user_id + self.req_project_id = self.req.environ['nova.context'].project_id + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.63') + self.body = { + 'rebuild': { + 'name': 'new_name', + 'imageRef': self.image_uuid, + 'metadata': { + 'open': 'stack', + }, + }, + } + + @mock.patch('nova.compute.api.API.get') + def _rebuild_server(self, mock_get, certs=None, + conf_enabled=True, conf_certs=None): + fakes.stub_out_trusted_certs(self, certs=certs) + ctx = self.req.environ['nova.context'] + mock_get.return_value = fakes.stub_instance_obj(ctx, + vm_state=vm_states.ACTIVE, trusted_certs=certs, + project_id=self.req_project_id, user_id=self.req_user_id) + + self.flags(default_trusted_certificate_ids=conf_certs, group='glance') + + if conf_enabled: + self.flags(verify_glance_signatures=True, group='glance') + self.flags(enable_certificate_validation=True, group='glance') + + self.body['rebuild']['trusted_image_certificates'] = certs + self.req.body = jsonutils.dump_as_bytes(self.body) + server = self.controller._action_rebuild( + self.req, FAKE_UUID, body=self.body).obj['server'] + + if certs: + self.assertEqual(certs, server['trusted_image_certificates']) + else: + if conf_enabled: + # configuration file default is used + self.assertEqual( + conf_certs, server['trusted_image_certificates']) + else: + # either not set or empty + self.assertIsNone(server['trusted_image_certificates']) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_rebuild_server_with_trusted_certs(self, get_min_ver): + """Test rebuild with valid trusted_image_certificates argument""" + self._rebuild_server( + certs=['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8', + '674736e3-f25c-405c-8362-bbf991e0ce0a']) + + def test_rebuild_server_without_trusted_certs(self): + """Test rebuild without trusted image certificates""" + self._rebuild_server() + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_rebuild_server_conf_options_turned_off_set(self, get_min_ver): + """Test rebuild with feature disabled and certs specified""" + self._rebuild_server( + certs=['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8'], conf_enabled=False) + + def test_rebuild_server_conf_options_turned_off_empty(self): + """Test rebuild with feature disabled""" + self._rebuild_server(conf_enabled=False) + + def test_rebuild_server_default_trusted_certificates_empty(self): + """Test rebuild with feature enabled and no certs specified""" + self._rebuild_server(conf_enabled=True) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_rebuild_server_default_trusted_certificates(self, get_min_ver): + """Test rebuild with certificate specified in configurations""" + self._rebuild_server(conf_enabled=True, conf_certs=['conf-id']) + + def test_rebuild_server_with_empty_trusted_cert_id(self): + """Make sure that we can't rebuild with an empty certificate ID""" + self.body['rebuild']['trusted_image_certificates'] = [''] + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('is too short', six.text_type(ex)) + + def test_rebuild_server_with_empty_trusted_certs(self): + """Make sure that we can't rebuild with an empty array of IDs""" + self.body['rebuild']['trusted_image_certificates'] = [] + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('is too short', six.text_type(ex)) + + def test_rebuild_server_with_too_many_trusted_certs(self): + """Make sure that we can't rebuild with an array of >50 unique IDs""" + self.body['rebuild']['trusted_image_certificates'] = [ + 'cert{}'.format(i) for i in range(51)] + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('is too long', six.text_type(ex)) + + def test_rebuild_server_with_nonunique_trusted_certs(self): + """Make sure that we can't rebuild with a non-unique array of IDs""" + self.body['rebuild']['trusted_image_certificates'] = ['cert', 'cert'] + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('has non-unique elements', six.text_type(ex)) + + def test_rebuild_server_with_invalid_trusted_cert_id(self): + """Make sure that we can't rebuild with non-string certificate IDs""" + self.body['rebuild']['trusted_image_certificates'] = [1, 2] + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('is not of type', six.text_type(ex)) + + def test_rebuild_server_with_invalid_trusted_certs(self): + """Make sure that we can't rebuild with certificates in a non-array""" + self.body['rebuild']['trusted_image_certificates'] = "not-an-array" + self.req.body = jsonutils.dump_as_bytes(self.body) + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('is not of type', six.text_type(ex)) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_rebuild_server_with_trusted_certs_pre_2_63_fails(self, + get_min_ver): + """Make sure we can't use trusted_certs before 2.63""" + self._rebuild_server(certs=['trusted-cert-id']) + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.62') + ex = self.assertRaises(exception.ValidationError, + self.controller._action_rebuild, + self.req, FAKE_UUID, body=self.body) + self.assertIn('Additional properties are not allowed', + six.text_type(ex)) + + def test_rebuild_server_with_trusted_certs_policy_failed(self): + rule_name = "os_compute_api:servers:rebuild:trusted_certs" + rules = {"os_compute_api:servers:rebuild": "@", + rule_name: "project:fake"} + self.policy.set_rules(rules) + exc = self.assertRaises(exception.PolicyNotAuthorized, + self._rebuild_server, + certs=['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8']) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) + + @mock.patch.object(compute_api.API, 'rebuild') + def test_rebuild_server_with_cert_validation_error( + self, mock_rebuild): + mock_rebuild.side_effect = exception.CertificateValidationFailed( + cert_uuid="cert id", reason="test cert validation error") + + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self._rebuild_server, + certs=['trusted-cert-id']) + self.assertIn('test cert validation error', + six.text_type(ex)) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS - 1) + def test_rebuild_server_with_cert_validation_not_available( + self, get_min_ver): + ex = self.assertRaises(webob.exc.HTTPConflict, + self._rebuild_server, + certs=['trusted-cert-id']) + self.assertIn('Image signature certificate validation support ' + 'is not yet available', + six.text_type(ex)) + + class ServersControllerUpdateTest(ControllerTest): def _get_request(self, body=None): @@ -4215,6 +4409,137 @@ class ServersControllerCreateTestV260(test.NoDBTestCase): six.text_type(ex)) +class ServersControllerCreateTestV263(ServersControllerCreateTest): + def _create_instance_req(self, certs=None): + self.body['server']['trusted_image_certificates'] = certs + + self.flags(verify_glance_signatures=True, group='glance') + self.flags(enable_certificate_validation=True, group='glance') + + self.req.body = jsonutils.dump_as_bytes(self.body) + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.63') + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_create_instance_with_trusted_certs(self, get_min_ver): + """Test create with valid trusted_image_certificates argument""" + self._create_instance_req( + ['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8', + '674736e3-f25c-405c-8362-bbf991e0ce0a']) + # The fact that the action doesn't raise is enough validation + self.controller.create(self.req, body=self.body).obj + + def test_create_instance_without_trusted_certs(self): + """Test create without trusted image certificates""" + self._create_instance_req() + # The fact that the action doesn't raise is enough validation + self.controller.create(self.req, body=self.body).obj + + def test_create_instance_with_empty_trusted_cert_id(self): + """Make sure we can't create with an empty certificate ID""" + self._create_instance_req(['']) + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('is too short', six.text_type(ex)) + + def test_create_instance_with_empty_trusted_certs(self): + """Make sure we can't create with an empty array of IDs""" + self.body['server']['trusted_image_certificates'] = [] + self.req.body = jsonutils.dump_as_bytes(self.body) + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.63') + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('is too short', six.text_type(ex)) + + def test_create_instance_with_too_many_trusted_certs(self): + """Make sure we can't create with an array of >50 unique IDs""" + self._create_instance_req(['cert{}'.format(i) for i in range(51)]) + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('is too long', six.text_type(ex)) + + def test_create_instance_with_nonunique_trusted_certs(self): + """Make sure we can't create with a non-unique array of IDs""" + self._create_instance_req(['cert', 'cert']) + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('has non-unique elements', six.text_type(ex)) + + def test_create_instance_with_invalid_trusted_cert_id(self): + """Make sure we can't create with non-string certificate IDs""" + self._create_instance_req([1, 2]) + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('is not of type', six.text_type(ex)) + + def test_create_instance_with_invalid_trusted_certs(self): + """Make sure we can't create with certificates in a non-array""" + self._create_instance_req("not-an-array") + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('is not of type', six.text_type(ex)) + + def test_create_server_with_trusted_certs_pre_2_63_fails(self): + """Make sure we can't use trusted_certs before 2.63""" + self._create_instance_req(['trusted-cert-id']) + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.62') + ex = self.assertRaises( + exception.ValidationError, self.controller.create, self.req, + body=self.body) + self.assertIn('Additional properties are not allowed', + six.text_type(ex)) + + def test_create_server_with_trusted_certs_policy_failed(self): + rule_name = "os_compute_api:servers:create:trusted_certs" + rules = {"os_compute_api:servers:create": "@", + "os_compute_api:servers:create:forced_host": "@", + "os_compute_api:servers:create:attach_volume": "@", + "os_compute_api:servers:create:attach_network": "@", + rule_name: "project:fake"} + self._create_instance_req(['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8']) + self.policy.set_rules(rules) + exc = self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, self.req, + body=self.body) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) + + @mock.patch.object(compute_api.API, 'create') + def test_create_server_with_cert_validation_error( + self, mock_create): + mock_create.side_effect = exception.CertificateValidationFailed( + cert_uuid="cert id", reason="test cert validation error") + + self._create_instance_req(['trusted-cert-id']) + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, self.req, + body=self.body) + self.assertIn('test cert validation error', + six.text_type(ex)) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS - 1) + def test_create_server_with_cert_validation_not_available( + self, mock_get_min_version_all_cells): + self._create_instance_req(['trusted-cert-id']) + ex = self.assertRaises(webob.exc.HTTPConflict, + self.controller.create, self.req, + body=self.body) + self.assertIn('Image signature certificate validation support ' + 'is not yet available', + six.text_type(ex)) + + class ServersControllerCreateTestWithMock(test.TestCase): image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' flavor_ref = 'http://localhost/123/flavors/3' diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py index 22d8066dce5d..300f901bf209 100644 --- a/nova/tests/unit/api/openstack/fakes.py +++ b/nova/tests/unit/api/openstack/fakes.py @@ -105,6 +105,30 @@ def stub_out_key_pair_funcs(testcase, have_key_pair=True, **kwargs): testcase.stub_out('nova.db.key_pair_get_all_by_user', no_key_pair) +def stub_out_trusted_certs(test, certs=None): + def fake_trusted_certs(cls, context, instance_uuid): + return objects.TrustedCerts(ids=trusted_certs) + + def fake_instance_extra(context, instance_uuid, columns): + if columns is ['trusted_certs']: + return {'trusted_certs': trusted_certs} + else: + return {'numa_topology': None, + 'pci_requests': None, + 'flavor': None, + 'vcpu_model': None, + 'trusted_certs': trusted_certs, + 'migration_context': None} + + trusted_certs = [] + if certs: + trusted_certs = certs + test.stub_out('nova.objects.TrustedCerts.get_by_instance_uuid', + fake_trusted_certs) + test.stub_out('nova.db.instance_extra_get_by_instance_uuid', + fake_instance_extra) + + def stub_out_instance_quota(test, allowed, quota, resource='instances'): def fake_reserve(context, **deltas): requested = deltas.pop(resource, 0) @@ -424,7 +448,7 @@ def stub_instance(id=1, user_id=None, project_id=None, host=None, memory_mb=0, vcpus=0, root_gb=0, ephemeral_gb=0, instance_type=None, launch_index=0, kernel_id="", ramdisk_id="", user_data=None, system_metadata=None, - services=None): + services=None, trusted_certs=None): if user_id is None: user_id = 'fake_user' if project_id is None: @@ -531,10 +555,12 @@ def stub_instance(id=1, user_id=None, project_id=None, host=None, "extra": {"numa_topology": None, "pci_requests": None, "flavor": flavorinfo, - }, + "trusted_certs": trusted_certs, + }, "cleaned": cleaned, "services": services, - "tags": []} + "tags": [], + } instance.update(info_cache) instance['info_cache']['instance_uuid'] = instance['uuid'] diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index 598d93ed51cd..aa9de6d0c8fd 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -262,6 +262,29 @@ class _ComputeAPIUnitTestMixIn(object): self.assertEqual(2, mock_get_image.call_count) self.assertEqual(2, mock_limit_check_pu.call_count) + @mock.patch('nova.objects.Quotas.limit_check') + def test_create_volume_backed_instance_with_trusted_certs(self, + check_limit): + # Creating an instance with no image_ref specified will result in + # creating a volume-backed instance + self.assertRaises(exception.CertificateValidationFailed, + self.compute_api.create, self.context, + instance_type=self._create_flavor(), image_href=None, + trusted_certs=['test-cert-1', 'test-cert-2']) + + @mock.patch('nova.objects.Quotas.limit_check') + def test_create_volume_backed_instance_with_conf_trusted_certs( + self, check_limit): + self.flags(verify_glance_signatures=True, group='glance') + self.flags(enable_certificate_validation=True, group='glance') + self.flags(default_trusted_certificate_ids=['certs'], group='glance') + # Creating an instance with no image_ref specified will result in + # creating a volume-backed instance + self.assertRaises(exception.CertificateValidationFailed, + self.compute_api.create, self.context, + instance_type=self._create_flavor(), + image_href=None) + def _test_create_max_net_count(self, max_net_count, min_count, max_count): with test.nested( mock.patch.object(self.compute_api, '_get_image', @@ -3790,6 +3813,171 @@ class _ComputeAPIUnitTestMixIn(object): self.assertNotEqual(orig_key_name, instance.key_name) self.assertNotEqual(orig_key_data, instance.key_data) + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + @mock.patch.object(objects.Instance, 'save') + @mock.patch.object(objects.Instance, 'get_flavor') + @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') + @mock.patch.object(compute_api.API, '_get_image') + @mock.patch.object(compute_api.API, '_check_auto_disk_config') + @mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild') + @mock.patch.object(compute_api.API, '_record_action_start') + def test_rebuild_change_trusted_certs(self, _record_action_start, + _checks_for_create_and_rebuild, _check_auto_disk_config, + _get_image, bdm_get_by_instance_uuid, get_flavor, instance_save, + req_spec_get_by_inst_uuid, get_min_version): + orig_system_metadata = {} + orig_trusted_certs = ['orig-trusted-cert-1', 'orig-trusted-cert-2'] + new_trusted_certs = ['new-trusted-cert-1', 'new-trusted-cert-2'] + instance = fake_instance.fake_instance_obj( + self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell', + launched_at=timeutils.utcnow(), + system_metadata=orig_system_metadata, image_ref='foo', + expected_attrs=['system_metadata'], + trusted_certs=orig_trusted_certs) + get_flavor.return_value = test_flavor.fake_flavor + flavor = instance.get_flavor() + image_href = 'foo' + image = { + "min_ram": 10, "min_disk": 1, + "properties": {'architecture': fields_obj.Architecture.X86_64, + 'vm_mode': 'hvm'}} + admin_pass = '' + files_to_inject = [] + bdms = objects.BlockDeviceMappingList() + + _get_image.return_value = (None, image) + bdm_get_by_instance_uuid.return_value = bdms + + fake_spec = objects.RequestSpec() + req_spec_get_by_inst_uuid.return_value = fake_spec + + with mock.patch.object(self.compute_api.compute_task_api, + 'rebuild_instance') as rebuild_instance: + self.compute_api.rebuild(self.context, instance, image_href, + admin_pass, files_to_inject, + trusted_certs=new_trusted_certs) + + rebuild_instance.assert_called_once_with( + self.context, instance=instance, new_pass=admin_pass, + injected_files=files_to_inject, image_ref=image_href, + orig_image_ref=image_href, + orig_sys_metadata=orig_system_metadata, bdms=bdms, + preserve_ephemeral=False, host=instance.host, + request_spec=fake_spec, kwargs={}) + + _check_auto_disk_config.assert_called_once_with(image=image) + _checks_for_create_and_rebuild.assert_called_once_with( + self.context, None, image, flavor, {}, [], None) + self.assertEqual(new_trusted_certs, instance.trusted_certs.ids) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + @mock.patch.object(objects.Instance, 'save') + @mock.patch.object(objects.Instance, 'get_flavor') + @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') + @mock.patch.object(compute_api.API, '_get_image') + @mock.patch.object(compute_api.API, '_check_auto_disk_config') + @mock.patch.object(compute_api.API, '_checks_for_create_and_rebuild') + @mock.patch.object(compute_api.API, '_record_action_start') + def test_rebuild_unset_trusted_certs(self, _record_action_start, + _checks_for_create_and_rebuild, + _check_auto_disk_config, + _get_image, bdm_get_by_instance_uuid, + get_flavor, instance_save, + req_spec_get_by_inst_uuid, + get_min_version): + """Tests the scenario that the server was created with some trusted + certs and then rebuilt without trusted_image_certificates=None + explicitly to unset the trusted certs on the server. + """ + orig_system_metadata = {} + orig_trusted_certs = ['orig-trusted-cert-1', 'orig-trusted-cert-2'] + new_trusted_certs = None + instance = fake_instance.fake_instance_obj( + self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell', + launched_at=timeutils.utcnow(), + system_metadata=orig_system_metadata, image_ref='foo', + expected_attrs=['system_metadata'], + trusted_certs=orig_trusted_certs) + get_flavor.return_value = test_flavor.fake_flavor + flavor = instance.get_flavor() + image_href = 'foo' + image = { + "min_ram": 10, "min_disk": 1, + "properties": {'architecture': fields_obj.Architecture.X86_64, + 'vm_mode': 'hvm'}} + admin_pass = '' + files_to_inject = [] + bdms = objects.BlockDeviceMappingList() + + _get_image.return_value = (None, image) + bdm_get_by_instance_uuid.return_value = bdms + + fake_spec = objects.RequestSpec() + req_spec_get_by_inst_uuid.return_value = fake_spec + + with mock.patch.object(self.compute_api.compute_task_api, + 'rebuild_instance') as rebuild_instance: + self.compute_api.rebuild(self.context, instance, image_href, + admin_pass, files_to_inject, + trusted_certs=new_trusted_certs) + + rebuild_instance.assert_called_once_with( + self.context, instance=instance, new_pass=admin_pass, + injected_files=files_to_inject, image_ref=image_href, + orig_image_ref=image_href, + orig_sys_metadata=orig_system_metadata, bdms=bdms, + preserve_ephemeral=False, host=instance.host, + request_spec=fake_spec, kwargs={}) + + _check_auto_disk_config.assert_called_once_with(image=image) + _checks_for_create_and_rebuild.assert_called_once_with( + self.context, None, image, flavor, {}, [], None) + self.assertIsNone(instance.trusted_certs) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + @mock.patch.object(compute_utils, 'is_volume_backed_instance', + return_value=True) + @mock.patch.object(objects.Instance, 'get_flavor') + @mock.patch.object(objects.BlockDeviceMappingList, 'get_by_instance_uuid') + @mock.patch.object(compute_api.API, '_get_image') + @mock.patch.object(compute_api.API, '_check_auto_disk_config') + @mock.patch.object(compute_api.API, '_record_action_start') + def test_rebuild_volume_backed_instance_with_trusted_certs( + self, _record_action_start, _check_auto_disk_config, _get_image, + bdm_get_by_instance_uuid, get_flavor, instance_is_volume_backed, + get_min_version): + orig_system_metadata = {} + new_trusted_certs = ['new-trusted-cert-1', 'new-trusted-cert-2'] + instance = fake_instance.fake_instance_obj( + self.context, vm_state=vm_states.ACTIVE, cell_name='fake-cell', + launched_at=timeutils.utcnow(), + system_metadata=orig_system_metadata, image_ref=None, + expected_attrs=['system_metadata'], trusted_certs=None) + get_flavor.return_value = test_flavor.fake_flavor + image_href = 'foo' + image = { + "min_ram": 10, "min_disk": 1, + "properties": {'architecture': fields_obj.Architecture.X86_64, + 'vm_mode': 'hvm'}} + admin_pass = '' + files_to_inject = [] + bdms = objects.BlockDeviceMappingList() + + _get_image.return_value = (None, image) + bdm_get_by_instance_uuid.return_value = bdms + + self.assertRaises(exception.CertificateValidationFailed, + self.compute_api.rebuild, self.context, instance, + image_href, admin_pass, files_to_inject, + trusted_certs=new_trusted_certs) + + _check_auto_disk_config.assert_called_once_with(image=image) + def _test_check_injected_file_quota_onset_file_limit_exceeded(self, side_effect): injected_files = [ @@ -4272,6 +4460,7 @@ class _ComputeAPIUnitTestMixIn(object): check_server_group_quota = False filter_properties = {'scheduler_hints': None, 'instance_type': flavor} + trusted_certs = None self.assertRaises(expected_exception, self.compute_api._provision_instances, ctxt, @@ -4279,7 +4468,7 @@ class _ComputeAPIUnitTestMixIn(object): boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - None, objects.TagList()) + None, objects.TagList(), trusted_certs) do_test() @@ -4348,7 +4537,7 @@ class _ComputeAPIUnitTestMixIn(object): {}, None, None, None, None, {}, None, fake_keypair, - objects.TagList()) + objects.TagList(), None) self.assertEqual( 'test', mock_instance.return_value.keypairs.objects[0].name) @@ -4357,7 +4546,8 @@ class _ComputeAPIUnitTestMixIn(object): 1, 1, mock.MagicMock(), {}, None, None, None, None, {}, None, - None, objects.TagList()) + None, objects.TagList(), + None) self.assertEqual( 0, len(mock_instance.return_value.keypairs.objects)) @@ -4424,6 +4614,7 @@ class _ComputeAPIUnitTestMixIn(object): mock_volume.get.return_value = {'id': '1', 'multiattach': False} instance_tags = objects.TagList(objects=[objects.Tag(tag='tag')]) shutdown_terminate = True + trusted_certs = None instance_group = None check_server_group_quota = False filter_properties = {'scheduler_hints': None, @@ -4434,7 +4625,7 @@ class _ComputeAPIUnitTestMixIn(object): min_count, max_count, base_options, boot_meta, security_groups, block_device_mappings, shutdown_terminate, instance_group, check_server_group_quota, - filter_properties, None, instance_tags) + filter_properties, None, instance_tags, trusted_certs) for rs, br, im in instances_to_build: self.assertIsInstance(br.instance, objects.Instance) @@ -4508,13 +4699,14 @@ class _ComputeAPIUnitTestMixIn(object): check_server_group_quota = False filter_properties = {'scheduler_hints': None, 'instance_type': flavor} + trusted_certs = None instances_to_build = ( self.compute_api._provision_instances(ctxt, flavor, min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, - filter_properties, None, objects.TagList())) + filter_properties, None, objects.TagList(), trusted_certs)) rs, br, im = instances_to_build[0] self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid)) self.assertEqual(br.instance_uuid, im.instance_uuid) @@ -4605,13 +4797,14 @@ class _ComputeAPIUnitTestMixIn(object): filter_properties = {'scheduler_hints': None, 'instance_type': flavor} tags = objects.TagList() + trusted_certs = None self.assertRaises(exception.InvalidVolume, self.compute_api._provision_instances, ctxt, flavor, min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - None, tags) + None, tags, trusted_certs) # First instance, build_req, mapping is created and destroyed self.assertTrue(build_req_mocks[0].create.called) self.assertTrue(build_req_mocks[0].destroy.called) @@ -4707,13 +4900,14 @@ class _ComputeAPIUnitTestMixIn(object): filter_properties = {'scheduler_hints': None, 'instance_type': flavor} tags = objects.TagList() + trusted_certs = None self.assertRaises(exception.InvalidVolume, self.compute_api._provision_instances, ctxt, flavor, min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - None, tags) + None, tags, trusted_certs) # First instance, build_req, mapping is created and destroyed self.assertTrue(build_req_mocks[0].create.called) self.assertTrue(build_req_mocks[0].destroy.called) @@ -4745,7 +4939,8 @@ class _ComputeAPIUnitTestMixIn(object): self.compute_api._provision_instances(ctxt, None, None, None, mock.MagicMock(), None, None, [], None, None, None, None, - None, objects.TagList()) + None, objects.TagList(), + None) secgroups = mock_secgroup.populate_security_groups.return_value mock_objects.RequestSpec.from_components.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, @@ -5697,6 +5892,60 @@ class _ComputeAPIUnitTestMixIn(object): False) self.assertEqual(0, len(instance.security_groups)) + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_retrieve_trusted_certs_object(self, get_min_version): + ids = ['0b5d2c72-12cc-4ba6-a8d7-3ff5cc1d8cb8', + '674736e3-f25c-405c-8362-bbf991e0ce0a'] + + retrieved_certs = self.compute_api._retrieve_trusted_certs_object( + self.context, ids) + self.assertEqual(ids, retrieved_certs.ids) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS - 1) + def test_retrieve_trusted_certs_object_old_compute(self, get_min_version): + ids = ['trusted-cert-id'] + + self.assertRaises(exception.CertificateValidationNotYetAvailable, + self.compute_api._retrieve_trusted_certs_object, + self.context, ids) + + @mock.patch('nova.objects.service.get_minimum_version_all_cells', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS) + def test_retrieve_trusted_certs_object_conf(self, get_min_version): + ids = ['conf-trusted-cert-1', 'conf-trusted-cert-2'] + + self.flags(verify_glance_signatures=True, group='glance') + self.flags(enable_certificate_validation=True, group='glance') + self.flags(default_trusted_certificate_ids='conf-trusted-cert-1, ' + 'conf-trusted-cert-2', + group='glance') + retrieved_certs = self.compute_api._retrieve_trusted_certs_object( + self.context, None) + self.assertEqual(ids, retrieved_certs.ids) + + def test_retrieve_trusted_certs_object_none(self): + self.flags(enable_certificate_validation=False, group='glance') + self.assertIsNone( + self.compute_api._retrieve_trusted_certs_object(self.context, + None)) + + def test_retrieve_trusted_certs_object_empty(self): + self.flags(enable_certificate_validation=False, group='glance') + self.assertIsNone(self.compute_api._retrieve_trusted_certs_object( + self.context, [])) + + @mock.patch('nova.objects.Service.get_minimum_version', + return_value=compute_api.MIN_COMPUTE_TRUSTED_CERTS - 1) + def test_retrieve_trusted_certs_object_old_compute_rebuild( + self, get_min_version): + ids = ['trusted-cert-id'] + self.assertRaises(exception.CertificateValidationNotYetAvailable, + self.compute_api._retrieve_trusted_certs_object, + self.context, ids, rebuild=True) + get_min_version.assert_called_once_with(self.context, 'nova-compute') + class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase): def setUp(self): diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index cbe1a2591170..947f2ad3b62c 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -377,12 +377,14 @@ class RealRolePolicyTestCase(test.NoDBTestCase): "os_compute_api:servers:create", "os_compute_api:servers:create:attach_network", "os_compute_api:servers:create:attach_volume", +"os_compute_api:servers:create:trusted_certs", "os_compute_api:servers:create_image", "os_compute_api:servers:delete", "os_compute_api:servers:detail", "os_compute_api:servers:index", "os_compute_api:servers:reboot", "os_compute_api:servers:rebuild", +"os_compute_api:servers:rebuild:trusted_certs", "os_compute_api:servers:resize", "os_compute_api:servers:revert_resize", "os_compute_api:servers:show", diff --git a/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml b/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml new file mode 100644 index 000000000000..70843549742c --- /dev/null +++ b/releasenotes/notes/trusted-certs-microversion-589b75f0180d4d51.yaml @@ -0,0 +1,30 @@ +--- +features: + - | + The 2.63 compute REST API microversion adds support for the + ``trusted_image_certificates`` parameter, which is used to define a + list of trusted certificate IDs that can be used during image + signature verification and certificate validation. The list is + restricted to a maximum of 50 IDs. Note that there is not support + with volume-backed servers. + + The ``trusted_image_certificates`` request parameter can be passed to + the server create and rebuild APIs (if allowed by policy): + + * ``POST /servers`` + * ``POST /servers/{server_id}/action (rebuild)`` + + The following policy rules were added to restrict the usage of the + ``trusted_image_certificates`` request parameter in the server create and + rebuild APIs: + + * ``os_compute_api:servers:create:trusted_certs`` + * ``os_compute_api:servers:rebuild:trusted_certs`` + + The ``trusted_image_certificates`` parameter will be in the response + body of the following APIs (not restricted by policy): + + * ``GET /servers/detail`` + * ``GET /servers/{server_id}`` + * ``PUT /servers/{server_id}`` + * ``POST /servers/{server_id}/action (rebuild)``