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
This commit is contained in:
Brianna Poulos 2018-06-06 16:43:56 -04:00 committed by Matt Riedemann
parent ca7d23a3e7
commit 8c7ca368b1
36 changed files with 1803 additions and 38 deletions

View File

@ -5706,6 +5706,40 @@ server_tags_create:
required: false required: false
type: array type: array
min_version: 2.52 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: server_usages:
description: | description: |
A list of the server usage objects. A list of the server usage objects.

View File

@ -488,10 +488,11 @@ Request
- description: server_description - description: server_description
- key_name: key_name_rebuild_req - key_name: key_name_rebuild_req
- user_data: user_data_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 :language: javascript
Response Response
@ -537,10 +538,11 @@ Response
- tags: tags - tags: tags
- key_name: key_name_rebuild_resp - key_name: key_name_rebuild_resp
- user_data: user_data_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 :language: javascript
Remove (Disassociate) Floating Ip (removeFloatingIp Action) (DEPRECATED) Remove (Disassociate) Floating Ip (removeFloatingIp Action) (DEPRECATED)

View File

@ -381,6 +381,7 @@ Request
- os:scheduler_hints.query: os:scheduler_hints_query - os:scheduler_hints.query: os:scheduler_hints_query
- os:scheduler_hints.same_host: os:scheduler_hints_same_host - os:scheduler_hints.same_host: os:scheduler_hints_same_host
- os:scheduler_hints.target_cell: os:scheduler_hints_target_cell - os:scheduler_hints.target_cell: os:scheduler_hints_target_cell
- trusted_image_certificates: server_trusted_image_certificates_create_req
**Example Create Server** **Example Create Server**
@ -392,6 +393,11 @@ Request
.. literalinclude:: ../../doc/api_samples/servers/v2.37/server-create-req.json .. literalinclude:: ../../doc/api_samples/servers/v2.37/server-create-req.json
:language: javascript :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 Response
-------- --------
@ -610,10 +616,11 @@ Response
- host_status: host_status - host_status: host_status
- description: server_description_resp - description: server_description_resp
- tags: tags - 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 :language: javascript
@ -716,10 +723,11 @@ Response
- host_status: host_status - host_status: host_status
- description: server_description_resp - description: server_description_resp
- tags: tags - 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 :language: javascript
Update Server Update Server
@ -808,10 +816,11 @@ Response
- locked: locked - locked: locked
- description: server_description_resp - description: server_description_resp
- tags: tags - 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 :language: javascript
Delete Server Delete Server

View File

@ -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"
}
}

View File

@ -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"
]
}
}

View File

@ -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}"
}
}

View File

