diff --git a/api-guide/source/server_concepts.rst b/api-guide/source/server_concepts.rst index b490fc375c90..95f1e8f397b6 100644 --- a/api-guide/source/server_concepts.rst +++ b/api-guide/source/server_concepts.rst @@ -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. diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index ee533eb6343d..047ef5c6ed7b 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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. diff --git a/api-ref/source/servers-actions.inc b/api-ref/source/servers-actions.inc index 73335a5951c3..22871e6ed615 100644 --- a/api-ref/source/servers-actions.inc +++ b/api-ref/source/servers-actions.inc @@ -383,15 +383,16 @@ See the "Lock, Unlock" item in `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 `_ 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 `_ 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) diff --git a/api-ref/source/servers.inc b/api-ref/source/servers.inc index f80ad5991285..14d3b53351eb 100644 --- a/api-ref/source/servers.inc +++ b/api-ref/source/servers.inc @@ -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 diff --git a/doc/api_samples/os-lock-server/v2.73/lock-server-with-reason.json b/doc/api_samples/os-lock-server/v2.73/lock-server-with-reason.json new file mode 100644 index 000000000000..c307fb39bf76 --- /dev/null +++ b/doc/api_samples/os-lock-server/v2.73/lock-server-with-reason.json @@ -0,0 +1,3 @@ +{ + "lock": {"locked_reason": "I don't want to work"} +} \ No newline at end of file diff --git a/doc/api_samples/os-lock-server/v2.73/lock-server.json b/doc/api_samples/os-lock-server/v2.73/lock-server.json new file mode 100644 index 000000000000..d7e96964ef23 --- /dev/null +++ b/doc/api_samples/os-lock-server/v2.73/lock-server.json @@ -0,0 +1,3 @@ +{ + "lock": null +} \ No newline at end of file diff --git a/doc/api_samples/os-lock-server/v2.73/unlock-server.json b/doc/api_samples/os-lock-server/v2.73/unlock-server.json new file mode 100644 index 000000000000..0eba7e725291 --- /dev/null +++ b/doc/api_samples/os-lock-server/v2.73/unlock-server.json @@ -0,0 +1,3 @@ +{ + "unlock": null +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/lock-server-with-reason.json b/doc/api_samples/servers/v2.73/lock-server-with-reason.json new file mode 100644 index 000000000000..c307fb39bf76 --- /dev/null +++ b/doc/api_samples/servers/v2.73/lock-server-with-reason.json @@ -0,0 +1,3 @@ +{ + "lock": {"locked_reason": "I don't want to work"} +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-action-rebuild-resp.json b/doc/api_samples/servers/v2.73/server-action-rebuild-resp.json new file mode 100644 index 000000000000..1234595ac549 --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-action-rebuild-resp.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-action-rebuild.json b/doc/api_samples/servers/v2.73/server-action-rebuild.json new file mode 100644 index 000000000000..f1431a050626 --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-action-rebuild.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-create-req.json b/doc/api_samples/servers/v2.73/server-create-req.json new file mode 100644 index 000000000000..c8ae2eac9748 --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-create-req.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-create-resp.json b/doc/api_samples/servers/v2.73/server-create-resp.json new file mode 100644 index 000000000000..d5ff5974d9cf --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-create-resp.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-get-resp.json b/doc/api_samples/servers/v2.73/server-get-resp.json new file mode 100644 index 000000000000..c2b7a3c84aab --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-get-resp.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-update-req.json b/doc/api_samples/servers/v2.73/server-update-req.json new file mode 100644 index 000000000000..3b3995d51e42 --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-update-req.json @@ -0,0 +1,9 @@ +{ + "server": { + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "OS-DCF:diskConfig": "AUTO", + "name": "new-server-test", + "description": "Sample description" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/server-update-resp.json b/doc/api_samples/servers/v2.73/server-update-resp.json new file mode 100644 index 000000000000..73c43120bb3e --- /dev/null +++ b/doc/api_samples/servers/v2.73/server-update-resp.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.73/servers-details-resp.json b/doc/api_samples/servers/v2.73/servers-details-resp.json new file mode 100644 index 000000000000..f82f8be54d7a --- /dev/null +++ b/doc/api_samples/servers/v2.73/servers-details-resp.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 4dc32b092a0a..34ed8b080925 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.72", + "version": "2.73", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 57ec43a55d63..f86347de062e 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.72", + "version": "2.73", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/notification_samples/common_payloads/InstanceActionPayload.json b/doc/notification_samples/common_payloads/InstanceActionPayload.json index c0886a65939e..4906f1428ed5 100644 --- a/doc/notification_samples/common_payloads/InstanceActionPayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionPayload.json @@ -5,5 +5,5 @@ }, "nova_object.name":"InstanceActionPayload", "nova_object.namespace":"nova", - "nova_object.version":"1.7" + "nova_object.version":"1.8" } diff --git a/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json b/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json index 35cf2646478b..2d05adadedfd 100644 --- a/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionRebuildPayload.json @@ -9,5 +9,5 @@ ] }, "nova_object.name": "InstanceActionRebuildPayload", - "nova_object.version": "1.8" + "nova_object.version": "1.9" } diff --git a/doc/notification_samples/common_payloads/InstanceActionRescuePayload.json b/doc/notification_samples/common_payloads/InstanceActionRescuePayload.json index e70dd84b9f86..697037222561 100644 --- a/doc/notification_samples/common_payloads/InstanceActionRescuePayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionRescuePayload.json @@ -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" } diff --git a/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json b/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json index cf7146b125b5..9c32576d69ee 100644 --- a/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionResizePrepPayload.json @@ -27,5 +27,5 @@ "task_state": "resize_prep" }, "nova_object.name": "InstanceActionResizePrepPayload", - "nova_object.version": "1.2" + "nova_object.version": "1.3" } diff --git a/doc/notification_samples/common_payloads/InstanceActionSnapshotPayload.json b/doc/notification_samples/common_payloads/InstanceActionSnapshotPayload.json index 371e1de3ae09..d0dd7b7f5b50 100644 --- a/doc/notification_samples/common_payloads/InstanceActionSnapshotPayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionSnapshotPayload.json @@ -5,5 +5,5 @@ }, "nova_object.name":"InstanceActionSnapshotPayload", "nova_object.namespace":"nova", - "nova_object.version":"1.8" + "nova_object.version":"1.9" } diff --git a/doc/notification_samples/common_payloads/InstanceActionVolumePayload.json b/doc/notification_samples/common_payloads/InstanceActionVolumePayload.json index 289fd3218edf..50108e821578 100644 --- a/doc/notification_samples/common_payloads/InstanceActionVolumePayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionVolumePayload.json @@ -5,5 +5,5 @@ }, "nova_object.name": "InstanceActionVolumePayload", "nova_object.namespace": "nova", - "nova_object.version": "1.5" + "nova_object.version": "1.6" } \ No newline at end of file diff --git a/doc/notification_samples/common_payloads/InstanceActionVolumeSwapPayload.json b/doc/notification_samples/common_payloads/InstanceActionVolumeSwapPayload.json index e0445b4375b2..ac56306a7422 100644 --- a/doc/notification_samples/common_payloads/InstanceActionVolumeSwapPayload.json +++ b/doc/notification_samples/common_payloads/InstanceActionVolumeSwapPayload.json @@ -6,5 +6,5 @@ }, "nova_object.name": "InstanceActionVolumeSwapPayload", "nova_object.namespace": "nova", - "nova_object.version": "1.7" + "nova_object.version": "1.8" } diff --git a/doc/notification_samples/common_payloads/InstanceCreatePayload.json b/doc/notification_samples/common_payloads/InstanceCreatePayload.json index f7c59362bde3..c7e6adc981a0 100644 --- a/doc/notification_samples/common_payloads/InstanceCreatePayload.json +++ b/doc/notification_samples/common_payloads/InstanceCreatePayload.json @@ -24,5 +24,5 @@ "instance_name": "instance-00000001" }, "nova_object.name":"InstanceCreatePayload", - "nova_object.version": "1.11" + "nova_object.version": "1.12" } diff --git a/doc/notification_samples/common_payloads/InstanceExistsPayload.json b/doc/notification_samples/common_payloads/InstanceExistsPayload.json index 735a8ce206fc..f054b824de3e 100644 --- a/doc/notification_samples/common_payloads/InstanceExistsPayload.json +++ b/doc/notification_samples/common_payloads/InstanceExistsPayload.json @@ -8,5 +8,5 @@ }, "nova_object.name":"InstanceExistsPayload", "nova_object.namespace":"nova", - "nova_object.version":"1.1" + "nova_object.version":"1.2" } diff --git a/doc/notification_samples/common_payloads/InstancePayload.json b/doc/notification_samples/common_payloads/InstancePayload.json index f92c6b43b087..9053ba76d267 100644 --- a/doc/notification_samples/common_payloads/InstancePayload.json +++ b/doc/notification_samples/common_payloads/InstancePayload.json @@ -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" } diff --git a/doc/notification_samples/common_payloads/InstanceUpdatePayload.json b/doc/notification_samples/common_payloads/InstanceUpdatePayload.json index 39072fa1b74a..2c1c63d7a659 100644 --- a/doc/notification_samples/common_payloads/InstanceUpdatePayload.json +++ b/doc/notification_samples/common_payloads/InstanceUpdatePayload.json @@ -29,5 +29,5 @@ }, "nova_object.name": "InstanceUpdatePayload", "nova_object.namespace": "nova", - "nova_object.version": "1.8" + "nova_object.version": "1.9" } \ No newline at end of file diff --git a/doc/notification_samples/instance-lock-with-reason.json b/doc/notification_samples/instance-lock-with-reason.json new file mode 100644 index 000000000000..45a6847f54fd --- /dev/null +++ b/doc/notification_samples/instance-lock-with-reason.json @@ -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" +} diff --git a/doc/notification_samples/instance-lock.json b/doc/notification_samples/instance-lock.json index d542d9473600..568f68b99fed 100644 --- a/doc/notification_samples/instance-lock.json +++ b/doc/notification_samples/instance-lock.json @@ -3,7 +3,8 @@ "payload":{ "$ref": "common_payloads/InstanceActionPayload.json#", "nova_object.data":{ - "locked":true + "locked":true, + "locked_reason": null } }, "priority":"INFO", diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 926782ed67a2..ab22995075cd 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index fd226130fbfb..770dcfec1fb8 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -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 diff --git a/nova/api/openstack/compute/lock_server.py b/nova/api/openstack/compute/lock_server.py index 39d48d1d3cb2..a81a06e19615 100644 --- a/nova/api/openstack/compute/lock_server.py +++ b/nova/api/openstack/compute/lock_server.py @@ -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) diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 5d4763d7ff3f..112a3ee061b5 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -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``. diff --git a/nova/api/openstack/compute/schemas/lock_server.py b/nova/api/openstack/compute/schemas/lock_server.py new file mode 100644 index 000000000000..8d3db82a9f7a --- /dev/null +++ b/nova/api/openstack/compute/schemas/lock_server.py @@ -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, +} diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 374f38c2c8e8..1c71cf688957 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -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, +}) diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index a85fc50f17e2..b5fbed2ab6e2 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -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): diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 7f4ba1205193..1e5f96fb2bb5 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -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") diff --git a/nova/compute/api.py b/nova/compute/api.py index beb2544e3591..70f4678502a9 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 1a7deeb26117..503fb4835564 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -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, diff --git a/nova/notifications/objects/instance.py b/nova/notifications/objects/instance.py index 9488473c68cc..1ec331146764 100644 --- a/nova/notifications/objects/instance.py +++ b/nova/notifications/objects/instance.py @@ -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'), diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server-with-reason.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server-with-reason.json.tpl new file mode 100644 index 000000000000..4c0c0cc15e22 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server-with-reason.json.tpl @@ -0,0 +1,3 @@ +{ + "lock": {"locked_reason": "I don't want to work"} +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server.json.tpl new file mode 100644 index 000000000000..a1863f2f3914 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/lock-server.json.tpl @@ -0,0 +1,3 @@ +{ + "lock": null +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/unlock-server.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/unlock-server.json.tpl new file mode 100644 index 000000000000..9e905ca2b99e --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-lock-server/v2.73/unlock-server.json.tpl @@ -0,0 +1,3 @@ +{ + "unlock": null +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/lock-server-with-reason.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/lock-server-with-reason.json.tpl new file mode 100644 index 000000000000..4c0c0cc15e22 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/lock-server-with-reason.json.tpl @@ -0,0 +1,3 @@ +{ + "lock": {"locked_reason": "I don't want to work"} +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild-resp.json.tpl new file mode 100644 index 000000000000..b4570476f849 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild-resp.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild.json.tpl new file mode 100644 index 000000000000..5b61faeed8e4 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-action-rebuild.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-req.json.tpl new file mode 100644 index 000000000000..e7c112c7e3c9 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-req.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-resp.json.tpl new file mode 100644 index 000000000000..20265ed88963 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-create-resp.json.tpl @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(compute_endpoint)s/servers/%(uuid)s", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-get-resp.json.tpl new file mode 100644 index 000000000000..765a6d087742 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-get-resp.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-req.json.tpl new file mode 100644 index 000000000000..f1f436642fed --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-req.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-resp.json.tpl new file mode 100644 index 000000000000..f9fe440d851c --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/server-update-resp.json.tpl @@ -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" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/servers-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/servers-details-resp.json.tpl new file mode 100644 index 000000000000..44d04a2f4b42 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.73/servers-details-resp.json.tpl @@ -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" + } + ] +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/test_lock_server.py b/nova/tests/functional/api_sample_tests/test_lock_server.py index 69e806b42715..ffcf3b53a6c4 100644 --- a/nova/tests/functional/api_sample_tests/test_lock_server.py +++ b/nova/tests/functional/api_sample_tests/test_lock_server.py @@ -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) diff --git a/nova/tests/functional/api_sample_tests/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index 795c67a36013..749a4f777e4b 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -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): diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index 594f3d3058ec..cf9d68f92319 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -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): diff --git a/nova/tests/unit/api/openstack/compute/test_lock_server.py b/nova/tests/unit/api/openstack/compute/test_lock_server.py index b7b70de1622c..24d15fea74fc 100644 --- a/nova/tests/unit/api/openstack/compute/test_lock_server.py +++ b/nova/tests/unit/api/openstack/compute/test_lock_server.py @@ -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" diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 34dd07d7d9fb..b738ea451b61 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -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): diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index 933a7f4d11f8..f20709ddb312 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -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() diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index 9d4b0e0f0e2c..b9bff4e88456 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -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() diff --git a/nova/tests/unit/notifications/objects/test_instance.py b/nova/tests/unit/notifications/objects/test_instance.py index e70a9a5694c9..c2b731558735 100644 --- a/nova/tests/unit/notifications/objects/test_instance.py +++ b/nova/tests/unit/notifications/objects/test_instance.py @@ -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) diff --git a/nova/tests/unit/notifications/objects/test_notification.py b/nova/tests/unit/notifications/objects/test_notification.py index 70c76f9bd98d..44652495d394 100644 --- a/nova/tests/unit/notifications/objects/test_notification.py +++ b/nova/tests/unit/notifications/objects/test_notification.py @@ -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', diff --git a/releasenotes/notes/bp-add-locked-reason-fb757750f7f077ef.yaml b/releasenotes/notes/bp-add-locked-reason-fb757750f7f077ef.yaml new file mode 100644 index 000000000000..3c780a4138e5 --- /dev/null +++ b/releasenotes/notes/bp-add-locked-reason-fb757750f7f077ef.yaml @@ -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. \ No newline at end of file