Merge "Microversion 2.73: Support adding the reason behind a server lock"

This commit is contained in:
Zuul 2019-05-12 02:05:18 +00:00 committed by Gerrit Code Review
commit cf76777b61
64 changed files with 1408 additions and 64 deletions

View File

@ -151,6 +151,7 @@ For different user roles, the user has different query options set:
- ``tags`` (New in version 2.26)
- ``tags-any`` (New in version 2.26)
- ``changes-before`` (New in version 2.66)
- ``locked`` (New in version 2.73)
Other options will be ignored by nova silently.

View File

@ -975,6 +975,18 @@ locked_by_query_server:
in: query
required: false
type: string
locked_query_server:
description: |
Specify the ``locked`` query parameter to list all locked or unlocked
instances. If the value is specified, ``1``, ``t``, ``true``,
``on``, ``y`` and ``yes`` are treated as ``True``. ``0``, ``f``,
``false``, ``off``, ``n`` and ``no`` are treated as ``False``.
(They are case-insensitive.) Any other value provided will be considered
invalid.
in: query
required: false
type: boolean
min_version: 2.73
marker:
description: |
The ID of the last-seen item. Use the ``limit`` parameter to make an initial limited
@ -1259,6 +1271,7 @@ sort_key_server:
- ``key_name``
- ``launch_index``
- ``launched_at``
- ``locked`` (New in version 2.73)
- ``locked_by``
- ``node``
- ``power_state``
@ -4315,9 +4328,11 @@ local_gb_used_total:
lock:
description: |
The action to lock a server.
This parameter can be ``null``.
Up to microversion 2.73, this parameter should be ``null``.
in: body
required: true
type: none
type: object
locked:
description: |
True if the instance is locked otherwise False.
@ -4325,6 +4340,20 @@ locked:
required: true
type: boolean
min_version: 2.9
locked_reason_req:
description: |
The reason behind locking a server. Limited to 255 characters in length.
in: body
required: false
type: string
min_version: 2.73
locked_reason_resp:
description: |
The reason behind locking a server.
in: body
required: true
type: string
min_version: 2.73
mac_addr:
description: |
The MAC address.

View File

