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 b6c1bc77df37..1561b48e78df 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 b6c1bc77df37..1561b48e78df 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 9b2a719f5fdc..6ed5663ef544 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 75727fb2f832..f037ba719897 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 43b4114b9863..45b876390766 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 479f35957051..3f86c105441f 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 15bc5dc41703..56c805b8d276 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 08f87caa9632..408b11836ac1 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 e6885fca96e4..e3bc17589a0b 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 29401c853b42..7208475c185c 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 c7b84a655ec2..22699c29941d 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 d17ca14c07e8..e489796746d3 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 71d3cc403d9a..1f57e957757d 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 900ea4e42c5b..906005b4a36f 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 28ec34885638..b7500d28298d 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 a6b6dbdcda07..d797155795e5 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 0f7d204dda9a..4e79f1624ecc 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 478cbc2c64e4..2f0afb320bf9 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 9728002e885e..e62b78f288b5 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 7070e5a99c16..2e137a78446c 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 590639d5edc6..48689522c404 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 7a71858c564b..c69f9b61e42c 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 8a77cf6a5c36..fd4db0d0b9c1 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 782e8767a1b5..f50de40e79f0 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 3d5f41c63e96..ba015a6ca34d 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):