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