@ -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"
}
]
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,8 @@
{
"server": {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"OS-DCF:diskConfig": "AUTO",
"name" : "new-server-test"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

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

View File

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

View File

@ -148,6 +148,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
/flavors APIs. /flavors APIs.
* 2.62 - Add ``host`` and ``hostId`` fields to instance action detail API * 2.62 - Add ``host`` and ``hostId`` fields to instance action detail API
responses. responses.
* 2.63 - Add support for applying trusted certificates when creating or
rebuilding a server.
""" """
# The minimum and maximum versions of the API supported # 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 # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.62" _MAX_API_VERSION = "2.63"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@ -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 ``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 for the ``events.traceback`` field. If the user is prevented by policy, only
``hostId`` will be displayed. ``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)``

View File

@ -148,6 +148,13 @@ base_create_v257 = copy.deepcopy(base_create_v252)
base_create_v257['properties']['server']['properties'].pop('personality') 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 = { base_update = {
'type': 'object', 'type': 'object',
'properties': { '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 = { resize = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -86,6 +86,9 @@ class ServersController(wsgi.Controller):
schema_server_create_v252 = schema_servers.base_create_v252 schema_server_create_v252 = schema_servers.base_create_v252
schema_server_create_v257 = schema_servers.base_create_v257 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 # NOTE(alex_xu): Please do not add more items into this list. This list
# should be removed in the future. # should be removed in the future.
schema_func_list = [ schema_func_list = [
@ -132,6 +135,7 @@ class ServersController(wsgi.Controller):
# TODO(alex_xu): The final goal is that merging all of # TODO(alex_xu): The final goal is that merging all of
# extended json-schema into server main json-schema. # 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_v257, '2.57')
self._create_schema(self.schema_server_create_v252, '2.52') self._create_schema(self.schema_server_create_v252, '2.52')
self._create_schema(self.schema_server_create_v242, '2.42') self._create_schema(self.schema_server_create_v242, '2.42')
@ -304,6 +308,8 @@ class ServersController(wsgi.Controller):
expected_attrs.append('services') expected_attrs.append('services')
if api_version_request.is_supported(req, '2.26'): if api_version_request.is_supported(req, '2.26'):
expected_attrs.append("tags") 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 # merge our expected attrs with what the view builder needs for
# showing details # showing details
@ -345,6 +351,8 @@ class ServersController(wsgi.Controller):
if is_detail: if is_detail:
if api_version_request.is_supported(req, '2.26'): if api_version_request.is_supported(req, '2.26'):
expected_attrs.append("tags") 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 = self._view_builder.get_show_expected_attrs(
expected_attrs) expected_attrs)
instance = common.get_instance(self.compute_api, context, 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_v237, '2.37', '2.41')
@validation.schema(schema_server_create_v242, '2.42', '2.51') @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_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): def create(self, req, body):
"""Creates a new server for a given user.""" """Creates a new server for a given user."""
context = req.environ['nova.context'] context = req.environ['nova.context']
@ -489,6 +498,14 @@ class ServersController(wsgi.Controller):
'availability_zone': availability_zone} 'availability_zone': availability_zone}
context.can(server_policies.SERVERS % 'create', target) 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 # TODO(Shao He, Feng) move this policy check to os-availability-zone
# extension after refactor it. # extension after refactor it.
parse_az = self.compute_api.parse_availability_zone parse_az = self.compute_api.parse_availability_zone
@ -634,13 +651,15 @@ class ServersController(wsgi.Controller):
exception.RealtimeMaskNotFoundOrInvalid, exception.RealtimeMaskNotFoundOrInvalid,
exception.SnapshotNotFound, exception.SnapshotNotFound,
exception.UnableToAutoAllocateNetwork, exception.UnableToAutoAllocateNetwork,
exception.MultiattachNotSupportedOldMicroversion) as error: exception.MultiattachNotSupportedOldMicroversion,
exception.CertificateValidationFailed) as error:
raise exc.HTTPBadRequest(explanation=error.format_message()) raise exc.HTTPBadRequest(explanation=error.format_message())
except (exception.PortInUse, except (exception.PortInUse,
exception.InstanceExists, exception.InstanceExists,
exception.NetworkAmbiguous, exception.NetworkAmbiguous,
exception.NoUniqueMatch, exception.NoUniqueMatch,
exception.MultiattachSupportNotYetAvailable) as error: exception.MultiattachSupportNotYetAvailable,
exception.CertificateValidationNotYetAvailable) as error:
raise exc.HTTPConflict(explanation=error.format_message()) raise exc.HTTPConflict(explanation=error.format_message())
# If the caller wanted a reservation_id, return it # 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, '2.1', '2.18')
@validation.schema(schema_server_rebuild_v219, '2.19', '2.53') @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_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): def _action_rebuild(self, req, id, body):
"""Rebuild an instance with the given attributes.""" """Rebuild an instance with the given attributes."""
rebuild_dict = body['rebuild'] rebuild_dict = body['rebuild']
@ -906,9 +926,9 @@ class ServersController(wsgi.Controller):
context = req.environ['nova.context'] context = req.environ['nova.context']
instance = self._get_server(context, req, id) instance = self._get_server(context, req, id)
context.can(server_policies.SERVERS % 'rebuild', target = {'user_id': instance.user_id,
target={'user_id': instance.user_id, 'project_id': instance.project_id}
'project_id': instance.project_id}) context.can(server_policies.SERVERS % 'rebuild', target=target)
attr_map = { attr_map = {
'name': 'display_name', 'name': 'display_name',
'description': 'display_description', 'description': 'display_description',
@ -930,6 +950,19 @@ class ServersController(wsgi.Controller):
if include_user_data and 'user_data' in rebuild_dict: if include_user_data and 'user_data' in rebuild_dict:
kwargs['user_data'] = rebuild_dict['user_data'] 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(): for request_attribute, instance_attribute in attr_map.items():
try: try:
if request_attribute == 'name': if request_attribute == 'name':
@ -947,7 +980,8 @@ class ServersController(wsgi.Controller):
image_href, image_href,
password, password,
**kwargs) **kwargs)
except exception.InstanceIsLocked as e: except (exception.InstanceIsLocked,
exception.CertificateValidationNotYetAvailable) as e:
raise exc.HTTPConflict(explanation=e.format_message()) raise exc.HTTPConflict(explanation=e.format_message())
except exception.InstanceInvalidState as state_error: except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error, common.raise_http_conflict_for_instance_invalid_state(state_error,
@ -970,7 +1004,8 @@ class ServersController(wsgi.Controller):
exception.FlavorDiskTooSmall, exception.FlavorDiskTooSmall,
exception.FlavorMemoryTooSmall, exception.FlavorMemoryTooSmall,
exception.InvalidMetadata, exception.InvalidMetadata,
exception.AutoDiskConfigDisabledByImage) as error: exception.AutoDiskConfigDisabledByImage,
exception.CertificateValidationFailed) as error:
raise exc.HTTPBadRequest(explanation=error.format_message()) raise exc.HTTPBadRequest(explanation=error.format_message())
instance = self._get_server(context, req, id, is_detail=True) instance = self._get_server(context, req, id, is_detail=True)

