From b967f2a693b372c6d85e0933b4829bb48e692c3a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 26 Mar 2024 22:02:33 +0000 Subject: [PATCH] api: Add response body schemas for remaining server action APIs This demonstrates far more complex response schemas, including the response to the rebuild action which is effectively the response to the server show API. Change-Id: I6dc355f3c3f164d0bc7887a58e8b13979f0b476e Signed-off-by: Stephen Finucane --- .../flavor-access-add-tenant-resp.json | 2 +- .../flavor-access-list-resp.json | 2 +- doc/notification_samples/flavor-update.json | 2 +- nova/api/openstack/compute/console_output.py | 5 +- nova/api/openstack/compute/create_backup.py | 12 +- nova/api/openstack/compute/evacuate.py | 10 +- nova/api/openstack/compute/flavor_access.py | 2 + nova/api/openstack/compute/remote_consoles.py | 17 +- .../compute/schemas/console_output.py | 9 + .../compute/schemas/create_backup.py | 18 +- .../api/openstack/compute/schemas/evacuate.py | 25 +- .../compute/schemas/flavor_access.py | 27 +- .../compute/schemas/remote_consoles.py | 75 +++ nova/api/openstack/compute/schemas/servers.py | 523 ++++++++++++++++-- nova/api/openstack/compute/servers.py | 26 + .../flavor-access-list-resp.json.tpl | 2 +- .../api_sample_tests/test_flavor_access.py | 9 +- .../notification_sample_tests/test_flavor.py | 2 +- .../openstack/compute/test_create_backup.py | 28 +- .../openstack/compute/test_flavor_access.py | 65 +-- .../api/openstack/compute/test_keypairs.py | 6 +- .../openstack/compute/test_server_actions.py | 25 +- .../api/openstack/compute/test_servers.py | 139 +++-- nova/tests/unit/api/openstack/fakes.py | 22 +- nova/tests/unit/policies/test_servers.py | 21 +- 25 files changed, 900 insertions(+), 174 deletions(-) diff --git a/doc/api_samples/flavor-access/flavor-access-add-tenant-resp.json b/doc/api_samples/flavor-access/flavor-access-add-tenant-resp.json index b6c1bc77df..1561b48e78 100644 --- a/doc/api_samples/flavor-access/flavor-access-add-tenant-resp.json +++ b/doc/api_samples/flavor-access/flavor-access-add-tenant-resp.json @@ -2,7 +2,7 @@ "flavor_access": [ { "flavor_id": "10", - "tenant_id": "fake_tenant" + "tenant_id": "6f70656e737461636b20342065766572" } ] } \ No newline at end of file diff --git a/doc/api_samples/flavor-access/flavor-access-list-resp.json b/doc/api_samples/flavor-access/flavor-access-list-resp.json index b6c1bc77df..1561b48e78 100644 --- a/doc/api_samples/flavor-access/flavor-access-list-resp.json +++ b/doc/api_samples/flavor-access/flavor-access-list-resp.json @@ -2,7 +2,7 @@ "flavor_access": [ { "flavor_id": "10", - "tenant_id": "fake_tenant" + "tenant_id": "6f70656e737461636b20342065766572" } ] } \ No newline at end of file diff --git a/doc/notification_samples/flavor-update.json b/doc/notification_samples/flavor-update.json index 9b2a719f5f..6ed5663ef5 100644 --- a/doc/notification_samples/flavor-update.json +++ b/doc/notification_samples/flavor-update.json @@ -13,7 +13,7 @@ "extra_specs": { "hw:numa_nodes": "2" }, - "projects": ["fake_tenant"], + "projects": ["6f70656e737461636b20342065766572"], "swap": 0, "rxtx_factor": 2.0, "is_public": false, diff --git a/nova/api/openstack/compute/console_output.py b/nova/api/openstack/compute/console_output.py index 75727fb2f8..f037ba7198 100644 --- a/nova/api/openstack/compute/console_output.py +++ b/nova/api/openstack/compute/console_output.py @@ -19,7 +19,7 @@ import re import webob from nova.api.openstack import common -from nova.api.openstack.compute.schemas import console_output +from nova.api.openstack.compute.schemas import console_output as schema from nova.api.openstack import wsgi from nova.api import validation from nova.compute import api as compute @@ -34,7 +34,8 @@ class ConsoleOutputController(wsgi.Controller): @wsgi.expected_errors((404, 409, 501)) @wsgi.action('os-getConsoleOutput') - @validation.schema(console_output.get_console_output) + @validation.schema(schema.get_console_output) + @validation.response_body_schema(schema.get_console_output_response) def get_console_output(self, req, id, body): """Get text console output.""" context = req.environ['nova.context'] diff --git a/nova/api/openstack/compute/create_backup.py b/nova/api/openstack/compute/create_backup.py index 43b4114b98..45b8763907 100644 --- a/nova/api/openstack/compute/create_backup.py +++ b/nova/api/openstack/compute/create_backup.py @@ -17,7 +17,7 @@ import webob from nova.api.openstack import api_version_request from nova.api.openstack import common -from nova.api.openstack.compute.schemas import create_backup +from nova.api.openstack.compute.schemas import create_backup as schema from nova.api.openstack import wsgi from nova.api import validation from nova.compute import api as compute @@ -33,8 +33,14 @@ class CreateBackupController(wsgi.Controller): @wsgi.response(202) @wsgi.expected_errors((400, 403, 404, 409)) @wsgi.action('createBackup') - @validation.schema(create_backup.create_backup_v20, '2.0', '2.0') - @validation.schema(create_backup.create_backup, '2.1') + @validation.schema(schema.create_backup_v20, '2.0', '2.0') + @validation.schema(schema.create_backup, '2.1') + @validation.response_body_schema( + schema.create_backup_response, '2.1', '2.44', + ) + @validation.response_body_schema( + schema.create_backup_response_v245, '2.45' + ) def _create_backup(self, req, id, body): """Backup a server instance. diff --git a/nova/api/openstack/compute/evacuate.py b/nova/api/openstack/compute/evacuate.py index 479f359570..3f86c10544 100644 --- a/nova/api/openstack/compute/evacuate.py +++ b/nova/api/openstack/compute/evacuate.py @@ -80,9 +80,13 @@ class EvacuateController(wsgi.Controller): @wsgi.action('evacuate') @validation.schema(evacuate.evacuate, "2.0", "2.13") @validation.schema(evacuate.evacuate_v214, "2.14", "2.28") - @validation.schema(evacuate.evacuate_v2_29, "2.29", "2.67") - @validation.schema(evacuate.evacuate_v2_68, "2.68", "2.94") - @validation.schema(evacuate.evacuate_v2_95, "2.95") + @validation.schema(evacuate.evacuate_v229, "2.29", "2.67") + @validation.schema(evacuate.evacuate_v268, "2.68", "2.94") + @validation.schema(evacuate.evacuate_v295, "2.95") + @validation.response_body_schema( + evacuate.evacuate_response, "2.0", "2.13" + ) + @validation.response_body_schema(evacuate.evacuate_response_v214, "2.14") def _evacuate(self, req, id, body): """Permit admins to evacuate a server from a failed host to a new one. diff --git a/nova/api/openstack/compute/flavor_access.py b/nova/api/openstack/compute/flavor_access.py index 15bc5dc417..56c805b8d2 100644 --- a/nova/api/openstack/compute/flavor_access.py +++ b/nova/api/openstack/compute/flavor_access.py @@ -63,6 +63,7 @@ class FlavorActionController(wsgi.Controller): @wsgi.expected_errors((400, 403, 404, 409)) @wsgi.action("addTenantAccess") @validation.schema(schema.add_tenant_access) + @validation.response_body_schema(schema.add_tenant_access_response) def _add_tenant_access(self, req, id, body): context = req.environ['nova.context'] context.can(fa_policies.POLICY_ROOT % "add_tenant_access", target={}) @@ -88,6 +89,7 @@ class FlavorActionController(wsgi.Controller): @wsgi.expected_errors((400, 403, 404)) @wsgi.action("removeTenantAccess") @validation.schema(schema.remove_tenant_access) + @validation.response_body_schema(schema.remove_tenant_access_response) def _remove_tenant_access(self, req, id, body): context = req.environ['nova.context'] context.can( diff --git a/nova/api/openstack/compute/remote_consoles.py b/nova/api/openstack/compute/remote_consoles.py index 08f87caa96..408b11836a 100644 --- a/nova/api/openstack/compute/remote_consoles.py +++ b/nova/api/openstack/compute/remote_consoles.py @@ -15,7 +15,7 @@ import webob from nova.api.openstack import common -from nova.api.openstack.compute.schemas import remote_consoles +from nova.api.openstack.compute.schemas import remote_consoles as schema from nova.api.openstack import wsgi from nova.api import validation from nova.compute import api as compute @@ -40,7 +40,8 @@ class RemoteConsolesController(wsgi.Controller): @wsgi.Controller.api_version("2.1", "2.5") @wsgi.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getVNCConsole') - @validation.schema(remote_consoles.get_vnc_console) + @validation.schema(schema.get_vnc_console) + @validation.response_body_schema(schema.get_vnc_console_response) def get_vnc_console(self, req, id, body): """Get text console output.""" context = req.environ['nova.context'] @@ -71,7 +72,8 @@ class RemoteConsolesController(wsgi.Controller): @wsgi.Controller.api_version("2.1", "2.5") @wsgi.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getSPICEConsole') - @validation.schema(remote_consoles.get_spice_console) + @validation.schema(schema.get_spice_console) + @validation.response_body_schema(schema.get_spice_console_response) def get_spice_console(self, req, id, body): """Get text console output.""" context = req.environ['nova.context'] @@ -100,7 +102,7 @@ class RemoteConsolesController(wsgi.Controller): @wsgi.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getRDPConsole') @wsgi.removed('29.0.0', _rdp_console_removal_reason) - @validation.schema(remote_consoles.get_rdp_console) + @validation.schema(schema.get_rdp_console) def get_rdp_console(self, req, id, body): """RDP console was available only for HyperV driver which has been removed from Nova in 29.0.0 (Caracal) release. @@ -110,7 +112,8 @@ class RemoteConsolesController(wsgi.Controller): @wsgi.Controller.api_version("2.1", "2.5") @wsgi.expected_errors((400, 404, 409, 501)) @wsgi.action('os-getSerialConsole') - @validation.schema(remote_consoles.get_serial_console) + @validation.schema(schema.get_serial_console) + @validation.response_body_schema(schema.get_serial_console_response) def get_serial_console(self, req, id, body): """Get connection to a serial console.""" context = req.environ['nova.context'] @@ -139,8 +142,8 @@ class RemoteConsolesController(wsgi.Controller): @wsgi.Controller.api_version("2.6") @wsgi.expected_errors((400, 404, 409, 501)) - @validation.schema(remote_consoles.create_v26, "2.6", "2.7") - @validation.schema(remote_consoles.create_v28, "2.8") + @validation.schema(schema.create_v26, "2.6", "2.7") + @validation.schema(schema.create_v28, "2.8") def create(self, req, server_id, body): context = req.environ['nova.context'] instance = common.get_instance(self.compute_api, context, server_id) diff --git a/nova/api/openstack/compute/schemas/console_output.py b/nova/api/openstack/compute/schemas/console_output.py index e6885fca96..e3bc17589a 100644 --- a/nova/api/openstack/compute/schemas/console_output.py +++ b/nova/api/openstack/compute/schemas/console_output.py @@ -34,3 +34,12 @@ get_console_output = { 'required': ['os-getConsoleOutput'], 'additionalProperties': False, } + +get_console_output_response = { + 'type': 'object', + 'properties': { + 'output': {'type': 'string'}, + }, + 'required': ['output'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/compute/schemas/create_backup.py b/nova/api/openstack/compute/schemas/create_backup.py index 29401c853b..7208475c18 100644 --- a/nova/api/openstack/compute/schemas/create_backup.py +++ b/nova/api/openstack/compute/schemas/create_backup.py @@ -41,5 +41,19 @@ create_backup = { create_backup_v20 = copy.deepcopy(create_backup) create_backup_v20['properties'][ - 'createBackup']['properties']['name'] = (parameter_types. - name_with_leading_trailing_spaces) + 'createBackup']['properties']['name'] = ( + parameter_types.name_with_leading_trailing_spaces) + + +create_backup_response = { + 'type': 'null', +} + +create_backup_response_v245 = { + 'type': 'object', + 'properties': { + 'image_id': {'type': 'string', 'format': 'uuid'}, + }, + 'required': ['image_id'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/compute/schemas/evacuate.py b/nova/api/openstack/compute/schemas/evacuate.py index c7b84a655e..22699c2994 100644 --- a/nova/api/openstack/compute/schemas/evacuate.py +++ b/nova/api/openstack/compute/schemas/evacuate.py @@ -39,14 +39,31 @@ evacuate_v214 = copy.deepcopy(evacuate) del evacuate_v214['properties']['evacuate']['properties']['onSharedStorage'] del evacuate_v214['properties']['evacuate']['required'] -evacuate_v2_29 = copy.deepcopy(evacuate_v214) -evacuate_v2_29['properties']['evacuate']['properties'][ +evacuate_v229 = copy.deepcopy(evacuate_v214) +evacuate_v229['properties']['evacuate']['properties'][ 'force'] = parameter_types.boolean # v2.68 removes the 'force' parameter added in v2.29, meaning it is identical # to v2.14 -evacuate_v2_68 = copy.deepcopy(evacuate_v214) +evacuate_v268 = copy.deepcopy(evacuate_v214) # v2.95 keeps the same schema, evacuating an instance will now result its state # to be stopped at destination. -evacuate_v2_95 = copy.deepcopy(evacuate_v2_68) +evacuate_v295 = copy.deepcopy(evacuate_v268) + +evacuate_response = { + 'type': ['object', 'null'], + 'properties': { + 'adminPass': { + 'type': ['null', 'string'], + } + }, + # adminPass is a rare-example of configuration-driven API behavior: the + # value depends on '[api] enable_instance_password' + 'required': [], + 'additionalProperties': False, +} + +evacuate_response_v214 = { + 'type': 'null', +} diff --git a/nova/api/openstack/compute/schemas/flavor_access.py b/nova/api/openstack/compute/schemas/flavor_access.py index d17ca14c07..e489796746 100644 --- a/nova/api/openstack/compute/schemas/flavor_access.py +++ b/nova/api/openstack/compute/schemas/flavor_access.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + add_tenant_access = { 'type': 'object', 'properties': { @@ -31,7 +33,6 @@ add_tenant_access = { 'additionalProperties': False, } - remove_tenant_access = { 'type': 'object', 'properties': { @@ -57,3 +58,27 @@ index_query = { 'properties': {}, 'additionalProperties': True, } + +_common_response = { + 'type': 'object', + 'properties': { + 'flavor_access': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'flavor_id': {'type': 'string'}, + 'tenant_id': {'type': 'string', 'format': 'uuid'}, + }, + 'required': ['flavor_id', 'tenant_id'], + 'additionalProperties': True, + }, + }, + }, + 'required': ['flavor_access'], + 'additionalProperties': True, +} + +add_tenant_access_response = copy.deepcopy(_common_response) + +remove_tenant_access_response = copy.deepcopy(_common_response) diff --git a/nova/api/openstack/compute/schemas/remote_consoles.py b/nova/api/openstack/compute/schemas/remote_consoles.py index 71d3cc403d..1f57e95775 100644 --- a/nova/api/openstack/compute/schemas/remote_consoles.py +++ b/nova/api/openstack/compute/schemas/remote_consoles.py @@ -119,3 +119,78 @@ create_v28 = { 'required': ['remote_console'], 'additionalProperties': False, } + +get_vnc_console_response = { + 'type': 'object', + 'properties': { + 'console': { + 'type': 'object', + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['novnc', 'xvpvnc'], + 'description': '', + }, + 'url': { + 'type': 'string', + 'format': 'uri', + 'description': '', + }, + }, + 'required': ['type', 'url'], + 'additionalProperties': False, + }, + }, + 'required': ['console'], + 'additionalProperties': False, +} + +get_spice_console_response = { + 'type': 'object', + 'properties': { + 'console': { + 'type': 'object', + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['spice-html5'], + 'description': '', + }, + 'url': { + 'type': 'string', + 'format': 'uri', + 'description': '', + }, + }, + 'required': ['type', 'url'], + 'additionalProperties': False, + }, + }, + 'required': ['console'], + 'additionalProperties': False, +} + +get_serial_console_response = { + 'type': 'object', + 'properties': { + 'console': { + 'type': 'object', + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['serial'], + 'description': '', + }, + 'url': { + 'type': 'string', + 'format': 'uri', + 'description': '', + }, + }, + 'required': ['type', 'url'], + 'additionalProperties': False, + }, + }, + 'required': ['console'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 900ea4e42c..906005b4a3 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -238,8 +238,9 @@ create_v20['properties']['server']['properties'][ 'security_groups']['items']['properties']['name'] = ( parameter_types.name_with_leading_trailing_spaces) create_v20['properties']['server']['properties']['user_data'] = { - 'oneOf': [{'type': 'string', 'format': 'base64', 'maxLength': 65535}, - {'type': 'null'}, + 'oneOf': [ + {'type': 'string', 'format': 'base64', 'maxLength': 65535}, + {'type': 'null'}, ], } @@ -282,45 +283,49 @@ create_v237 = copy.deepcopy(create_v233) create_v237['properties']['server']['required'].append('networks') create_v237['properties']['server']['properties']['networks'] = { 'oneOf': [ - {'type': 'array', - 'items': { - 'type': 'object', - 'properties': { - 'fixed_ip': parameter_types.ip_address, - 'port': { - 'oneOf': [{'type': 'string', 'format': 'uuid'}, - {'type': 'null'}] - }, - 'uuid': {'type': 'string', 'format': 'uuid'}, - }, - 'additionalProperties': False, - }, + { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'fixed_ip': parameter_types.ip_address, + 'port': { + 'oneOf': [{'type': 'string', 'format': 'uuid'}, + {'type': 'null'}] + }, + 'uuid': {'type': 'string', 'format': 'uuid'}, + }, + 'additionalProperties': False, + }, }, {'type': 'string', 'enum': ['none', 'auto']}, - ]} + ], +} # 2.42 builds on 2.37 and re-introduces the tag field to the list of network # objects. create_v242 = copy.deepcopy(create_v237) create_v242['properties']['server']['properties']['networks'] = { 'oneOf': [ - {'type': 'array', - 'items': { - 'type': 'object', - 'properties': { - 'fixed_ip': parameter_types.ip_address, - 'port': { - 'oneOf': [{'type': 'string', 'format': 'uuid'}, - {'type': 'null'}] - }, - 'uuid': {'type': 'string', 'format': 'uuid'}, - 'tag': parameter_types.tag, - }, - 'additionalProperties': False, - }, + { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'fixed_ip': parameter_types.ip_address, + 'port': { + 'oneOf': [{'type': 'string', 'format': 'uuid'}, + {'type': 'null'}] + }, + 'uuid': {'type': 'string', 'format': 'uuid'}, + 'tag': parameter_types.tag, + }, + 'additionalProperties': False, + }, }, {'type': 'string', 'enum': ['none', 'auto']}, - ]} + ], +} create_v242['properties']['server'][ 'properties']['block_device_mapping_v2']['items'][ 'properties']['tag'] = parameter_types.tag @@ -465,7 +470,6 @@ rebuild_v294 = copy.deepcopy(rebuild_v290) rebuild_v294['properties']['rebuild']['properties'][ 'hostname'] = parameter_types.fqdn - resize = { 'type': 'object', 'properties': { @@ -771,3 +775,458 @@ stop_server_response = { trigger_crash_dump_response = { 'type': 'null', } + +create_image_response = { + 'type': 'null', +} + +create_image_response_v245 = { + 'type': 'object', + 'properties': { + 'image_id': {'type': 'string', 'format': 'uuid'}, + }, + 'required': ['image_id'], + 'additionalProperties': False, +} + +rebuild_response = { + 'type': 'object', + 'properties': { + 'server': { + 'type': 'object', + 'properties': { + 'accessIPv4': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'const': ''}, + ], + }, + 'accessIPv6': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv6'}, + {'const': ''}, + ], + }, + 'addresses': { + 'type': 'object', + 'patternProperties': { + '^.+$': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'addr': { + 'type': 'string', + 'oneOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'}, + ], + }, + 'version': { + 'type': 'number', + 'enum': [4, 6], + }, + }, + 'required': [ + 'addr', + 'version' + ], + 'additionalProperties': False, + }, + }, + }, + 'additionalProperties': False, + }, + 'adminPass': {'type': ['null', 'string']}, + 'created': {'type': 'string', 'format': 'date-time'}, + 'fault': { + 'type': 'object', + 'properties': { + 'code': {'type': 'integer'}, + 'created': {'type': 'string', 'format': 'date-time'}, + 'details': {'type': 'string'}, + 'message': {'type': 'string'}, + }, + 'required': ['code', 'created', 'message'], + 'additionalProperties': False, + }, + 'flavor': { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + }, + 'links': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': { + 'type': 'string', + 'format': 'uri', + }, + 'rel': { + 'type': 'string', + }, + }, + 'required': [ + 'href', + 'rel' + ], + "additionalProperties": False, + }, + }, + }, + 'additionalProperties': False, + }, + 'hostId': {'type': 'string'}, + 'id': {'type': 'string'}, + 'image': { + 'oneOf': [ + { + 'type': 'string', + 'const': '', + }, + { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string' + }, + 'links': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': { + 'type': 'string', + 'format': 'uri', + }, + 'rel': { + 'type': 'string', + }, + }, + 'required': [ + 'href', + 'rel' + ], + "additionalProperties": False, + }, + }, + }, + 'additionalProperties': False, + }, + ], + }, + 'links': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': { + 'type': 'string', + 'format': 'uri', + }, + 'rel': { + 'type': 'string', + }, + }, + 'required': [ + 'href', + 'rel' + ], + 'additionalProperties': False, + }, + }, + 'metadata': { + 'type': 'object', + 'patternProperties': { + '^.+$': { + 'type': 'string' + }, + }, + 'additionalProperties': False, + }, + 'name': {'type': ['string', 'null']}, + 'progress': {'type': ['null', 'number']}, + 'status': {'type': 'string'}, + 'tenant_id': {'type': 'string', 'format': 'uuid'}, + 'updated': {'type': 'string', 'format': 'date-time'}, + 'user_id': {'type': 'string'}, + 'OS-DCF:diskConfig': {'type': 'string'}, + }, + 'required': [ + 'accessIPv4', + 'accessIPv6', + 'addresses', + 'created', + 'flavor', + 'hostId', + 'id', + 'image', + 'links', + 'metadata', + 'name', + 'progress', + 'status', + 'tenant_id', + 'updated', + 'user_id', + 'OS-DCF:diskConfig', + ], + 'additionalProperties': False, + }, + }, + 'required': [ + 'server' + ], + 'additionalProperties': False, +} + +rebuild_response_v29 = copy.deepcopy(rebuild_response) +rebuild_response_v29['properties']['server']['properties']['locked'] = { + 'type': 'boolean', +} +rebuild_response_v29['properties']['server']['required'].append('locked') + +rebuild_response_v219 = copy.deepcopy(rebuild_response_v29) +rebuild_response_v219['properties']['server']['properties']['description'] = { + 'type': ['null', 'string'], +} +rebuild_response_v219['properties']['server']['required'].append('description') + +rebuild_response_v226 = copy.deepcopy(rebuild_response_v219) +rebuild_response_v226['properties']['server']['properties']['tags'] = { + 'type': 'array', + 'items': { + 'type': 'string', + }, + 'maxItems': 50, +} +rebuild_response_v226['properties']['server']['required'].append('tags') + +# NOTE(stephenfin): We overwrite rather than extend 'flavor', since we now +# embed the flavor in this version +rebuild_response_v246 = copy.deepcopy(rebuild_response_v226) +rebuild_response_v246['properties']['server']['properties']['flavor'] = { + 'type': 'object', + 'properties': { + 'vcpus': { + 'type': 'integer', + }, + 'ram': { + 'type': 'integer', + }, + 'disk': { + 'type': 'integer', + }, + 'ephemeral': { + 'type': 'integer', + }, + 'swap': { + 'type': 'integer', + }, + 'original_name': { + 'type': 'string', + }, + 'extra_specs': { + 'type': 'object', + 'patternProperties': { + '^.+$': { + 'type': 'string' + }, + }, + 'additionalProperties': False, + }, + }, + 'required': ['vcpus', 'ram', 'disk', 'ephemeral', 'swap', 'original_name'], + 'additionalProperties': False, +} + +rebuild_response_v254 = copy.deepcopy(rebuild_response_v246) +rebuild_response_v254['properties']['server']['properties']['key_name'] = { + 'type': ['null', 'string'], +} +rebuild_response_v254['properties']['server']['required'].append('key_name') + +rebuild_response_v257 = copy.deepcopy(rebuild_response_v254) +rebuild_response_v257['properties']['server']['properties']['user_data'] = { + 'oneOf': [ + {'type': 'string', 'format': 'base64', 'maxLength': 65535}, + {'type': 'null'}, + ], +} +rebuild_response_v257['properties']['server']['required'].append('user_data') + +rebuild_response_v263 = copy.deepcopy(rebuild_response_v257) +rebuild_response_v263['properties']['server']['properties'].update( + { + 'trusted_image_certificates': { + 'type': ['array', 'null'], + 'items': { + 'type': 'string', + }, + }, + }, +) +rebuild_response_v263['properties']['server']['required'].append( + 'trusted_image_certificates' +) + +rebuild_response_v271 = copy.deepcopy(rebuild_response_v263) +rebuild_response_v271['properties']['server']['properties'].update( + { + 'server_groups': { + 'type': 'array', + 'items': { + 'type': 'string', + 'format': 'uuid', + }, + 'maxLength': 1, + }, + }, +) +rebuild_response_v271['properties']['server']['required'].append( + 'server_groups' +) + +rebuild_response_v273 = copy.deepcopy(rebuild_response_v271) +rebuild_response_v273['properties']['server']['properties'].update( + { + 'locked_reason': { + 'type': ['null', 'string'], + }, + }, +) +rebuild_response_v273['properties']['server']['required'].append( + 'locked_reason' +) + +rebuild_response_v275 = copy.deepcopy(rebuild_response_v273) +rebuild_response_v275['properties']['server']['properties'].update( + { + 'config_drive': { + # TODO(stephenfin): Our tests return null but this shouldn't happen + # in practice, apparently? + 'type': ['string', 'boolean', 'null'], + }, + 'OS-EXT-AZ:availability_zone': { + 'type': 'string', + }, + 'OS-EXT-SRV-ATTR:host': { + 'type': ['string', 'null'], + }, + 'OS-EXT-SRV-ATTR:hypervisor_hostname': { + 'type': ['string', 'null'], + }, + 'OS-EXT-SRV-ATTR:instance_name': { + 'type': 'string', + }, + 'OS-EXT-STS:power_state': { + 'type': 'integer', + 'enum': [0, 1, 3, 4, 6, 7], + }, + 'OS-EXT-STS:task_state': { + 'type': ['null', 'string'], + }, + 'OS-EXT-STS:vm_state': { + 'type': 'string', + }, + 'OS-EXT-SRV-ATTR:hostname': { + 'type': 'string', + }, + 'OS-EXT-SRV-ATTR:reservation_id': { + 'type': ['string', 'null'], + }, + 'OS-EXT-SRV-ATTR:launch_index': { + 'type': 'integer', + }, + 'OS-EXT-SRV-ATTR:kernel_id': { + 'type': ['string', 'null'], + }, + 'OS-EXT-SRV-ATTR:ramdisk_id': { + 'type': ['string', 'null'], + }, + 'OS-EXT-SRV-ATTR:root_device_name': { + 'type': ['string', 'null'], + }, + 'os-extended-volumes:volumes_attached': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + }, + 'delete_on_termination': { + 'type': 'boolean', + 'default': False, + }, + }, + 'required': ['id', 'delete_on_termination'], + 'additionalProperties': False, + }, + }, + 'OS-SRV-USG:launched_at': { + 'oneOf': [ + {'type': 'null'}, + {'type': 'string', 'format': 'date-time'}, + ], + }, + 'OS-SRV-USG:terminated_at': { + 'oneOf': [ + {'type': 'null'}, + {'type': 'string', 'format': 'date-time'}, + ], + }, + 'security_groups': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + }, + }, + 'required': ['name'], + 'additionalProperties': False, + }, + }, + 'host_status': { + 'type': 'string', + }, + }, +) +rebuild_response_v275['properties']['server']['required'].extend([ + 'config_drive', + 'OS-EXT-AZ:availability_zone', + 'OS-EXT-STS:power_state', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:vm_state', + 'os-extended-volumes:volumes_attached', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at', +]) +rebuild_response_v275['properties']['server']['properties']['addresses'][ + 'patternProperties' +]['^.+$']['items']['properties'].update({ + 'OS-EXT-IPS-MAC:mac_addr': {'type': 'string', 'format': 'mac-address'}, + 'OS-EXT-IPS:type': {'type': 'string', 'enum': ['fixed', 'floating']}, +}) +rebuild_response_v275['properties']['server']['properties']['addresses'][ + 'patternProperties' +]['^.+$']['items']['required'].extend([ + 'OS-EXT-IPS-MAC:mac_addr', 'OS-EXT-IPS:type' +]) + +rebuild_response_v296 = copy.deepcopy(rebuild_response_v275) +rebuild_response_v296['properties']['server']['properties'].update({ + 'pinned_availability_zone': { + 'type': ['null', 'string'], + }, +}) +rebuild_response_v296['properties']['server']['required'].append( + 'pinned_availability_zone' +) diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 28ec348856..b7500d2829 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -1163,6 +1163,29 @@ class ServersController(wsgi.Controller): @validation.schema(schema.rebuild_v263, '2.63', '2.89') @validation.schema(schema.rebuild_v290, '2.90', '2.93') @validation.schema(schema.rebuild_v294, '2.94') + @validation.response_body_schema(schema.rebuild_response, '2.0', '2.8') + @validation.response_body_schema( + schema.rebuild_response_v29, '2.9', '2.18') + @validation.response_body_schema( + schema.rebuild_response_v219, '2.19', '2.25') + @validation.response_body_schema( + schema.rebuild_response_v226, '2.26', '2.45') + @validation.response_body_schema( + schema.rebuild_response_v246, '2.46', '2.53') + @validation.response_body_schema( + schema.rebuild_response_v254, '2.54', '2.56') + @validation.response_body_schema( + schema.rebuild_response_v257, '2.57', '2.62') + @validation.response_body_schema( + schema.rebuild_response_v263, '2.63', '2.70') + @validation.response_body_schema( + schema.rebuild_response_v271, '2.71', '2.72') + @validation.response_body_schema( + schema.rebuild_response_v273, '2.73', '2.74') + @validation.response_body_schema( + schema.rebuild_response_v275, '2.75', '2.95') + @validation.response_body_schema( + schema.rebuild_response_v296, '2.96') def _action_rebuild(self, req, id, body): """Rebuild an instance with the given attributes.""" rebuild_dict = body['rebuild'] @@ -1333,6 +1356,9 @@ class ServersController(wsgi.Controller): @wsgi.action('createImage') @validation.schema(schema.create_image, '2.0', '2.0') @validation.schema(schema.create_image, '2.1') + @validation.response_body_schema( + schema.create_image_response, '2.0', '2.44') + @validation.response_body_schema(schema.create_image_response_v245, '2.45') def _action_create_image(self, req, id, body): """Snapshot a server instance.""" context = req.environ['nova.context'] diff --git a/nova/tests/functional/api_sample_tests/api_samples/flavor-access/flavor-access-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/flavor-access/flavor-access-list-resp.json.tpl index a6b6dbdcda..d797155795 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/flavor-access/flavor-access-list-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/flavor-access/flavor-access-list-resp.json.tpl @@ -2,7 +2,7 @@ "flavor_access": [ { "flavor_id": "%(flavor_id)s", - "tenant_id": "fake_tenant" + "tenant_id": "%(tenant_id)s" } ] } diff --git a/nova/tests/functional/api_sample_tests/test_flavor_access.py b/nova/tests/functional/api_sample_tests/test_flavor_access.py index 0f7d204dda..4e79f1624e 100644 --- a/nova/tests/functional/api_sample_tests/test_flavor_access.py +++ b/nova/tests/functional/api_sample_tests/test_flavor_access.py @@ -11,7 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - from nova.tests.functional.api_sample_tests import api_sample_base @@ -21,7 +20,7 @@ class FlavorAccessTestsBase(api_sample_base.ApiSampleTestBaseV21): def _add_tenant(self): subs = { - 'tenant_id': 'fake_tenant', + 'tenant_id': self.api.project_id, 'flavor_id': '10', } response = self._do_post('flavors/10/action', @@ -49,7 +48,7 @@ class FlavorAccessSampleJsonTests(FlavorAccessTestsBase): response = self._do_get('flavors/%s/os-flavor-access' % flavor_id) subs = { 'flavor_id': flavor_id, - 'tenant_id': 'fake_tenant', + 'tenant_id': self.api.project_id, } self._verify_response('flavor-access-list-resp', subs, response, 200) @@ -61,7 +60,7 @@ class FlavorAccessSampleJsonTests(FlavorAccessTestsBase): self._create_flavor() self._add_tenant() subs = { - 'tenant_id': 'fake_tenant', + 'tenant_id': self.api.project_id, } response = self._do_post('flavors/10/action', "flavor-access-remove-tenant-req", @@ -88,7 +87,7 @@ class FlavorAccessV27SampleJsonTests(FlavorAccessTestsBase): subs = { 'flavor_id': '10', - 'tenant_id': 'fake_tenant' + 'tenant_id': self.api.project_id } # Version 2.7+ will return HTTPConflict (409) # if the flavor is public diff --git a/nova/tests/functional/notification_sample_tests/test_flavor.py b/nova/tests/functional/notification_sample_tests/test_flavor.py index 478cbc2c64..2f0afb320b 100644 --- a/nova/tests/functional/notification_sample_tests/test_flavor.py +++ b/nova/tests/functional/notification_sample_tests/test_flavor.py @@ -75,7 +75,7 @@ class TestFlavorNotificationSample( body = { "addTenantAccess": { - "tenant": "fake_tenant" + "tenant": "6f70656e737461636b20342065766572" } } self.admin_api.api_post( diff --git a/nova/tests/unit/api/openstack/compute/test_create_backup.py b/nova/tests/unit/api/openstack/compute/test_create_backup.py index 9728002e88..e62b78f288 100644 --- a/nova/tests/unit/api/openstack/compute/test_create_backup.py +++ b/nova/tests/unit/api/openstack/compute/test_create_backup.py @@ -15,12 +15,12 @@ from unittest import mock +from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils import timeutils import webob from nova.api.openstack import common -from nova.api.openstack.compute import create_backup \ - as create_backup_v21 +from nova.api.openstack.compute import create_backup from nova.compute import api from nova.compute import utils as compute_utils from nova import exception @@ -32,7 +32,7 @@ from nova.tests.unit import fake_instance class CreateBackupTestsV21(admin_only_action_common.CommonMixin, test.NoDBTestCase): - create_backup = create_backup_v21 + create_backup = create_backup controller_name = 'CreateBackupController' validation_error = exception.ValidationError @@ -54,7 +54,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, }, } - image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + image = dict(id=uuids.image_id, status='ACTIVE', name='Backup 1', properties=metadata) instance = fake_instance.fake_instance_obj(self.context) @@ -70,7 +70,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, extra_properties=metadata) self.assertEqual(202, res.status_int) - self.assertIn('fake-image-id', res.headers['Location']) + self.assertIn(uuids.image_id, res.headers['Location']) def test_create_backup_no_name(self): # Name is required for backups. @@ -107,7 +107,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, 'rotation': 1, }, } - image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + image = dict(id=uuids.image_id, status='ACTIVE', name='Backup 1', properties={}) instance = fake_instance.fake_instance_obj(self.context) self.mock_get.return_value = instance @@ -217,7 +217,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, }, } - image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + image = dict(id=uuids.image_id, status='ACTIVE', name='Backup 1', properties={}) instance = fake_instance.fake_instance_obj(self.context) self.mock_get.return_value = instance @@ -246,7 +246,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, }, } - image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + image = dict(id=uuids.image_id, status='ACTIVE', name='Backup 1', properties={}) instance = fake_instance.fake_instance_obj(self.context) self.mock_get.return_value = instance @@ -261,7 +261,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, extra_properties={}) self.assertEqual(202, res.status_int) - self.assertIn('fake-image-id', res.headers['Location']) + self.assertIn(uuids.image_id, res.headers['Location']) @mock.patch.object(common, 'check_img_metadata_properties_quota') @mock.patch.object(api.API, 'backup') @@ -275,7 +275,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, }, } - image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1', + image = dict(id=uuids.image_id, status='ACTIVE', name='Backup 1', properties={}) instance = fake_instance.fake_instance_obj(self.context) self.mock_get.return_value = instance @@ -289,11 +289,11 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, 'daily', 1, extra_properties={}) self.assertEqual(202, res.status_int) - self.assertIn('fake-image-id', res.headers['Location']) + self.assertIn(uuids.image_id, res.headers['Location']) @mock.patch.object(common, 'check_img_metadata_properties_quota') @mock.patch.object(api.API, 'backup', return_value=dict( - id='fake-image-id', status='ACTIVE', name='Backup 1', properties={})) + id=uuids.image_id, status='ACTIVE', name='Backup 1', properties={})) def test_create_backup_v2_45(self, mock_backup, mock_check_image): """Tests the 2.45 microversion to ensure the Location header is not in the response. @@ -310,7 +310,7 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin, req = fakes.HTTPRequest.blank('', version='2.45') res = self.controller._create_backup(req, instance['uuid'], body=body) self.assertIsInstance(res, dict) - self.assertEqual('fake-image-id', res['image_id']) + self.assertEqual(uuids.image_id, res['image_id']) @mock.patch.object(common, 'check_img_metadata_properties_quota') @mock.patch.object(api.API, 'backup') @@ -396,7 +396,7 @@ class CreateBackupTestsV239(test.NoDBTestCase): def setUp(self): super(CreateBackupTestsV239, self).setUp() - self.controller = create_backup_v21.CreateBackupController() + self.controller = create_backup.CreateBackupController() self.req = fakes.HTTPRequest.blank('', version='2.39') @mock.patch.object(common, 'check_img_metadata_properties_quota') diff --git a/nova/tests/unit/api/openstack/compute/test_flavor_access.py b/nova/tests/unit/api/openstack/compute/test_flavor_access.py index 7070e5a99c..2e137a7844 100644 --- a/nova/tests/unit/api/openstack/compute/test_flavor_access.py +++ b/nova/tests/unit/api/openstack/compute/test_flavor_access.py @@ -16,11 +16,11 @@ import datetime from unittest import mock +from oslo_utils.fixture import uuidsentinel as uuids from webob import exc from nova.api.openstack import api_version_request as api_version -from nova.api.openstack.compute import flavor_access \ - as flavor_access_v21 +from nova.api.openstack.compute import flavor_access from nova.api.openstack.compute import flavors as flavors_api from nova import context from nova import exception @@ -57,9 +57,9 @@ FLAVORS = { ACCESS_LIST = [ - {'flavor_id': '2', 'project_id': 'proj2'}, - {'flavor_id': '2', 'project_id': 'proj3'}, - {'flavor_id': '3', 'project_id': 'proj3'}, + {'flavor_id': '2', 'project_id': uuids.proj2}, + {'flavor_id': '2', 'project_id': uuids.proj3}, + {'flavor_id': '3', 'project_id': uuids.proj3}, ] @@ -126,8 +126,8 @@ def fake_get_flavor_projects_from_db(context, flavorid): class FlavorAccessTestV21(test.NoDBTestCase): api_version = "2.1" - FlavorAccessController = flavor_access_v21.FlavorAccessController - FlavorActionController = flavor_access_v21.FlavorActionController + FlavorAccessController = flavor_access.FlavorAccessController + FlavorActionController = flavor_access.FlavorActionController _prefix = "/v2/%s" % fakes.FAKE_PROJECT_ID validation_ex = exception.ValidationError @@ -175,8 +175,8 @@ class FlavorAccessTestV21(test.NoDBTestCase): req.environ = {"nova.context": context.RequestContext( 'fake_user', fakes.FAKE_PROJECT_ID)} expected = {'flavor_access': [ - {'flavor_id': '2', 'tenant_id': 'proj2'}, - {'flavor_id': '2', 'tenant_id': 'proj3'}]} + {'flavor_id': '2', 'tenant_id': uuids.proj2}, + {'flavor_id': '2', 'tenant_id': uuids.proj3}]} result = self.flavor_access_controller.index(req, '2') self.assertEqual(result, expected) @@ -192,7 +192,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}]} req = fakes.HTTPRequest.blank(self._prefix + '/flavors', use_admin_context=True) - req.environ['nova.context'].project_id = 'proj2' + req.environ['nova.context'].project_id = uuids.proj2 result = self.flavor_controller.index(req) self._verify_flavor_list(result['flavors'], expected['flavors']) @@ -217,7 +217,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): url = self._prefix + '/flavors?is_public=false' req = fakes.HTTPRequest.blank(url, use_admin_context=True) - req.environ['nova.context'].project_id = 'proj2' + req.environ['nova.context'].project_id = uuids.proj2 result = self.flavor_controller.index(req) self._verify_flavor_list(result['flavors'], expected['flavors']) @@ -264,12 +264,13 @@ class FlavorAccessTestV21(test.NoDBTestCase): def test_add_tenant_access(self): def stub_add_flavor_access(context, flavor_id, projectid): self.assertEqual(3, flavor_id, "flavor_id") - self.assertEqual("proj2", projectid, "projectid") + self.assertEqual(uuids.proj2, projectid, "projectid") self.stub_out('nova.objects.Flavor._flavor_add_project', stub_add_flavor_access) - expected = {'flavor_access': - [{'flavor_id': '3', 'tenant_id': 'proj3'}]} - body = {'addTenantAccess': {'tenant': 'proj2'}} + expected = { + 'flavor_access': [{'flavor_id': '3', 'tenant_id': uuids.proj3}] + } + body = {'addTenantAccess': {'tenant': uuids.proj2}} req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) @@ -280,7 +281,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): @mock.patch('nova.objects.Flavor.get_by_flavor_id', side_effect=exception.FlavorNotFound(flavor_id='1')) def test_add_tenant_access_with_flavor_not_found(self, mock_get): - body = {'addTenantAccess': {'tenant': 'proj2'}} + body = {'addTenantAccess': {'tenant': uuids.proj2}} req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) self.assertRaises(exc.HTTPNotFound, @@ -290,7 +291,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): def test_add_tenant_access_with_no_tenant(self): req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) - body = {'addTenantAccess': {'foo': 'proj2'}} + body = {'addTenantAccess': {'foo': uuids.proj2}} self.assertRaises(self.validation_ex, self.flavor_action_controller._add_tenant_access, req, '2', body=body) @@ -309,7 +310,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): self._prefix + '/flavors/3/os-flavor-access') req.environ = {"nova.context": context.RequestContext( 'fake_user', fakes.FAKE_PROJECT_ID)} - body = {'addTenantAccess': {'tenant': 'proj2'}} + body = {'addTenantAccess': {'tenant': uuids.proj2}} self.assertRaises(exc.HTTPConflict, self.flavor_action_controller._add_tenant_access, req, '3', body=body) @@ -320,7 +321,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): project_id=projectid) self.stub_out('nova.objects.Flavor._flavor_del_project', stub_remove_flavor_access) - body = {'removeTenantAccess': {'tenant': 'proj2'}} + body = {'removeTenantAccess': {'tenant': uuids.proj2}} req = fakes.HTTPRequest.blank( self._prefix + '/flavors/3/os-flavor-access') req.environ = {"nova.context": context.RequestContext( @@ -330,7 +331,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): req, '3', body=body) def test_add_tenant_access_is_public(self): - body = {'addTenantAccess': {'tenant': 'proj2'}} + body = {'addTenantAccess': {'tenant': uuids.proj2}} req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) req.api_version_request = api_version.APIVersionRequest('2.7') @@ -343,7 +344,7 @@ class FlavorAccessTestV21(test.NoDBTestCase): def test_delete_tenant_access_with_no_tenant(self, mock_api_get): req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) - body = {'removeTenantAccess': {'foo': 'proj2'}} + body = {'removeTenantAccess': {'foo': uuids.proj2}} self.assertRaises(self.validation_ex, self.flavor_action_controller._remove_tenant_access, req, '2', body=body) @@ -359,30 +360,32 @@ class FlavorAccessTestV21(test.NoDBTestCase): """Tests the case that the tenant does not exist in Keystone.""" req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) - body = {'addTenantAccess': {'tenant': 'proj2'}} + body = {'addTenantAccess': {'tenant': uuids.proj2}} self.assertRaises(exc.HTTPBadRequest, self.flavor_action_controller._add_tenant_access, req, '2', body=body) mock_verify.assert_called_once_with( - req.environ['nova.context'], 'proj2') + req.environ['nova.context'], uuids.proj2) @mock.patch('nova.objects.Flavor.remove_access') - @mock.patch('nova.api.openstack.identity.verify_project_id', - side_effect=exc.HTTPBadRequest( - explanation="Project ID proj2 is not a valid project.")) + @mock.patch('nova.api.openstack.identity.verify_project_id') def test_remove_tenant_access_with_invalid_tenant(self, mock_verify, mock_remove_access): """Tests the case that the tenant does not exist in Keystone.""" + mock_verify.side_effect = exc.HTTPBadRequest(explanation=( + f"Project ID {uuids.proj2} is not a valid project." + )) + req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) - body = {'removeTenantAccess': {'tenant': 'proj2'}} + body = {'removeTenantAccess': {'tenant': uuids.proj2}} self.flavor_action_controller._remove_tenant_access( req, '2', body=body) mock_verify.assert_called_once_with( - req.environ['nova.context'], 'proj2') - mock_remove_access.assert_called_once_with('proj2') + req.environ['nova.context'], uuids.proj2) + mock_remove_access.assert_called_once_with(uuids.proj2) @mock.patch('nova.api.openstack.identity.verify_project_id', side_effect=exc.HTTPBadRequest( @@ -395,10 +398,10 @@ class FlavorAccessTestV21(test.NoDBTestCase): """ req = fakes.HTTPRequest.blank(self._prefix + '/flavors/2/action', use_admin_context=True) - body = {'removeTenantAccess': {'tenant': 'proj2'}} + body = {'removeTenantAccess': {'tenant': uuids.proj2}} self.assertRaises(exc.HTTPBadRequest, self.flavor_action_controller._remove_tenant_access, req, '2', body=body) mock_verify.assert_called_once_with( - req.environ['nova.context'], 'proj2') + req.environ['nova.context'], uuids.proj2) diff --git a/nova/tests/unit/api/openstack/compute/test_keypairs.py b/nova/tests/unit/api/openstack/compute/test_keypairs.py index 590639d5ed..48689522c4 100644 --- a/nova/tests/unit/api/openstack/compute/test_keypairs.py +++ b/nova/tests/unit/api/openstack/compute/test_keypairs.py @@ -395,7 +395,7 @@ class KeypairsTestV210(KeypairsTestV22): with mock.patch.object(self.controller.api, 'get_key_pairs') as mock_g: self.controller.index(req) userid = mock_g.call_args_list[0][0][1] - self.assertEqual('fake_user', userid) + self.assertEqual(fakes.FAKE_USER_ID, userid) class KeypairsTestV235(test.TestCase): @@ -421,7 +421,7 @@ class KeypairsTestV235(test.TestCase): res_dict = self.controller.index(req) mock_kp_get.assert_called_once_with( - req.environ['nova.context'], 'fake_user', + req.environ['nova.context'], fakes.FAKE_USER_ID, limit=3, marker='fake_marker') response = {'keypairs': [{'keypair': dict(keypair_data, name='FAKE', type='ssh')}]} @@ -458,7 +458,7 @@ class KeypairsTestV235(test.TestCase): self.controller.index(req) mock_kp_get.assert_called_once_with( - req.environ['nova.context'], 'fake_user', + req.environ['nova.context'], fakes.FAKE_USER_ID, limit=None, marker=None) diff --git a/nova/tests/unit/api/openstack/compute/test_server_actions.py b/nova/tests/unit/api/openstack/compute/test_server_actions.py index 7a71858c56..c69f9b61e4 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_actions.py +++ b/nova/tests/unit/api/openstack/compute/test_server_actions.py @@ -1002,12 +1002,15 @@ class ServerActionsControllerTestV21(test.TestCase): response = self.controller._action_create_image(self.req, FAKE_UUID, body=body) - location = response.headers['Location'] - self.assertEqual(self.image_url + '123' if self.image_url else - self.image_api.generate_image_url('123', self.context), - location) + if self.image_url: + expected_location = self.image_url + uuids.snapshot_id + else: + expected_location = self.image_api.generate_image_url( + uuids.snapshot_id, self.context + ) + self.assertEqual(response.headers['Location'], expected_location) - def test_create_image_v2_45(self): + def test_create_image_v245(self): """Tests the createImage server action API with the 2.45 microversion where there is a response body but no Location header. """ @@ -1020,7 +1023,7 @@ class ServerActionsControllerTestV21(test.TestCase): response = self.controller._action_create_image(req, FAKE_UUID, body=body) self.assertIsInstance(response, dict) - self.assertEqual('123', response['image_id']) + self.assertEqual(uuids.snapshot_id, response['image_id']) def test_create_image_name_too_long(self): long_name = 'a' * 260 @@ -1254,9 +1257,13 @@ class ServerActionsControllerTestV21(test.TestCase): response = self.controller._action_create_image(self.req, FAKE_UUID, body=body) - location = response.headers['Location'] - self.assertEqual(self.image_url + '123' if self.image_url else - self.image_api.generate_image_url('123', self.context), location) + if self.image_url: + expected_location = self.image_url + uuids.snapshot_id + else: + expected_location = self.image_api.generate_image_url( + uuids.snapshot_id, self.context + ) + self.assertEqual(response.headers['Location'], expected_location) def test_create_image_with_too_much_metadata(self): body = { diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py index 8a77cf6a5c..fd4db0d0b9 100644 --- a/nova/tests/unit/api/openstack/compute/test_servers.py +++ b/nova/tests/unit/api/openstack/compute/test_servers.py @@ -171,7 +171,7 @@ def fake_get_inst_mappings_by_instance_uuids_from_db(*args, **kwargs): 'transport_url': 'fake://nowhere/', 'updated_at': None, 'database_connection': uuids.cell1, 'created_at': None, 'disabled': False}, - 'project_id': 'fake-project' + 'project_id': fakes.FAKE_PROJECT_ID, }] @@ -265,7 +265,7 @@ class _ServersControllerTest(ControllerTest): return { "server": { "id": uuid, - "user_id": "fake_user", + "user_id": fakes.FAKE_USER_ID, "created": "2010-10-10T12:00:00Z", "updated": "2010-11-11T11:00:00Z", "progress": progress, @@ -3767,6 +3767,21 @@ class ServersControllerRebuildTestV275(ControllerTest): microversion = '2.75' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' + def setUp(self): + super().setUp() + + mock_rebuild = mock.patch( + 'nova.compute.api.API.rebuild', return_value=None) + self.mock_rebuild = mock_rebuild.start() + self.addCleanup(mock_rebuild.stop) + + self.mock_get_instance_host_status = self.useFixture( + fixtures.MockPatchObject( + compute_api.API, 'get_instance_host_status', + return_value='UP' + ) + ).mock + def test_rebuild_response_no_show_server_only_attributes_old_version(self): # There are some old server attributes which were added only for # GET server APIs not for Rebuild. GET server and Rebuild server share @@ -3799,11 +3814,29 @@ class ServersControllerRebuildTestV275(ControllerTest): req = fakes.HTTPRequest.blank(self.path_with_query % 'unknown=1', use_admin_context=True, version=self.microversion) - fake_get = fakes.fake_compute_get( + self.mock_get.side_effect = fakes.fake_compute_get( + id=2, + display_description="", + uuid=FAKE_UUID, + node="node-fake", + reservation_id="r-1", + launch_index=0, + kernel_id=UUID1, + ramdisk_id=UUID2, + display_name="server2", + host='host', + root_device_name="/dev/vda", + user_data="userdata", + metadata={"seq": "2"}, + availability_zone='nova', + launched_at=None, + terminated_at=None, + task_state="ACTIVE", vm_state=vm_states.ACTIVE, + power_state=1, project_id=req.environ['nova.context'].project_id, user_id=req.environ['nova.context'].user_id) - self.mock_get.side_effect = fake_get + res_dict = self.controller._action_rebuild(req, FAKE_UUID, body=body).obj for field in GET_ONLY_FIELDS: @@ -3829,6 +3862,13 @@ class ServersControllerRebuildTestV290(ControllerTest): self.mock_rebuild = mock_rebuild.start() self.addCleanup(mock_rebuild.stop) + self.mock_get_instance_host_status = self.useFixture( + fixtures.MockPatchObject( + compute_api.API, 'get_instance_host_status', + return_value='UP' + ) + ).mock + def _get_request(self, body=None): req = fakes.HTTPRequest.blank( self.path_action % FAKE_UUID, @@ -3851,6 +3891,29 @@ class ServersControllerRebuildTestV290(ControllerTest): } req = self._get_request(body) + self.mock_get.side_effect = fakes.fake_compute_get( + id=2, + display_description="", + uuid=FAKE_UUID, + node="node-fake", + reservation_id="r-1", + launch_index=0, + kernel_id=UUID1, + ramdisk_id=UUID2, + display_name="server2", + host='host', + root_device_name="/dev/vda", + user_data="userdata", + metadata={"seq": "2"}, + availability_zone='nova', + launched_at=None, + terminated_at=None, + task_state="ACTIVE", + vm_state=vm_states.ACTIVE, + power_state=1, + project_id=req.environ['nova.context'].project_id, + user_id=req.environ['nova.context'].user_id) + # There's nothing to check here from the return value since the # 'rebuild' API is a cast and we immediately fetch the instance from # the database after this cast...which returns a mocked Instance @@ -5826,7 +5889,7 @@ class ServersControllerCreateTest(_ServersControllerCreateTest): mock_count.return_value = count mock_get_all_p.return_value = {'project_id': fakes.FAKE_PROJECT_ID} mock_get_all_pu.return_value = {'project_id': fakes.FAKE_PROJECT_ID, - 'user_id': 'fake_user'} + 'user_id': fakes.FAKE_USER_ID} if resource in db.PER_PROJECT_QUOTAS: mock_get_all_p.return_value[resource] = quota else: @@ -7235,7 +7298,7 @@ class ServersControllerCreateTestV274(_ServersControllerCreateTest): def setUp(self): super(ServersControllerCreateTestV274, self).setUp() self.req.environ['nova.context'] = fakes.FakeRequestContext( - user_id='fake_user', + user_id=fakes.FAKE_USER_ID, project_id=self.project_id, is_admin=True) self.mock_get = self.useFixture( @@ -7533,8 +7596,8 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): expected_server = { "server": { "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -7552,12 +7615,12 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): }, "flavor": { "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], }, "addresses": { 'test1': [ @@ -7623,8 +7686,8 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): expected_server = { "server": { "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "name": "test_server", @@ -7641,12 +7704,12 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): }, "flavor": { "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], }, "addresses": { 'test1': [ @@ -7822,8 +7885,8 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): expected_server = { "server": { "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 100, @@ -7841,12 +7904,12 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): }, "flavor": { "id": "1", - "links": [ - { - "rel": "bookmark", - "href": flavor_bookmark, - }, - ], + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], }, "addresses": { 'test1': [ @@ -7914,8 +7977,8 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): expected_server = { "server": { "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -7934,7 +7997,7 @@ class ServersViewBuilderTest(_ServersViewBuilderTest): "flavor": { "id": "1", "links": [ - { + { "rel": "bookmark", "href": flavor_bookmark, }, @@ -8042,8 +8105,8 @@ class ServersViewBuilderTestV269(_ServersViewBuilderTest): expected = { "servers": [{ "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "updated": "2010-11-11T11:00:00Z", "created": "2010-10-10T12:00:00Z", "progress": 0, @@ -8225,8 +8288,8 @@ class ServersViewBuilderTestV269(_ServersViewBuilderTest): expected = { "server": { "id": self.uuid, - "user_id": "fake_user", - "tenant_id": "fake_project", + "user_id": fakes.FAKE_USER_ID, + "tenant_id": fakes.FAKE_PROJECT_ID, "created": '1955-11-05T00:00:00Z', "status": "UNKNOWN", "image": { @@ -8288,7 +8351,7 @@ class ServersViewBuilderTestV269(_ServersViewBuilderTest): "server": { "id": self.uuid, "user_id": "UNKNOWN", - "tenant_id": "fake_project", + "tenant_id": fakes.FAKE_PROJECT_ID, "created": '1955-11-05T00:00:00Z', "status": "UNKNOWN", "image": "", diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py index 782e8767a1..f50de40e79 100644 --- a/nova/tests/unit/api/openstack/fakes.py +++ b/nova/tests/unit/api/openstack/fakes.py @@ -16,6 +16,7 @@ import datetime from oslo_serialization import jsonutils +from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils import timeutils from oslo_utils import uuidutils import routes @@ -139,8 +140,10 @@ def stub_out_compute_api_snapshot(test): # emulate glance rejecting image names which are too long if len(name) > 256: raise exc.Invalid - return dict(id='123', status='ACTIVE', name=name, - properties=extra_properties) + return { + 'id': uuids.snapshot_id, 'status': 'ACTIVE', 'name': name, + 'properties': extra_properties, + } test.stub_out('nova.compute.api.API.snapshot', snapshot) @@ -154,10 +157,12 @@ class stub_out_compute_api_backup(object): def backup(self, context, instance, name, backup_type, rotation, extra_properties=None): self.extra_props_last_call = extra_properties - props = dict(backup_type=backup_type, - rotation=rotation) + props = {'backup_type': backup_type, 'rotation': rotation} props.update(extra_properties or {}) - return dict(id='123', status='ACTIVE', name=name, properties=props) + return { + 'id': uuids.backup_id, 'status': 'ACTIVE', 'name': name, + 'properties': props, + } def stub_out_nw_api(test, cls=None, private=None, publics=None): @@ -244,11 +249,12 @@ class HTTPRequest(os_wsgi.Request): if use_admin_context: roles.append('admin') project_id = kwargs.pop('project_id', FAKE_PROJECT_ID) + user_id = kwargs.pop('user_id', FAKE_USER_ID) version = kwargs.pop('version', os_wsgi.DEFAULT_API_VERSION) defaults.update(kwargs) out = super(HTTPRequest, cls).blank(*args, **defaults) out.environ['nova.context'] = FakeRequestContext( - user_id='fake_user', + user_id=user_id, project_id=project_id, is_admin=use_admin_context, roles=roles) @@ -434,9 +440,9 @@ def stub_instance(id=1, user_id=None, project_id=None, host=None, services=None, trusted_certs=None, hidden=False, compute_id=None): if user_id is None: - user_id = 'fake_user' + user_id = FAKE_USER_ID if project_id is None: - project_id = 'fake_project' + project_id = FAKE_PROJECT_ID if metadata: metadata = [{'key': k, 'value': v} for k, v in metadata.items()] diff --git a/nova/tests/unit/policies/test_servers.py b/nova/tests/unit/policies/test_servers.py index 3d5f41c63e..ba015a6ca3 100644 --- a/nova/tests/unit/policies/test_servers.py +++ b/nova/tests/unit/policies/test_servers.py @@ -20,6 +20,7 @@ from oslo_utils import timeutils from nova.api.openstack.compute import migrate_server from nova.api.openstack.compute import servers from nova.compute import api as compute +from nova.compute import power_state from nova.compute import vm_states import nova.conf from nova import exception @@ -63,14 +64,18 @@ class ServersPolicyTest(base.BasePolicyTest): self.controller._view_builder._add_security_grps = mock.MagicMock() self.controller._view_builder._get_metadata = mock.MagicMock() self.controller._view_builder._get_addresses = mock.MagicMock() - self.controller._view_builder._get_host_id = mock.MagicMock() + self.controller._view_builder._get_host_id = mock.MagicMock( + return_value='' + ) self.controller._view_builder._get_fault = mock.MagicMock() self.instance = fake_instance.fake_instance_obj( - self.project_member_context, - id=1, uuid=uuids.fake_id, project_id=self.project_id, - user_id=user_id, vm_state=vm_states.ACTIVE, - system_metadata={}, expected_attrs=['system_metadata']) + self.project_member_context, + id=1, uuid=uuids.fake_id, project_id=self.project_id, + user_id=user_id, vm_state=vm_states.ACTIVE, + system_metadata={}, expected_attrs=['system_metadata'], + task_state=None, power_state=power_state.SHUTDOWN, + hostname='foo', launch_index=0) self.mock_flavor = self.useFixture( fixtures.MockPatch('nova.compute.flavors.get_flavor_by_flavor_id' @@ -912,7 +917,8 @@ class ServersPolicyTest(base.BasePolicyTest): self.assertNotIn(attr, resp['server']) @mock.patch('nova.objects.BlockDeviceMappingList.bdms_by_instance_uuid') - @mock.patch('nova.compute.api.API.get_instance_host_status') + @mock.patch('nova.compute.api.API.get_instance_host_status', + return_value=fields.HostStatus.UP) @mock.patch('nova.compute.api.API.rebuild') def test_server_rebuild_with_extended_attr_policy(self, mock_rebuild, mock_get, mock_bdm): @@ -1011,7 +1017,8 @@ class ServersPolicyTest(base.BasePolicyTest): self.assertNotIn('host_status', resp['server']) @mock.patch('nova.objects.BlockDeviceMappingList.bdms_by_instance_uuid') - @mock.patch('nova.compute.api.API.get_instance_host_status') + @mock.patch('nova.compute.api.API.get_instance_host_status', + return_value=fields.HostStatus.UP) @mock.patch('nova.compute.api.API.rebuild') def test_server_rebuild_with_host_status_policy(self, mock_rebuild, mock_status, mock_bdm):