@ -383,15 +383,16 @@ See the "Lock, Unlock" item in `Server actions
<https://developer.openstack.org/api-guide/compute/server_concepts.html#server-actions>`_
for the restricted actions.
But administrators can perform actions on the server
even though the server is locked.
even though the server is locked. Note that from microversion 2.73 it is
possible to specify a reason when locking the server.
The `unlock action
<https://developer.openstack.org/api-ref/compute/#unlock-server-unlock-action>`_
will unlock a server in locked state so additional actions can
be performed on the server by non-admin users.
You can know whether a server is locked or not
by the `List Servers Detailed API
You can know whether a server is locked or not and the ``locked_reason``
(if specified, from the 2.73 microversion) by the `List Servers Detailed API
<https://developer.openstack.org/api-ref/compute/#list-servers-detailed>`_
or
the `Show Server Details API
@ -414,12 +415,18 @@ Request
- server_id: server_id_path
- lock: lock
- locked_reason: locked_reason_req
**Example Lock Server (lock Action)**
.. literalinclude:: ../../doc/api_samples/os-lock-server/lock-server.json
:language: javascript
**Example Lock Server (lock Action) (v2.73)**
.. literalinclude:: ../../doc/api_samples/os-lock-server/v2.73/lock-server-with-reason.json
:language: javascript
Response
--------
@ -626,10 +633,11 @@ Response
- user_data: user_data_rebuild_resp
- trusted_image_certificates: server_trusted_image_certificates_resp
- server_groups: server_groups_2_71
- locked_reason: locked_reason_resp
**Example Rebuild Server (rebuild Action) (v2.63)**
**Example Rebuild Server (rebuild Action) (v2.73)**
.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-action-rebuild-resp.json
.. literalinclude:: ../../doc/api_samples/servers/v2.73/server-action-rebuild-resp.json
:language: javascript
Remove (Disassociate) Floating Ip (removeFloatingIp Action) (DEPRECATED)

View File

@ -170,6 +170,7 @@ whitelist will be silently ignored.
- ``tags`` (New in version 2.26)
- ``tags-any`` (New in version 2.26)
- ``changes-before`` (New in version 2.66)
- ``locked`` (New in version 2.73)
- For admin user, whitelist includes all filter keys mentioned in
@ -240,6 +241,7 @@ Request
- tags: tags_query
- tags-any: tags_any_query
- changes-before: changes_before_server
- locked: locked_query_server
Response
--------
@ -577,6 +579,7 @@ Request
- tags: tags_query
- tags-any: tags_any_query
- changes-before: changes_before_server
- locked: locked_query_server
Response
--------
@ -646,10 +649,11 @@ Response
- description: server_description_resp
- tags: tags
- trusted_image_certificates: server_trusted_image_certificates_resp
- locked_reason: locked_reason_resp
**Example List Servers Detailed (2.63)**
**Example List Servers Detailed (2.73)**
.. literalinclude:: /../../doc/api_samples/servers/v2.63/servers-details-resp.json
.. literalinclude:: /../../doc/api_samples/servers/v2.73/servers-details-resp.json
:language: javascript
**Example List Servers Detailed (2.69)**
@ -769,10 +773,11 @@ Response
- tags: tags
- trusted_image_certificates: server_trusted_image_certificates_resp
- server_groups: server_groups_2_71
- locked_reason: locked_reason_resp
**Example Show Server Details (2.63)**
**Example Show Server Details (2.73)**
.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-get-resp.json
.. literalinclude:: ../../doc/api_samples/servers/v2.73/server-get-resp.json
:language: javascript
**Example Show Server Details (2.69)**
@ -861,10 +866,11 @@ Response
- tags: tags
- trusted_image_certificates: server_trusted_image_certificates_resp
- server_groups: server_groups_2_71
- locked_reason: locked_reason_resp
**Example Update Server (2.63)**
**Example Update Server (2.73)**
.. literalinclude:: ../../doc/api_samples/servers/v2.63/server-update-resp.json
.. literalinclude:: ../../doc/api_samples/servers/v2.73/server-update-resp.json
:language: javascript
Delete Server

View File

@ -0,0 +1,3 @@
{
"lock": {"locked_reason": "I don't want to work"}
}

View File

@ -0,0 +1,3 @@
{
"lock": null
}

View File

@ -0,0 +1,3 @@
{
"unlock": null
}

View File

@ -0,0 +1,3 @@
{
"lock": {"locked_reason": "I don't want to work"}
}

View File

@ -0,0 +1,64 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"adminPass": "seekr3t",
"created": "2019-04-23T17:10:22Z",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"id": "0c37a84a-c757-4f22-8c7f-0bf8b6970886",
"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/0c37a84a-c757-4f22-8c7f-0bf8b6970886",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/0c37a84a-c757-4f22-8c7f-0bf8b6970886",
"rel": "bookmark"
}
],
"locked": false,
"locked_reason": null,
"metadata": {
"meta_var": "meta_val"
},
"name": "foobar",
"progress": 0,
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "2019-04-23T17:10:24Z",
"user_data": "ZWNobyAiaGVsbG8gd29ybGQi",
"user_id": "fake"
}
}

View File

@ -0,0 +1,14 @@
{
"rebuild" : {
"accessIPv4" : "1.2.3.4",
"accessIPv6" : "80fe::",
"OS-DCF:diskConfig": "AUTO",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"name" : "foobar",
"adminPass" : "seekr3t",
"metadata" : {
"meta_var" : "meta_val"
},
"user_data": "ZWNobyAiaGVsbG8gd29ybGQi"
}
}

View File

@ -0,0 +1,20 @@
{
"server" : {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"name" : "new-server-test",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "1",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==",
"networks": "auto"
}
}

View File

@ -0,0 +1,22 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"adminPass": "kJTmMkszoB6A",
"id": "ae10adbb-9b5e-4667-9cc5-05ebdc80a941",
"links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/ae10adbb-9b5e-4667-9cc5-05ebdc80a941",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/ae10adbb-9b5e-4667-9cc5-05ebdc80a941",
"rel": "bookmark"
}
],
"security_groups": [
{
"name": "default"
}
]
}
}

View File

@ -0,0 +1,88 @@
{
"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-t61j9da6",
"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": "2019-04-23T15:19:10.855016",
"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": "2019-04-23T15:19:09Z",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"host_status": "UP",
"id": "0e12087a-7c87-476a-8f84-7398e991cecc",
"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/0e12087a-7c87-476a-8f84-7398e991cecc",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/0e12087a-7c87-476a-8f84-7398e991cecc",
"rel": "bookmark"
}
],
"locked": true,
"locked_reason": "I don't want to work",
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"os-extended-volumes:volumes_attached": [],
"progress": 0,
"security_groups": [
{
"name": "default"
}
],
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "2019-04-23T15:19:11Z",
"user_id": "fake"
}
}

View File

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

View File

@ -0,0 +1,61 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"created": "2019-04-23T17:37:48Z",
"description": "Sample description",
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"id": "f9a6c4fe-28e0-48a9-b02c-164e4d04d0b2",
"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/f9a6c4fe-28e0-48a9-b02c-164e4d04d0b2",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/f9a6c4fe-28e0-48a9-b02c-164e4d04d0b2",
"rel": "bookmark"
}
],
"locked": false,
"locked_reason": null,
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "2019-04-23T17:37:48Z",
"user_id": "fake"
}
}

View File

@ -0,0 +1,89 @@
{
"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-l0i0clt2",
"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": "2019-04-23T15:19:15.317839",
"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": "2019-04-23T15:19:14Z",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"host_status": "UP",
"id": "2ce4c5b3-2866-4972-93ce-77a2ea46a7f9",
"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/2ce4c5b3-2866-4972-93ce-77a2ea46a7f9",
"rel": "self"
},
{
"href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/2ce4c5b3-2866-4972-93ce-77a2ea46a7f9",
"rel": "bookmark"
}
],
"locked": true,
"locked_reason": "I don't want to work",
"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": null,
"updated": "2019-04-23T15:19:15Z",
"user_id": "fake"
}
]
}

View File

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

View File

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

View File

@ -5,5 +5,5 @@
},
"nova_object.name":"InstanceActionPayload",
"nova_object.namespace":"nova",
"nova_object.version":"1.7"
"nova_object.version":"1.8"
}

View File

@ -9,5 +9,5 @@
]
},
"nova_object.name": "InstanceActionRebuildPayload",
"nova_object.version": "1.8"
"nova_object.version": "1.9"
}

View File

@ -4,5 +4,5 @@
"rescue_image_ref": "a2459075-d96c-40d5-893e-577ff92e721c"
},
"nova_object.name": "InstanceActionRescuePayload",
"nova_object.version": "1.2"
"nova_object.version": "1.3"
}

View File

@ -27,5 +27,5 @@
"task_state": "resize_prep"
},
"nova_object.name": "InstanceActionResizePrepPayload",
"nova_object.version": "1.2"
"nova_object.version": "1.3"
}

View File

@ -5,5 +5,5 @@
},
"nova_object.name":"InstanceActionSnapshotPayload",
"nova_object.namespace":"nova",
"nova_object.version":"1.8"
"nova_object.version":"1.9"
}

View File

@ -5,5 +5,5 @@
},
"nova_object.name": "InstanceActionVolumePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.5"
"nova_object.version": "1.6"
}

View File

@ -6,5 +6,5 @@
},
"nova_object.name": "InstanceActionVolumeSwapPayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.7"
"nova_object.version": "1.8"
}

View File

@ -24,5 +24,5 @@
"instance_name": "instance-00000001"
},
"nova_object.name":"InstanceCreatePayload",
"nova_object.version": "1.11"
"nova_object.version": "1.12"
}

View File

@ -8,5 +8,5 @@
},
"nova_object.name":"InstanceExistsPayload",
"nova_object.namespace":"nova",
"nova_object.version":"1.1"
"nova_object.version":"1.2"
}

View File

@ -37,9 +37,10 @@
"uuid":"178b0921-8f85-4257-88b6-2e743b5a975c",
"request_id": "req-5b6c791d-5709-4f36-8fbe-c3e02869e35d",
"action_initiator_user": "fake",
"action_initiator_project": "6f70656e737461636b20342065766572"
"action_initiator_project": "6f70656e737461636b20342065766572",
"locked_reason": null
},
"nova_object.name":"InstancePayload",
"nova_object.namespace":"nova",
"nova_object.version":"1.7"
"nova_object.version":"1.8"
}

View File

@ -29,5 +29,5 @@
},
"nova_object.name": "InstanceUpdatePayload",
"nova_object.namespace": "nova",
"nova_object.version": "1.8"
"nova_object.version": "1.9"
}

View File

@ -0,0 +1,12 @@
{
"event_type":"instance.lock",
"payload":{
"$ref": "common_payloads/InstanceActionPayload.json#",
"nova_object.data":{
"locked":true,
"locked_reason":"global warming"
}
},
"priority":"INFO",
"publisher_id":"nova-api:fake-mini"
}

View File

@ -3,7 +3,8 @@
"payload":{
"$ref": "common_payloads/InstanceActionPayload.json#",
"nova_object.data":{
"locked":true
"locked":true,
"locked_reason": null
}
},
"priority":"INFO",

View File

@ -178,6 +178,12 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.72 - Add support for neutron ports with resource request during server
create. Server move operations are not yet supported for servers
with such ports.
* 2.73 - Adds support for specifying a reason when locking the server and
exposes this via the response from ``GET /servers/detail``,
``GET /servers/{server_id}``, ``PUT servers/{server_id}`` and
``POST /servers/{server_id}/action`` where the action is rebuild.
It also supports ``locked`` as a filter/sort parameter for
``GET /servers/detail`` and ``GET /servers``.
"""
# The minimum and maximum versions of the API supported
@ -186,7 +192,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.72"
_MAX_API_VERSION = "2.73"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@ -512,6 +512,21 @@ def is_all_tenants(search_opts):
return all_tenants
def is_locked(search_opts):
"""Converts the value of the locked parameter to a boolean. Note that
this function will be called only if locked exists in search_opts.
:param dict search_opts: The search options for a request
:returns: boolean indicating if locked is being requested or not
"""
locked = search_opts.get('locked')
try:
locked = strutils.bool_from_string(locked, strict=True)
except ValueError as err:
raise exception.InvalidInput(six.text_type(err))
return locked
def supports_multiattach_volume(req):
"""Check to see if the requested API version is high enough for multiattach

View File

@ -13,8 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import lock_server
from nova.api.openstack import wsgi
from nova.api import validation
from nova import compute
from nova.policies import lock_server as ls_policies
@ -27,6 +30,7 @@ class LockServerController(wsgi.Controller):
@wsgi.response(202)
@wsgi.expected_errors(404)
@wsgi.action('lock')
@validation.schema(lock_server.lock_v2_73, "2.73")
def _lock(self, req, id, body):
"""Lock a server instance."""
context = req.environ['nova.context']
@ -34,7 +38,11 @@ class LockServerController(wsgi.Controller):
context.can(ls_policies.POLICY_ROOT % 'lock',
target={'user_id': instance.user_id,
'project_id': instance.project_id})
self.compute_api.lock(context, instance)
reason = None
if (api_version_request.is_supported(req, min_version='2.73') and
body['lock'] is not None):
reason = body['lock'].get('locked_reason')
self.compute_api.lock(context, instance, reason=reason)
@wsgi.response(202)
@wsgi.expected_errors(404)

View File

@ -930,3 +930,13 @@ API limitations:
request is not yet supported.
.. _QoS minimum bandwidth rule: https://docs.openstack.org/neutron/latest/admin/config-qos-min-bw.html
2.73
----
API microversion 2.73 adds support for specifying a reason when locking the
server and exposes this information via ``GET /servers/detail``,
``GET /servers/{server_id}``, ``PUT servers/{server_id}`` and
``POST /servers/{server_id}/action`` where the action is rebuild. It also
supports ``locked`` as a filter/sort parameter for ``GET /servers/detail``
and ``GET /servers``.

View File

@ -0,0 +1,28 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
lock_v2_73 = {
'type': 'object',
'properties': {
'lock': {
'type': ['object', 'null'],
'properties': {
'locked_reason': {
'type': 'string', 'minLength': 1, 'maxLength': 255,
},
},
'additionalProperties': False,
},
},
'required': ['lock'],
'additionalProperties': False,
}

View File

@ -531,6 +531,9 @@ SERVER_LIST_IGNORE_SORT_KEY = [
'shutdown_terminate', 'user_data', 'vcpus', 'vm_mode'
]
# From microversion 2.73 we start offering locked as a valid sort key.
SERVER_LIST_IGNORE_SORT_KEY_V273 = list(SERVER_LIST_IGNORE_SORT_KEY)
SERVER_LIST_IGNORE_SORT_KEY_V273.remove('locked')
VALID_SORT_KEYS = {
"type": "string",
@ -545,6 +548,14 @@ VALID_SORT_KEYS = {
SERVER_LIST_IGNORE_SORT_KEY
}
# We reuse the existing list and add locked to the list of valid sort keys.
VALID_SORT_KEYS_V273 = {
"type": "string",
"enum": ['locked'] + list(
set(VALID_SORT_KEYS["enum"]) - set(SERVER_LIST_IGNORE_SORT_KEY)) +
SERVER_LIST_IGNORE_SORT_KEY_V273
}
query_params_v21 = {
'type': 'object',
'properties': {
@ -634,3 +645,9 @@ query_params_v266['properties'].update({
'changes-before': multi_params({'type': 'string',
'format': 'date-time'}),
})
query_params_v273 = copy.deepcopy(query_params_v266)
query_params_v273['properties'].update({
'sort_key': multi_params(VALID_SORT_KEYS_V273),
'locked': parameter_types.common_query_param,
})

View File

@ -111,7 +111,8 @@ class ServersController(wsgi.Controller):
self.network_api = network_api.API()
@wsgi.expected_errors((400, 403))
@validation.query_schema(schema_servers.query_params_v266, '2.66')
@validation.query_schema(schema_servers.query_params_v273, '2.73')
@validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72')
@validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65')
@validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25')
def index(self, req):
@ -125,7 +126,8 @@ class ServersController(wsgi.Controller):
return servers
@wsgi.expected_errors((400, 403))
@validation.query_schema(schema_servers.query_params_v266, '2.66')
@validation.query_schema(schema_servers.query_params_v273, '2.73')
@validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72')
@validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65')
@validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25')
def detail(self, req):
@ -273,6 +275,9 @@ class ServersController(wsgi.Controller):
# further down the stack.
search_opts.pop('all_tenants', None)
if 'locked' in search_opts:
search_opts['locked'] = common.is_locked(search_opts)
elevated = None
if all_tenants:
if is_detail:
@ -291,9 +296,11 @@ class ServersController(wsgi.Controller):
limit, marker = common.get_limit_and_marker(req)
sort_keys, sort_dirs = common.get_sort_params(req.params)
blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY
if api_version_request.is_supported(req, min_version='2.73'):
blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY_V273
sort_keys, sort_dirs = remove_invalid_sort_keys(
context, sort_keys, sort_dirs,
schema_servers.SERVER_LIST_IGNORE_SORT_KEY, ('host', 'node'))
context, sort_keys, sort_dirs, blacklist, ('host', 'node'))
expected_attrs = []
if is_detail:
@ -303,6 +310,8 @@ class ServersController(wsgi.Controller):
expected_attrs.append("tags")
if api_version_request.is_supported(req, '2.63'):
expected_attrs.append("trusted_certs")
if api_version_request.is_supported(req, '2.73'):
expected_attrs.append("system_metadata")
# merge our expected attrs with what the view builder needs for
# showing details
@ -1220,6 +1229,8 @@ class ServersController(wsgi.Controller):
opt_list += TAG_SEARCH_FILTERS
if api_version_request.is_supported(req, min_version='2.66'):
opt_list += ('changes-before',)
if api_version_request.is_supported(req, min_version='2.73'):
opt_list += ('locked',)
return opt_list
def _get_instance(self, context, instance_uuid):

View File

@ -330,6 +330,10 @@ class ViewBuilder(common.ViewBuilder):
server["server"]["locked"] = (True if instance["locked_by"]
else False)
if api_version_request.is_supported(request, min_version="2.73"):
server["server"]["locked_reason"] = (instance.system_metadata.get(
"locked_reason"))
if api_version_request.is_supported(request, min_version="2.19"):
server["server"]["description"] = instance.get(
"display_description")

View File

@ -4023,7 +4023,7 @@ class API(base.Base):
return self.compute_rpcapi.get_console_output(context,
instance=instance, tail_length=tail_length)
def lock(self, context, instance):
def lock(self, context, instance, reason=None):
"""Lock the given instance."""
# Only update the lock if we are an admin (non-owner)
is_owner = instance.project_id == context.project_id
@ -4035,13 +4035,15 @@ class API(base.Base):
instance_actions.LOCK)
@wrap_instance_event(prefix='api')
def lock(self, context, instance):
def lock(self, context, instance, reason=None):
LOG.debug('Locking', instance=instance)
instance.locked = True
instance.locked_by = 'owner' if is_owner else 'admin'
if reason:
instance.system_metadata['locked_reason'] = reason
instance.save()
lock(self, context, instance)
lock(self, context, instance, reason=reason)
compute_utils.notify_about_instance_action(
context, instance, CONF.host,
action=fields_obj.NotificationAction.LOCK,
@ -4066,6 +4068,7 @@ class API(base.Base):
LOG.debug('Unlocking', instance=instance)
instance.locked = False
instance.locked_by = None
instance.system_metadata.pop('locked_reason', None)
instance.save()
unlock(self, context, instance)

View File

@ -2036,7 +2036,7 @@ def instance_get_all_by_filters_sort(context, filters, limit=None, marker=None,
| ['project_id', 'user_id', 'image_ref',
| 'vm_state', 'instance_type_id', 'uuid',
| 'metadata', 'host', 'system_metadata']
| 'metadata', 'host', 'system_metadata', 'locked']
A third type of filter (also using exact matching), filters
@ -2204,7 +2204,7 @@ def instance_get_all_by_filters_sort(context, filters, limit=None, marker=None,
exact_match_filter_names = ['project_id', 'user_id', 'image_ref',
'vm_state', 'instance_type_id', 'uuid',
'metadata', 'host', 'task_state',
'system_metadata']
'system_metadata', 'locked']
# Filter the query
query_prefix = _exact_instance_filter(query_prefix,

View File

@ -68,7 +68,8 @@ class InstancePayload(base.NotificationPayloadBase):
# Version 1.6: Add request_id field
# Version 1.7: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.7'
# Version 1.8: Added locked_reason field
VERSION = '1.8'
fields = {
'uuid': fields.UUIDField(),
'user_id': fields.StringField(nullable=True),
@ -113,6 +114,7 @@ class InstancePayload(base.NotificationPayloadBase):
'request_id': fields.StringField(nullable=True),
'action_initiator_user': fields.StringField(nullable=True),
'action_initiator_project': fields.StringField(nullable=True),
'locked_reason': fields.StringField(nullable=True),
}
def __init__(self, context, instance, bdms=None):
@ -132,6 +134,7 @@ class InstancePayload(base.NotificationPayloadBase):
context.user_id) else None
self.action_initiator_user = context.user_id
self.action_initiator_project = context.project_id
self.locked_reason = instance.system_metadata.get("locked_reason")
self.populate_schema(instance=instance)
@ -147,7 +150,8 @@ class InstanceActionPayload(InstancePayload):
# Version 1.6: Added request_id field to InstancePayload
# Version 1.7: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.7'
# Version 1.8: Added locked_reason field to InstancePayload
VERSION = '1.8'
fields = {
'fault': fields.ObjectField('ExceptionPayload', nullable=True),
'request_id': fields.StringField(nullable=True),
@ -169,7 +173,8 @@ class InstanceActionVolumePayload(InstanceActionPayload):
# Version 1.4: Added request_id field to InstancePayload
# Version 1.5: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.5'
# Version 1.6: Added locked_reason field to InstancePayload
VERSION = '1.6'
fields = {
'volume_id': fields.UUIDField()
}
@ -194,7 +199,8 @@ class InstanceActionVolumeSwapPayload(InstanceActionPayload):
# Version 1.6: Added request_id field to InstancePayload
# Version 1.7: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.7'
# Version 1.8: Added locked_reason field to InstancePayload
VERSION = '1.8'
fields = {
'old_volume_id': fields.UUIDField(),
'new_volume_id': fields.UUIDField(),
@ -229,7 +235,8 @@ class InstanceCreatePayload(InstanceActionPayload):
# 1.10: Added action_initiator_user and action_initiator_project to
# InstancePayload
# 1.11: Added instance_name to InstanceCreatePayload
VERSION = '1.11'
# Version 1.12: Added locked_reason field to InstancePayload
VERSION = '1.12'
fields = {
'keypairs': fields.ListOfObjectsField('KeypairPayload'),
'tags': fields.ListOfStringsField(),
@ -262,7 +269,8 @@ class InstanceActionResizePrepPayload(InstanceActionPayload):
# Version 1.1: Added request_id field to InstancePayload
# Version 1.2: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.2'
# Version 1.3: Added locked_reason field to InstancePayload
VERSION = '1.3'
fields = {
'new_flavor': fields.ObjectField('FlavorPayload', nullable=True)
}
@ -287,7 +295,8 @@ class InstanceUpdatePayload(InstancePayload):
# Version 1.7: Added request_id field to InstancePayload
# Version 1.8: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.8'
# Version 1.9: Added locked_reason field to InstancePayload
VERSION = '1.9'
fields = {
'state_update': fields.ObjectField('InstanceStateUpdatePayload'),
'audit_period': fields.ObjectField('AuditPeriodPayload'),
@ -314,7 +323,8 @@ class InstanceActionRescuePayload(InstanceActionPayload):
# Version 1.1: Added request_id field to InstancePayload
# Version 1.2: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.2'
# Version 1.3: Added locked_reason field to InstancePayload
VERSION = '1.3'
fields = {
'rescue_image_ref': fields.UUIDField(nullable=True)
}
@ -338,7 +348,8 @@ class InstanceActionRebuildPayload(InstanceActionPayload):
# signal the change of nova_object.name.
# Version 1.8: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.8'
# Version 1.9: Added locked_reason field to InstancePayload
VERSION = '1.9'
fields = {
'trusted_image_certificates': fields.ListOfStringsField(
nullable=True)
@ -688,7 +699,8 @@ class InstanceActionSnapshotPayload(InstanceActionPayload):
# Version 1.7: Added request_id field to InstancePayload
# Version 1.8: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.8'
# Version 1.9: Added locked_reason field to InstancePayload
VERSION = '1.9'
fields = {
'snapshot_image_id': fields.UUIDField(),
}
@ -706,7 +718,8 @@ class InstanceExistsPayload(InstancePayload):
# Version 1.0: Initial version
# Version 1.1: Added action_initiator_user and action_initiator_project to
# InstancePayload
VERSION = '1.1'
# Version 1.2: Added locked_reason field to InstancePayload
VERSION = '1.2'
fields = {
'audit_period': fields.ObjectField('AuditPeriodPayload'),
'bandwidth': fields.ListOfObjectsField('BandwidthPayload'),

View File

@ -0,0 +1,3 @@
{
"lock": {"locked_reason": "I don't want to work"}
}

View File

@ -0,0 +1,3 @@
{
"lock": {"locked_reason": "I don't want to work"}
}

View File

@ -0,0 +1,64 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"adminPass": "seekr3t",
"created": "%(isotime)s",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"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"
}
],
"locked": false,
"locked_reason": null,
"metadata": {
"meta_var": "meta_val"
},
"name": "foobar",
"progress": 0,
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "%(isotime)s",
"user_data": "ZWNobyAiaGVsbG8gd29ybGQi",
"user_id": "fake"
}
}

View File

@ -0,0 +1,14 @@
{
"rebuild" : {
"accessIPv4" : "%(access_ip_v4)s",
"accessIPv6" : "%(access_ip_v6)s",
"OS-DCF:diskConfig": "AUTO",
"imageRef" : "%(uuid)s",
"name" : "%(name)s",
"adminPass" : "%(pass)s",
"metadata" : {
"meta_var" : "meta_val"
},
"user_data": "ZWNobyAiaGVsbG8gd29ybGQi"
}
}

View File

@ -0,0 +1,20 @@
{
"server" : {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"name" : "new-server-test",
"imageRef" : "%(image_id)s",
"flavorRef" : "1",
"OS-DCF:diskConfig": "AUTO",
"metadata" : {
"My Server Name" : "Apache1"
},
"security_groups": [
{
"name": "default"
}
],
"user_data" : "%(user_data)s",
"networks": "auto"
}
}

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,88 @@
{
"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": "%(reservation_id)s",
"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": "%(strtime)s",
"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": "%(isotime)s",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"host_status": "UP",
"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"
}
],
"locked": true,
"locked_reason": "I don't want to work",
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"os-extended-volumes:volumes_attached": [],
"progress": 0,
"security_groups": [
{
"name": "default"
}
],
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "%(isotime)s",
"user_id": "fake"
}
}

View File

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

View File

@ -0,0 +1,61 @@
{
"server": {
"OS-DCF:diskConfig": "AUTO",
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"created": "%(isotime)s",
"description": "Sample description",
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"id": "%(id)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/%(id)s",
"rel": "bookmark"
}
],
"locked": false,
"locked_reason": null,
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"server_groups": [],
"status": "ACTIVE",
"tags": [],
"tenant_id": "6f70656e737461636b20342065766572",
"trusted_image_certificates": null,
"updated": "%(isotime)s",
"user_id": "fake"
}
}

View File

@ -0,0 +1,89 @@
{
"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": "%(reservation_id)s",
"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": "%(strtime)s",
"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": "%(ip)s",
"version": 4
}
]
},
"config_drive": "",
"created": "%(isotime)s",
"description": null,
"flavor": {
"disk": 1,
"ephemeral": 0,
"extra_specs": {},
"original_name": "m1.tiny",
"ram": 512,
"swap": 0,
"vcpus": 1
},
"hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6",
"host_status": "UP",
"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"
}
],
"locked": true,
"locked_reason": "I don't want to work",
"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": null,
"updated": "%(isotime)s",
"user_id": "fake"
}
]
}

View File

@ -39,3 +39,38 @@ class LockServerSamplesJsonTest(test_servers.ServersSampleBase):
response = self._do_post('servers/%s/action' % self.uuid,
'unlock-server', {})
self.assertEqual(202, response.status_code)
class LockServerSamplesJsonTestV273(test_servers.ServersSampleBase):
sample_dir = "os-lock-server"
microversion = '2.73'
scenarios = [('v2_73', {'api_major_version': 'v2.1'})]
def setUp(self):
"""setUp Method for LockServer api samples extension
This method creates the server that will be used in each test
"""
super(LockServerSamplesJsonTestV273, self).setUp()
self.uuid = self._post_server()
def test_post_lock_server(self):
# backwards compatibility.
response = self._do_post('servers/%s/action' % self.uuid,
name='lock-server', subs={})
self.assertEqual(202, response.status_code)
def test_post_lock_server_with_reason(self):
# Get api samples to lock server request.
response = self._do_post('servers/%s/action' % self.uuid,
name='lock-server-with-reason', subs={})
self.assertEqual(202, response.status_code)
def test_post_unlock_server(self):
# Get api samples to unlock server request.
# We first call the previous test to lock the server with reason
# and then unlock it to post a response for unlock.
self.test_post_lock_server_with_reason()
response = self._do_post('servers/%s/action' % self.uuid,
name='unlock-server', subs={})
self.assertEqual(202, response.status_code)

View File

@ -515,6 +515,62 @@ class ServersSampleJson271Test(ServersSampleBase):
subs, response, 200)
class ServersSampleJson273Test(ServersSampleBase):
microversion = '2.73'
scenarios = [('v2_73', {'api_major_version': 'v2.1'})]
def setUp(self):
super(ServersSampleJson273Test, self).setUp()
def _post_server_and_lock(self):
uuid = self._post_server(use_common_server_api_samples=False)
reason = "I don't want to work"
self._do_post('servers/%s/action' % uuid,
'lock-server-with-reason',
{"locked_reason": reason})
return uuid
def test_servers_details_with_locked_reason(self):
uuid = self._post_server_and_lock()
response = self._do_get('servers/detail')
subs = {'id': uuid}
self._verify_response('servers-details-resp', subs, response, 200)
def test_server_get_with_locked_reason(self):
uuid = self._post_server_and_lock()
response = self._do_get('servers/%s' % uuid)
subs = {'id': uuid}
self._verify_response('server-get-resp', subs, response, 200)
def test_server_rebuild_with_empty_locked_reason(self):
uuid = self._post_server(use_common_server_api_samples=False)
image = fake.get_valid_image_id()
params = {
'uuid': image,
'name': 'foobar',
'pass': 'seekr3t',
'hostid': '[a-f0-9]+',
'access_ip_v4': '1.2.3.4',
'access_ip_v6': '80fe::',
}
resp = self._do_post('servers/%s/action' % uuid,
'server-action-rebuild', params)
subs = params.copy()
del subs['uuid']
self._verify_response('server-action-rebuild-resp', subs, resp, 202)
def test_update_server_with_empty_locked_reason(self):
uuid = self._post_server(use_common_server_api_samples=False)
subs = {}
subs['hostid'] = '[a-f0-9]+'
subs['access_ip_v4'] = '1.2.3.4'
subs['access_ip_v6'] = '80fe::'
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

@ -400,6 +400,7 @@ class TestInstanceNotificationSample(
self._test_interface_attach_and_detach,
self._test_interface_attach_error,
self._test_lock_unlock_instance,
self._test_lock_unlock_instance_with_reason,
]
for action in actions:
@ -1934,6 +1935,30 @@ class TestInstanceNotificationSample(
'uuid': server['id']},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[1])
def _test_lock_unlock_instance_with_reason(self, server):
self.api.post_server_action(
server['id'], {'lock': {"locked_reason": "global warming"}})
self._wait_for_server_parameter(self.api, server, {'locked': True})
self.api.post_server_action(server['id'], {'unlock': {}})
self._wait_for_server_parameter(self.api, server, {'locked': False})
# Two versioned notifications are generated
# 0. instance-lock
# 1. instance-unlock
self.assertEqual(2, len(fake_notifier.VERSIONED_NOTIFICATIONS))
self._verify_notification(
'instance-lock-with-reason',
replacements={
'reservation_id': server['reservation_id'],
'uuid': server['id']},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[0])
self._verify_notification(
'instance-unlock',
replacements={
'reservation_id': server['reservation_id'],
'uuid': server['id']},
actual=fake_notifier.VERSIONED_NOTIFICATIONS[1])
class TestInstanceNotificationSampleOldAttachFlow(
TestInstanceNotificationSample):

View File

@ -14,7 +14,9 @@
# under the License.
import mock
import six
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute import lock_server as lock_server_v21
from nova import context
@ -40,10 +42,15 @@ class LockServerTestsV21(admin_only_action_common.CommonTests):
lambda *a, **kw: self.controller)
def test_lock_unlock(self):
self._test_actions(['_lock', '_unlock'])
args_map = {'_lock': ((), {"reason": None})}
body_map = {'_lock': {"lock": None}}
self._test_actions(['_lock', '_unlock'], args_map=args_map,
body_map=body_map)
def test_lock_unlock_with_non_existed_instance(self):
self._test_actions_with_non_existed_instance(['_lock', '_unlock'])
body_map = {'_lock': {"lock": None}}
self._test_actions_with_non_existed_instance(['_lock', '_unlock'],
body_map=body_map)
def test_unlock_not_authorized(self):
instance = self._stub_instance_get()
@ -83,6 +90,84 @@ class LockServerTestsV21(admin_only_action_common.CommonTests):
self.controller._unlock(admin_req, instance.uuid, {'unlock': None})
mock_unlock.assert_called_once_with(admin_ctxt, instance)
@mock.patch.object(common, 'get_instance')
def test_unlock_with_any_body(self, get_instance_mock):
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
get_instance_mock.return_value = instance
# This will pass since there is no schema validation.
body = {'unlock': {'blah': 'blah'}}
with mock.patch.object(self.compute_api, 'unlock') as mock_lock:
self.controller._unlock(self.req, instance.uuid, body=body)
mock_lock.assert_called_once_with(
self.req.environ['nova.context'], instance)
@mock.patch.object(common, 'get_instance')
def test_lock_with_empty_dict_body_is_valid(self, get_instance_mock):
# Empty dict with no key in the body is allowed.
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
get_instance_mock.return_value = instance
body = {'lock': {}}
with mock.patch.object(self.compute_api, 'lock') as mock_lock:
self.controller._lock(self.req, instance.uuid, body=body)
mock_lock.assert_called_once_with(
self.req.environ['nova.context'], instance, reason=None)
class LockServerTestsV273(LockServerTestsV21):
def setUp(self):
super(LockServerTestsV273, self).setUp()
self.req.api_version_request = api_version_request.APIVersionRequest(
'2.73')
@mock.patch.object(common, 'get_instance')
def test_lock_with_reason_V273(self, get_instance_mock):
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
get_instance_mock.return_value = instance
reason = "I don't want to work"
body = {'lock': {"locked_reason": reason}}
with mock.patch.object(self.compute_api, 'lock') as mock_lock:
self.controller._lock(self.req, instance.uuid, body=body)
mock_lock.assert_called_once_with(
self.req.environ['nova.context'], instance, reason=reason)
def test_lock_with_reason_exceeding_255_chars(self):
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
reason = 's' * 256
body = {'lock': {"locked_reason": reason}}
exp = self.assertRaises(exception.ValidationError,
self.controller._lock, self.req, instance.uuid, body=body)
self.assertIn('is too long', six.text_type(exp))
def test_lock_with_reason_in_invalid_format(self):
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
reason = 256
body = {'lock': {"locked_reason": reason}}
exp = self.assertRaises(exception.ValidationError,
self.controller._lock, self.req, instance.uuid, body=body)
self.assertIn("256 is not of type 'string'", six.text_type(exp))
def test_lock_with_invalid_paramater(self):
# This will fail from 2.73 since we have a schema check that allows
# only locked_reason
instance = fake_instance.fake_instance_obj(
self.req.environ['nova.context'])
body = {'lock': {'blah': 'blah'}}
exp = self.assertRaises(exception.ValidationError,
self.controller._lock, self.req, instance.uuid, body=body)
self.assertIn("('blah' was unexpected)", six.text_type(exp))
class LockServerPolicyEnforcementV21(test.NoDBTestCase):
@ -138,7 +223,7 @@ class LockServerPolicyEnforcementV21(test.NoDBTestCase):
self.policy.set_rules({rule_name: "user_id:%(user_id)s"})
self.controller._lock(self.req, fakes.FAKE_UUID, body={'lock': {}})
lock_mock.assert_called_once_with(self.req.environ['nova.context'],
instance)
instance, reason=None)
def test_unlock_policy_failed(self):
rule_name = "os_compute_api:os-lock-server:unlock"

View File

@ -793,7 +793,7 @@ class ServersControllerTest(ControllerTest):
self.assertEqual(0, len(res_dict['servers']))
self.mock_get_all.assert_called_once_with(
req.environ['nova.context'],
expected_attrs=expected_attrs,
expected_attrs=sorted(expected_attrs),
limit=1000, marker=None,
search_opts={'deleted': False, 'project_id': 'fake'},
sort_dirs=['desc'], sort_keys=['created_at'],
@ -918,6 +918,15 @@ class ServersControllerTest(ControllerTest):
expected_attrs=mock.ANY, sort_keys=[], sort_dirs=[],
cell_down_support=False, all_tenants=False)
def test_get_servers_ignore_locked_sort_key(self):
# Prior to microversion 2.73 locked sort key is ignored.
req = self.req('/fake/servers?sort_key=locked&sort_dir=asc')
self.controller.detail(req)
self.mock_get_all.assert_called_once_with(
mock.ANY, search_opts=mock.ANY, limit=mock.ANY, marker=mock.ANY,
expected_attrs=mock.ANY, sort_keys=[], sort_dirs=[],
cell_down_support=False, all_tenants=False)
def test_get_servers_ignore_sort_key_only_one_dir(self):
req = self.req(
'/fake/servers?sort_key=user_id&sort_key=vcpus&sort_dir=asc')
@ -980,6 +989,30 @@ class ServersControllerTest(ControllerTest):
sort_dirs=['desc'], sort_keys=['created_at'],
cell_down_support=False, all_tenants=False)
def test_get_servers_with_locked_filter(self):
# Prior to microversion 2.73 locked filter parameter is ignored.
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
expected_attrs=None, sort_keys=None, sort_dirs=None,
cell_down_support=False, all_tenants=False):
db_list = [fakes.stub_instance(100, uuid=uuids.fake)]
return instance_obj._make_instance_list(
context, objects.InstanceList(), db_list, FIELDS)
self.mock_get_all.side_effect = fake_get_all
req = self.req('/fake/servers?locked=true')
servers = self.controller.index(req)['servers']
self.assertEqual(1, len(servers))
self.assertEqual(uuids.fake, servers[0]['id'])
self.mock_get_all.assert_called_once_with(
req.environ['nova.context'], expected_attrs=[],
limit=1000, marker=None,
search_opts={'deleted': False, 'project_id': 'fake'},
sort_dirs=['desc'], sort_keys=['created_at'],
cell_down_support=False, all_tenants=False)
def test_get_servers_allows_image(self):
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
@ -2514,6 +2547,107 @@ class ServersControllerTestV271(ControllerTest):
self.assertEqual(expect_sg, servers['server']['server_groups'])
class ServersControllerTestV273(ControllerTest):
"""Server Controller test for microversion 2.73
The intent here is simply to verify that when showing server details
after microversion 2.73 the response will also have the locked_reason
key for the servers.
"""
wsgi_api_version = '2.73'
def setUp(self):
super(ServersControllerTestV273, self).setUp()
def req(self, url, use_admin_context=False):
return fakes.HTTPRequest.blank(url,
use_admin_context=use_admin_context,
version=self.wsgi_api_version)
def test_get_servers_with_locked_filter(self):
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
expected_attrs=None, sort_keys=None, sort_dirs=None,
cell_down_support=False, all_tenants=False):
db_list = [fakes.stub_instance(
100, uuid=uuids.fake, locked_by='fake')]
return instance_obj._make_instance_list(
context, objects.InstanceList(), db_list, FIELDS)
self.mock_get_all.side_effect = fake_get_all
req = self.req('/fake/servers?locked=true')
servers = self.controller.index(req)['servers']
self.assertEqual(1, len(servers))
self.assertEqual(uuids.fake, servers[0]['id'])
search = {'deleted': False, 'project_id': 'fake', 'locked': True}
self.mock_get_all.assert_called_once_with(
req.environ['nova.context'], expected_attrs=[],
limit=1000, marker=None,
search_opts=search,
sort_dirs=['desc'], sort_keys=['created_at'],
cell_down_support=False, all_tenants=False)
def test_get_servers_with_locked_filter_invalid_value(self):
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
expected_attrs=None, sort_keys=None, sort_dirs=None,
cell_down_support=False, all_tenants=False):
db_list = [fakes.stub_instance(
100, uuid=uuids.fake, locked_by='fake')]
return instance_obj._make_instance_list(
context, objects.InstanceList(), db_list, FIELDS)
self.mock_get_all.side_effect = fake_get_all
req = self.req('/fake/servers?locked=price')
exp = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
self.assertIn("Unrecognized value 'price'", six.text_type(exp))
def test_get_servers_with_locked_filter_empty_value(self):
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
expected_attrs=None, sort_keys=None, sort_dirs=None,
cell_down_support=False, all_tenants=False):
db_list = [fakes.stub_instance(
100, uuid=uuids.fake, locked_by='fake')]
return instance_obj._make_instance_list(
context, objects.InstanceList(), db_list, FIELDS)
self.mock_get_all.side_effect = fake_get_all
req = self.req('/fake/servers?locked=')
exp = self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.index, req)
self.assertIn("Unrecognized value ''", six.text_type(exp))
def test_get_servers_with_locked_sort_key(self):
def fake_get_all(context, search_opts=None,
limit=None, marker=None,
expected_attrs=None, sort_keys=None, sort_dirs=None,
cell_down_support=False, all_tenants=False):
db_list = [fakes.stub_instance(
100, uuid=uuids.fake, locked_by='fake')]
return instance_obj._make_instance_list(
context, objects.InstanceList(), db_list, FIELDS)
self.mock_get_all.side_effect = fake_get_all
req = self.req('/fake/servers?sort_dir=desc&sort_key=locked')
servers = self.controller.index(req)['servers']
self.assertEqual(1, len(servers))
self.assertEqual(uuids.fake, servers[0]['id'])
self.mock_get_all.assert_called_once_with(
req.environ['nova.context'], expected_attrs=[],
limit=1000, marker=None,
search_opts={'deleted': False, 'project_id': 'fake'},
sort_dirs=['desc'], sort_keys=['locked'],
cell_down_support=False, all_tenants=False)
class ServersControllerDeleteTest(ControllerTest):
def setUp(self):

View File

@ -11257,6 +11257,28 @@ class ComputeAPITestCase(BaseTestCase):
self.context, instance, CONF.host, action='lock',
source='nova-api')
@mock.patch('nova.compute.utils.notify_about_instance_action')
@mock.patch('nova.context.RequestContext.elevated')
@mock.patch('nova.compute.api.API._record_action_start')
@mock.patch.object(compute_utils, 'EventReporter')
def test_lock_with_reason(self, mock_event, mock_record, mock_elevate,
mock_notify):
mock_elevate.return_value = self.context
instance = self._create_fake_instance_obj()
self.assertNotIn("locked_reason", instance.system_metadata)
self.compute_api.lock(self.context, instance, reason="blah")
self.assertEqual("blah", instance.system_metadata["locked_reason"])
mock_record.assert_called_once_with(
self.context, instance, instance_actions.LOCK
)
mock_event.assert_called_once_with(self.context,
'api_lock',
CONF.host,
instance.uuid)
mock_notify.assert_called_once_with(
self.context, instance, CONF.host, action='lock',
source='nova-api')
@mock.patch('nova.compute.utils.notify_about_instance_action')
@mock.patch('nova.context.RequestContext.elevated')
@mock.patch('nova.compute.api.API._record_action_start')
@ -11278,6 +11300,29 @@ class ComputeAPITestCase(BaseTestCase):
self.context, instance, CONF.host, action='unlock',
source='nova-api')
@mock.patch('nova.compute.utils.notify_about_instance_action')
@mock.patch('nova.context.RequestContext.elevated')
@mock.patch('nova.compute.api.API._record_action_start')
@mock.patch.object(compute_utils, 'EventReporter')
def test_unlock_with_reason(self, mock_event, mock_record, mock_elevate,
mock_notify):
mock_elevate.return_value = self.context
sm = {"locked_reason": "blah"}
instance = self._create_fake_instance_obj(
params={"system_metadata": sm})
self.compute_api.unlock(self.context, instance)
self.assertNotIn("locked_reason", instance.system_metadata)
mock_record.assert_called_once_with(
self.context, instance, instance_actions.UNLOCK
)
mock_event.assert_called_once_with(self.context,
'api_unlock',
CONF.host,
instance.uuid)
mock_notify.assert_called_once_with(
self.context, instance, CONF.host, action='unlock',
source='nova-api')
def test_add_remove_security_group(self):
instance = self._create_fake_instance_obj()

View File

@ -2236,6 +2236,19 @@ class InstanceTestCase(test.TestCase, ModelsObjectComparatorMixin):
{'host': 'host1'})
self._assertEqualListsOfInstances([instance], result)
def test_instance_get_all_by_filters_locked_key_true(self):
instance = self.create_instance_with_args(locked=True)
self.create_instance_with_args(locked=False)
result = db.instance_get_all_by_filters(self.ctxt,
{'locked': True})
self._assertEqualListsOfInstances([instance], result)
def test_instance_get_all_by_filters_locked_key_false(self):
self.create_instance_with_args(locked=True)
result = db.instance_get_all_by_filters(self.ctxt,
{'locked': False})
self._assertEqualListsOfInstances([], result)
def test_instance_get_all_by_filters_metadata(self):
instance = self.create_instance_with_args(metadata={'foo': 'bar'})
self.create_instance_with_args()

View File

@ -43,6 +43,7 @@ class TestInstanceNotification(test.NoDBTestCase):
uuid=uuids.instance1,
locked=False,
auto_disk_config=False,
system_metadata={},
**instance_values)
self.payload = {
'bandwidth': {},
@ -118,6 +119,7 @@ class TestInstanceNotification(test.NoDBTestCase):
instance = fake_instance.fake_instance_obj(ctxt)
# Set some other fields otherwise populate_schema tries to hit the DB.
instance.metadata = {}
instance.system_metadata = {}
instance.info_cache = objects.InstanceInfoCache(
network_info=network_model.NetworkInfo([]))
payload = instance_notification.InstancePayload(ctxt, instance)

View File

@ -383,35 +383,35 @@ notification_object_data = {
'ImageMetaPayload': '1.0-0e65beeacb3393beed564a57bc2bc989',
'ImageMetaPropsPayload': '1.0-0665065e198b4ab1b03aa80f442d2302',
'InstanceActionNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionPayload': '1.7-8c77f0c85a83d325fded152376ca809a',
'InstanceActionPayload': '1.8-4fa3da9cbf0761f1f700ae578f36dc2f',
'InstanceActionRebuildNotification':
'1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionRebuildPayload': '1.8-ab76ecbf73b82bc010ab82bdc2792e1d',
'InstanceActionRebuildPayload': '1.9-10eebfbf6e944aaac43188173dff9e01',
'InstanceActionRescueNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionRescuePayload': '1.2-b82aa24a966713dce26de3126716e8ef',
'InstanceActionRescuePayload': '1.3-dbf4de42bc02ebc4cdbe42f90d343bfd',
'InstanceActionResizePrepNotification':
'1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionResizePrepPayload': '1.2-1b41bec00f2b679e77a906b1df0c1d5a',
'InstanceActionResizePrepPayload': '1.3-baca73cc450f72d4e1ce6b9aca2bbdf6',
'InstanceActionVolumeNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionVolumePayload': '1.5-3027aae42ee85155b2c378fad1f3b678',
'InstanceActionVolumePayload': '1.6-0a30e870677e6166c50645623e287f78',
'InstanceActionVolumeSwapNotification':
'1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionVolumeSwapPayload': '1.7-d3252403a9437bcdc80f1075214f8b45',
'InstanceActionVolumeSwapPayload': '1.8-d2255347cb2353cb12c174aad4dab93c',
'InstanceCreateNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceCreatePayload': '1.11-7513127966bc8f270946634d099e71c0',
'InstancePayload': '1.7-78354572f699b9a6ad9996b199d03375',
'InstanceCreatePayload': '1.12-749f2da7c2435a0e55c076d6bf0ea81d',
'InstancePayload': '1.8-60d62df5a6b6aa7817ec5d09f4b8a3e5',
'InstanceActionSnapshotNotification':
'1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceActionSnapshotPayload': '1.8-6a3a66f823b56268ea4b759c83e38c31',
'InstanceActionSnapshotPayload': '1.9-c3e0bbaaefafdfa2f8e6e504c2c9b12c',
'InstanceExistsNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceExistsPayload': '1.1-b7095abb18f5b75f39dc1aa59942535d',
'InstanceExistsPayload': '1.2-e082c02438ee57164829afaeee3bf7f8',
'InstanceNUMACellPayload': '1.0-2f13614648bc46f2e29578a206561ef6',
'InstanceNUMATopologyPayload': '1.0-247361b152047c18ae9ad1da2544a3c9',
'InstancePCIRequestPayload': '1.0-12d0d61baf183daaafd93cbeeed2956f',
'InstancePCIRequestsPayload': '1.0-6751cffe0c0fabd212aad624f672429a',
'InstanceStateUpdatePayload': '1.0-07e111c0fa0f6db0f79b0726d593e3da',
'InstanceUpdateNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'InstanceUpdatePayload': '1.8-375131acb12e612a460f68211a2b3a35',
'InstanceUpdatePayload': '1.9-0295e45efc2c6ba98fbca77bbddf882d',
'IpPayload': '1.0-8ecf567a99e516d4af094439a7632d34',
'KeypairNotification': '1.0-a73147b93b520ff0061865849d3dfa56',
'KeypairPayload': '1.0-6daebbbde0e1bf35c1556b1ecd9385c1',

View File

@ -0,0 +1,18 @@
---
features:
- |
Added a new ``locked_reason`` option in microversion 2.73 to the
``POST /servers/{server_id}/action`` request where the action is lock.
It enables the user to specify a reason when locking a server. This
information will be exposed through the response of the following APIs:
- ``GET servers/{server_id}``
- ``GET /servers/detail``
- ``POST /servers/{server_id}/action`` where the action is rebuild
- ``PUT servers/{server_id}``
In addition, ``locked`` will be supported as a valid filter/sort parameter
for ``GET /servers/detail`` and ``GET /servers`` so that users can filter
servers based on their locked value. Also the instance action versioned
notifications for the lock/unlock actions now contain the ``locked_reason``
field.