View File

@ -171,6 +171,12 @@ class ViewBuilder(common.ViewBuilder):
if api_version_request.is_supported(request, min_version="2.26"): if api_version_request.is_supported(request, min_version="2.26"):
server["server"]["tags"] = [t.tag for t in instance.tags] 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 return server
def index(self, request, instances): def index(self, request, instances):

View File

@ -476,3 +476,16 @@ pagination_parameters = {
'limit': multi_params(non_negative_integer), 'limit': multi_params(non_negative_integer),
'marker': multi_params({'type': 'string'}) '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,
}
}

View File

@ -104,6 +104,7 @@ AGGREGATE_ACTION_ADD = 'Add'
BFV_RESERVE_MIN_COMPUTE_VERSION = 17 BFV_RESERVE_MIN_COMPUTE_VERSION = 17
CINDER_V3_ATTACH_MIN_COMPUTE_VERSION = 24 CINDER_V3_ATTACH_MIN_COMPUTE_VERSION = 24
MIN_COMPUTE_MULTIATTACH = 27 MIN_COMPUTE_MULTIATTACH = 27
MIN_COMPUTE_TRUSTED_CERTS = 31
# FIXME(danms): Keep a global cache of the cells we find the # 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 # 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, max_count, base_options, boot_meta, security_groups,
block_device_mapping, shutdown_terminate, block_device_mapping, shutdown_terminate,
instance_group, check_server_group_quota, filter_properties, instance_group, check_server_group_quota, filter_properties,
key_pair, tags, supports_multiattach=False): key_pair, tags, trusted_certs, supports_multiattach=False):
# Check quotas # Check quotas
num_instances = compute_utils.check_num_instances_quota( num_instances = compute_utils.check_num_instances_quota(
context, instance_type, min_count, max_count) context, instance_type, min_count, max_count)
@ -887,6 +888,10 @@ class API(base.Base):
instance.keypairs = objects.KeyPairList(objects=[]) instance.keypairs = objects.KeyPairList(objects=[])
if key_pair: if key_pair:
instance.keypairs.objects.append(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 = self.create_db_entry_for_new_instance(context,
instance_type, boot_meta, instance, security_groups, instance_type, boot_meta, instance, security_groups,
block_device_mapping, num_instances, i, block_device_mapping, num_instances, i,
@ -961,6 +966,65 @@ class API(base.Base):
return instances_to_build 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, def _get_bdm_image_metadata(self, context, block_device_mapping,
legacy_bdm=True): legacy_bdm=True):
"""If we are booting from a volume, we need to get the """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, block_device_mapping, auto_disk_config, filter_properties,
reservation_id=None, legacy_bdm=True, shutdown_terminate=False, reservation_id=None, legacy_bdm=True, shutdown_terminate=False,
check_server_group_quota=False, tags=None, 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 """Verify all the input parameters regardless of the provisioning
strategy being performed and schedule the instance(s) for strategy being performed and schedule the instance(s) for
creation. creation.
@ -1049,6 +1113,14 @@ class API(base.Base):
if image_href: if image_href:
image_id, boot_meta = self._get_image(context, image_href) image_id, boot_meta = self._get_image(context, image_href)
else: 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 image_id = None
boot_meta = self._get_bdm_image_metadata( boot_meta = self._get_bdm_image_metadata(
context, block_device_mapping, legacy_bdm) context, block_device_mapping, legacy_bdm)
@ -1096,7 +1168,8 @@ class API(base.Base):
context, instance_type, min_count, max_count, base_options, context, instance_type, min_count, max_count, base_options,
boot_meta, security_groups, block_device_mapping, boot_meta, security_groups, block_device_mapping,
shutdown_terminate, instance_group, check_server_group_quota, 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 = [] instances = []
request_specs = [] request_specs = []
@ -1577,7 +1650,7 @@ class API(base.Base):
config_drive=None, auto_disk_config=None, scheduler_hints=None, config_drive=None, auto_disk_config=None, scheduler_hints=None,
legacy_bdm=True, shutdown_terminate=False, legacy_bdm=True, shutdown_terminate=False,
check_server_group_quota=False, tags=None, check_server_group_quota=False, tags=None,
supports_multiattach=False): supports_multiattach=False, trusted_certs=None):
"""Provision instances, sending instance information to the """Provision instances, sending instance information to the
scheduler. The scheduler will determine where the instance(s) scheduler. The scheduler will determine where the instance(s)
go and will handle creating the DB entries. go and will handle creating the DB entries.
@ -1617,7 +1690,8 @@ class API(base.Base):
legacy_bdm=legacy_bdm, legacy_bdm=legacy_bdm,
shutdown_terminate=shutdown_terminate, shutdown_terminate=shutdown_terminate,
check_server_group_quota=check_server_group_quota, 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, def _check_auto_disk_config(self, instance=None, image=None,
**extra_instance_updates): **extra_instance_updates):
@ -2998,6 +3072,18 @@ class API(base.Base):
instance.key_data = None instance.key_data = None
instance.keypairs = objects.KeyPairList(objects=[]) 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) image_id, image = self._get_image(context, image_href)
self._check_auto_disk_config(image=image, **kwargs) 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( is_volume_backed = compute_utils.is_volume_backed_instance(
context, instance, bdms) context, instance, bdms)
if is_volume_backed: 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 # For boot from volume, instance.image_ref is empty, so we need to
# query the image from the volume. # query the image from the volume.
if root_bdm is None: if root_bdm is None:

View File

@ -2297,3 +2297,9 @@ class AllocationCreateFailed(NovaException):
class CertificateValidationFailed(NovaException): class CertificateValidationFailed(NovaException):
msg_fmt = _("Image signature certificate validation failed for " msg_fmt = _("Image signature certificate validation failed for "
"certificate: %(cert_uuid)s. %(reason)s") "certificate: %(cert_uuid)s. %(reason)s")
class CertificateValidationNotYetAvailable(NovaException):
msg_fmt = _("Image signature certificate validation support is "
"not yet available.")
code = 409

View File

@ -127,6 +127,16 @@ rules = [
'path': '/servers' 'path': '/servers'
} }
]), ]),
policy.DocumentedRuleDefault(
SERVERS % 'create:trusted_certs',
RULE_AOO,
"Create a server with trusted image certificate IDs",
[
{
'method': 'POST',
'path': '/servers'
}
]),
policy.DocumentedRuleDefault( policy.DocumentedRuleDefault(
NETWORK_ATTACH_EXTERNAL, NETWORK_ATTACH_EXTERNAL,
'is_admin:True', 'is_admin:True',
@ -213,6 +223,16 @@ rules = [
'path': '/servers/{server_id}/action (rebuild)' '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( policy.DocumentedRuleDefault(
SERVERS % 'create_image', SERVERS % 'create_image',
RULE_AOO, RULE_AOO,

View File

@ -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"
}
}

View File

@ -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"
]
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}
}

View File

@ -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"
]
}
}

View File

@ -0,0 +1,8 @@
{
"server": {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"OS-DCF:diskConfig": "AUTO",
"name" : "new-server-test"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -19,10 +19,13 @@ import time
import six import six
from nova.api.openstack import api_version_request as avr 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.functional.api_sample_tests import api_sample_base
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
from nova.tests.unit.image import fake from nova.tests.unit.image import fake
CONF = nova.conf.CONF
class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21): class ServersSampleBase(api_sample_base.ApiSampleTestBaseV21):
microversion = None microversion = None
@ -237,6 +240,75 @@ class ServersSampleJson252Test(ServersSampleJsonTest):
use_common_server_post = False 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): class ServersUpdateSampleJsonTest(ServersSampleBase):
def test_update_server(self): def test_update_server(self):

View File

@ -2406,6 +2406,200 @@ class ServersControllerRebuildTestV219(ServersControllerRebuildInstanceTest):
self.req, FAKE_UUID, body=self.body) 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): class ServersControllerUpdateTest(ControllerTest):
def _get_request(self, body=None): def _get_request(self, body=None):
@ -4215,6 +4409,137 @@ class ServersControllerCreateTestV260(test.NoDBTestCase):
six.text_type(ex)) 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): class ServersControllerCreateTestWithMock(test.TestCase):
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
flavor_ref = 'http://localhost/123/flavors/3' flavor_ref = 'http://localhost/123/flavors/3'

View File

@ -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) 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 stub_out_instance_quota(test, allowed, quota, resource='instances'):
def fake_reserve(context, **deltas): def fake_reserve(context, **deltas):
requested = deltas.pop(resource, 0) 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, memory_mb=0, vcpus=0, root_gb=0, ephemeral_gb=0,
instance_type=None, launch_index=0, kernel_id="", instance_type=None, launch_index=0, kernel_id="",
ramdisk_id="", user_data=None, system_metadata=None, ramdisk_id="", user_data=None, system_metadata=None,
services=None): services=None, trusted_certs=None):
if user_id is None: if user_id is None:
user_id = 'fake_user' user_id = 'fake_user'
if project_id is None: 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, "extra": {"numa_topology": None,
"pci_requests": None, "pci_requests": None,
"flavor": flavorinfo, "flavor": flavorinfo,
}, "trusted_certs": trusted_certs,
},
"cleaned": cleaned, "cleaned": cleaned,
"services": services, "services": services,
"tags": []} "tags": [],
}
instance.update(info_cache) instance.update(info_cache)
instance['info_cache']['instance_uuid'] = instance['uuid'] instance['info_cache']['instance_uuid'] = instance['uuid']

View File

@ -262,6 +262,29 @@ class _ComputeAPIUnitTestMixIn(object):
self.assertEqual(2, mock_get_image.call_count) self.assertEqual(2, mock_get_image.call_count)
self.assertEqual(2, mock_limit_check_pu.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): def _test_create_max_net_count(self, max_net_count, min_count, max_count):
with test.nested( with test.nested(
mock.patch.object(self.compute_api, '_get_image', 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_name, instance.key_name)
self.assertNotEqual(orig_key_data, instance.key_data) 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, def _test_check_injected_file_quota_onset_file_limit_exceeded(self,
side_effect): side_effect):
injected_files = [ injected_files = [
@ -4272,6 +4460,7 @@ class _ComputeAPIUnitTestMixIn(object):
check_server_group_quota = False check_server_group_quota = False
filter_properties = {'scheduler_hints': None, filter_properties = {'scheduler_hints': None,
'instance_type': flavor} 'instance_type': flavor}
trusted_certs = None
self.assertRaises(expected_exception, self.assertRaises(expected_exception,
self.compute_api._provision_instances, ctxt, self.compute_api._provision_instances, ctxt,
@ -4279,7 +4468,7 @@ class _ComputeAPIUnitTestMixIn(object):
boot_meta, security_groups, block_device_mapping, boot_meta, security_groups, block_device_mapping,
shutdown_terminate, instance_group, shutdown_terminate, instance_group,
check_server_group_quota, filter_properties, check_server_group_quota, filter_properties,
None, objects.TagList()) None, objects.TagList(), trusted_certs)
do_test() do_test()
@ -4348,7 +4537,7 @@ class _ComputeAPIUnitTestMixIn(object):
{}, None, {}, None,
None, None, None, {}, None, None, None, None, {}, None,
fake_keypair, fake_keypair,
objects.TagList()) objects.TagList(), None)
self.assertEqual( self.assertEqual(
'test', 'test',
mock_instance.return_value.keypairs.objects[0].name) mock_instance.return_value.keypairs.objects[0].name)
@ -4357,7 +4546,8 @@ class _ComputeAPIUnitTestMixIn(object):
1, 1, mock.MagicMock(), 1, 1, mock.MagicMock(),
{}, None, {}, None,
None, None, None, {}, None, None, None, None, {}, None,
None, objects.TagList()) None, objects.TagList(),
None)
self.assertEqual( self.assertEqual(
0, 0,
len(mock_instance.return_value.keypairs.objects)) len(mock_instance.return_value.keypairs.objects))
@ -4424,6 +4614,7 @@ class _ComputeAPIUnitTestMixIn(object):
mock_volume.get.return_value = {'id': '1', 'multiattach': False} mock_volume.get.return_value = {'id': '1', 'multiattach': False}
instance_tags = objects.TagList(objects=[objects.Tag(tag='tag')]) instance_tags = objects.TagList(objects=[objects.Tag(tag='tag')])
shutdown_terminate = True shutdown_terminate = True
trusted_certs = None
instance_group = None instance_group = None
check_server_group_quota = False check_server_group_quota = False
filter_properties = {'scheduler_hints': None, filter_properties = {'scheduler_hints': None,
@ -4434,7 +4625,7 @@ class _ComputeAPIUnitTestMixIn(object):
min_count, max_count, base_options, boot_meta, min_count, max_count, base_options, boot_meta,
security_groups, block_device_mappings, shutdown_terminate, security_groups, block_device_mappings, shutdown_terminate,
instance_group, check_server_group_quota, 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: for rs, br, im in instances_to_build:
self.assertIsInstance(br.instance, objects.Instance) self.assertIsInstance(br.instance, objects.Instance)
@ -4508,13 +4699,14 @@ class _ComputeAPIUnitTestMixIn(object):
check_server_group_quota = False check_server_group_quota = False
filter_properties = {'scheduler_hints': None, filter_properties = {'scheduler_hints': None,
'instance_type': flavor} 'instance_type': flavor}
trusted_certs = None
instances_to_build = ( instances_to_build = (
self.compute_api._provision_instances(ctxt, flavor, self.compute_api._provision_instances(ctxt, flavor,
min_count, max_count, base_options, boot_meta, min_count, max_count, base_options, boot_meta,
security_groups, block_device_mapping, shutdown_terminate, security_groups, block_device_mapping, shutdown_terminate,
instance_group, check_server_group_quota, 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] rs, br, im = instances_to_build[0]
self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid)) self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid))
self.assertEqual(br.instance_uuid, im.instance_uuid) self.assertEqual(br.instance_uuid, im.instance_uuid)
@ -4605,13 +4797,14 @@ class _ComputeAPIUnitTestMixIn(object):
filter_properties = {'scheduler_hints': None, filter_properties = {'scheduler_hints': None,
'instance_type': flavor} 'instance_type': flavor}
tags = objects.TagList() tags = objects.TagList()
trusted_certs = None
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.compute_api._provision_instances, ctxt, self.compute_api._provision_instances, ctxt,
flavor, min_count, max_count, base_options, flavor, min_count, max_count, base_options,
boot_meta, security_groups, block_device_mapping, boot_meta, security_groups, block_device_mapping,
shutdown_terminate, instance_group, shutdown_terminate, instance_group,
check_server_group_quota, filter_properties, check_server_group_quota, filter_properties,
None, tags) None, tags, trusted_certs)
# First instance, build_req, mapping is created and destroyed # First instance, build_req, mapping is created and destroyed
self.assertTrue(build_req_mocks[0].create.called) self.assertTrue(build_req_mocks[0].create.called)
self.assertTrue(build_req_mocks[0].destroy.called) self.assertTrue(build_req_mocks[0].destroy.called)
@ -4707,13 +4900,14 @@ class _ComputeAPIUnitTestMixIn(object):
filter_properties = {'scheduler_hints': None, filter_properties = {'scheduler_hints': None,
'instance_type': flavor} 'instance_type': flavor}
tags = objects.TagList() tags = objects.TagList()
trusted_certs = None
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.InvalidVolume,
self.compute_api._provision_instances, ctxt, self.compute_api._provision_instances, ctxt,
flavor, min_count, max_count, base_options, flavor, min_count, max_count, base_options,
boot_meta, security_groups, block_device_mapping, boot_meta, security_groups, block_device_mapping,
shutdown_terminate, instance_group, shutdown_terminate, instance_group,
check_server_group_quota, filter_properties, check_server_group_quota, filter_properties,
None, tags) None, tags, trusted_certs)
# First instance, build_req, mapping is created and destroyed # First instance, build_req, mapping is created and destroyed
self.assertTrue(build_req_mocks[0].create.called) self.assertTrue(build_req_mocks[0].create.called)
self.assertTrue(build_req_mocks[0].destroy.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, self.compute_api._provision_instances(ctxt, None, None, None,
mock.MagicMock(), None, None, mock.MagicMock(), None, None,
[], None, None, None, None, [], None, None, None, None,
None, objects.TagList()) None, objects.TagList(),
None)
secgroups = mock_secgroup.populate_security_groups.return_value secgroups = mock_secgroup.populate_security_groups.return_value
mock_objects.RequestSpec.from_components.assert_called_once_with( mock_objects.RequestSpec.from_components.assert_called_once_with(
mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
@ -5697,6 +5892,60 @@ class _ComputeAPIUnitTestMixIn(object):
False) False)
self.assertEqual(0, len(instance.security_groups)) 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): class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
def setUp(self): def setUp(self):

View File

@ -377,12 +377,14 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:servers:create", "os_compute_api:servers:create",
"os_compute_api:servers:create:attach_network", "os_compute_api:servers:create:attach_network",
"os_compute_api:servers:create:attach_volume", "os_compute_api:servers:create:attach_volume",
"os_compute_api:servers:create:trusted_certs",
"os_compute_api:servers:create_image", "os_compute_api:servers:create_image",
"os_compute_api:servers:delete", "os_compute_api:servers:delete",
"os_compute_api:servers:detail", "os_compute_api:servers:detail",
"os_compute_api:servers:index", "os_compute_api:servers:index",
"os_compute_api:servers:reboot", "os_compute_api:servers:reboot",
"os_compute_api:servers:rebuild", "os_compute_api:servers:rebuild",
"os_compute_api:servers:rebuild:trusted_certs",
"os_compute_api:servers:resize", "os_compute_api:servers:resize",
"os_compute_api:servers:revert_resize", "os_compute_api:servers:revert_resize",
"os_compute_api:servers:show", "os_compute_api:servers:show",

View File

@ -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)``