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
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.

View File

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

View File

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

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",
"version": "2.62",
"version": "2.63",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

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

View File

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

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

View File

@ -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': {

View File

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

View File

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

View File

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

View File

@ -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:

View File

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

View File

@ -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,

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
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):

View File

@ -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'

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)
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']

View File

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

View File

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

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