diff --git a/nova/api/openstack/compute/attach_interfaces.py b/nova/api/openstack/compute/attach_interfaces.py index ea1d19fe7b..dbf677ed3c 100644 --- a/nova/api/openstack/compute/attach_interfaces.py +++ b/nova/api/openstack/compute/attach_interfaces.py @@ -20,7 +20,7 @@ from webob import exc from nova.api.openstack import api_version_request from nova.api.openstack import common -from nova.api.openstack.compute.schemas import attach_interfaces +from nova.api.openstack.compute.schemas import attach_interfaces as schema from nova.api.openstack import wsgi from nova.api import validation from nova.compute import api as compute @@ -64,7 +64,9 @@ class InterfaceAttachmentController(wsgi.Controller): self.network_api = neutron.API() @wsgi.expected_errors((404, 501)) - @validation.query_schema(attach_interfaces.index_query) + @validation.query_schema(schema.index_query) + @validation.response_body_schema(schema.index_response, '2.1', '2.69') + @validation.response_body_schema(schema.index_response_v270, '2.70') def index(self, req, server_id): """Returns the list of interface attachments for a given instance.""" context = req.environ['nova.context'] @@ -107,7 +109,9 @@ class InterfaceAttachmentController(wsgi.Controller): return {'interfaceAttachments': results} @wsgi.expected_errors((403, 404)) - @validation.query_schema(attach_interfaces.show_query) + @validation.query_schema(schema.show_query) + @validation.response_body_schema(schema.show_response, '2.1', '2.69') + @validation.response_body_schema(schema.show_response_v270, '2.70') def show(self, req, server_id, id): """Return data about the given interface attachment.""" context = req.environ['nova.context'] @@ -135,8 +139,10 @@ class InterfaceAttachmentController(wsgi.Controller): show_tag=api_version_request.is_supported(req, '2.70'))} @wsgi.expected_errors((400, 403, 404, 409, 500, 501)) - @validation.schema(attach_interfaces.create, '2.0', '2.48') - @validation.schema(attach_interfaces.create_v249, '2.49') + @validation.schema(schema.create, '2.0', '2.48') + @validation.schema(schema.create_v249, '2.49') + @validation.response_body_schema(schema.create_response, '2.1', '2.69') + @validation.response_body_schema(schema.create_response_v270, '2.70') def create(self, req, server_id, body): """Attach an interface to an instance.""" context = req.environ['nova.context'] @@ -206,6 +212,7 @@ class InterfaceAttachmentController(wsgi.Controller): @wsgi.response(202) @wsgi.expected_errors((400, 404, 409, 501)) + @validation.response_body_schema(schema.delete_response) def delete(self, req, server_id, id): """Detach an interface from an instance.""" context = req.environ['nova.context'] diff --git a/nova/api/openstack/compute/schemas/attach_interfaces.py b/nova/api/openstack/compute/schemas/attach_interfaces.py index 8020c34ca8..1dd326a04c 100644 --- a/nova/api/openstack/compute/schemas/attach_interfaces.py +++ b/nova/api/openstack/compute/schemas/attach_interfaces.py @@ -48,8 +48,7 @@ create = { } create_v249 = copy.deepcopy(create) -create_v249['properties']['interfaceAttachment'][ - 'properties']['tag'] = parameter_types.tag +create_v249['properties']['interfaceAttachment']['properties']['tag'] = parameter_types.tag # noqa: E501 # TODO(stephenfin): Remove additionalProperties in a future API version index_query = { @@ -64,3 +63,78 @@ show_query = { 'properties': {}, 'additionalProperties': True, } + +_interface_attachment = { + 'type': 'object', + 'properties': { + 'fixed_ips': { + 'type': ['null', 'array'], + 'items': { + 'type': 'object', + 'properties': { + 'ip_address': { + 'type': 'string', + 'anyOf': [ + {'format': 'ipv4'}, + {'format': 'ipv6'}, + ], + }, + 'subnet_id': {'type': 'string', 'format': 'uuid'}, + }, + 'required': ['ip_address', 'subnet_id'], + 'additionalProperties': False, + }, + }, + 'mac_addr': {'type': 'string', 'format': 'mac-address'}, + 'net_id': {'type': 'string', 'format': 'uuid'}, + 'port_id': {'type': 'string', 'format': 'uuid'}, + 'port_state': {'type': 'string'}, + }, + 'required': ['fixed_ips', 'mac_addr', 'net_id', 'port_id', 'port_state'], + 'additionalProperties': False, +} + +_interface_attachment_v270 = copy.deepcopy(_interface_attachment) +_interface_attachment_v270['properties']['tag'] = { + 'type': ['null', 'string'], +} +_interface_attachment_v270['required'].append('tag') + +index_response = { + 'type': 'object', + 'properties': { + 'interfaceAttachments': { + 'type': 'array', + 'items': copy.deepcopy(_interface_attachment), + }, + }, + 'required': ['interfaceAttachments'], + 'additionalProperties': False, +} + +index_response_v270 = copy.deepcopy(index_response) +index_response_v270['properties']['interfaceAttachments']['items'] = copy.deepcopy( # noqa: E501 + _interface_attachment_v270 +) + +show_response = { + 'type': 'object', + 'properties': { + 'interfaceAttachment': copy.deepcopy(_interface_attachment), + }, + 'required': ['interfaceAttachment'], + 'additionalProperties': False, +} + +show_response_v270 = copy.deepcopy(show_response) +show_response_v270['properties']['interfaceAttachment'] = copy.deepcopy( + _interface_attachment_v270 +) + +# create responses are identical to show, including microversions +create_response = copy.deepcopy(show_response) +create_response_v270 = copy.deepcopy(show_response_v270) + +delete_response = { + 'type': 'null', +} diff --git a/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py b/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py index e4719ea052..ed1dd7de09 100644 --- a/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py +++ b/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py @@ -48,7 +48,9 @@ port_data1 = { "admin_state_up": True, "status": "ACTIVE", "mac_address": "aa:aa:aa:aa:aa:aa", - "fixed_ips": ["10.0.1.2"], + "fixed_ips": [ + {"ip_address": "10.0.1.2", "subnet_id": FAKE_UUID2}, + ], "device_id": FAKE_UUID1, } @@ -58,7 +60,9 @@ port_data2 = { "admin_state_up": True, "status": "ACTIVE", "mac_address": "bb:bb:bb:bb:bb:bb", - "fixed_ips": ["10.0.2.2"], + "fixed_ips": [ + {"ip_address": "10.0.2.2", "subnet_id": FAKE_UUID2}, + ], "device_id": FAKE_UUID1, } @@ -68,7 +72,9 @@ port_data3 = { "admin_state_up": True, "status": "ACTIVE", "mac_address": "bb:bb:bb:bb:bb:bb", - "fixed_ips": ["10.0.2.2"], + "fixed_ips": [ + {"ip_address": "10.0.3.2", "subnet_id": FAKE_UUID2}, + ], "device_id": '', } @@ -529,6 +535,13 @@ class InterfaceAttachTestsV249(test.NoDBTestCase): def setUp(self): super(InterfaceAttachTestsV249, self).setUp() self.attachments = self.controller_cls() + show_port_patch = mock.patch.object(self.attachments.network_api, + 'show_port', fake_show_port) + show_port_patch.start() + self.addCleanup(show_port_patch.stop) + self.stub_out('nova.compute.api.API.attach_interface', + fake_attach_interface) + self.req = fakes.HTTPRequest.blank('', version='2.49') def test_tagged_interface_attach_invalid_tag_comma(self): @@ -550,12 +563,12 @@ class InterfaceAttachTestsV249(test.NoDBTestCase): self.assertRaises(exception.ValidationError, self.attachments.create, self.req, FAKE_UUID1, body=body) - @mock.patch('nova.compute.api.API.attach_interface') @mock.patch('nova.compute.api.API.get', fake_get_instance) - def test_tagged_interface_attach_valid_tag(self, _): - body = {'interfaceAttachment': {'net_id': FAKE_NET_ID2, + def test_tagged_interface_attach_valid_tag(self): + body = {'interfaceAttachment': {'net_id': FAKE_NET_ID1, 'tag': 'foo'}} - with mock.patch.object(self.attachments, 'show'): + with mock.patch.object(self.attachments.network_api, 'show_port', + fake_show_port): self.attachments.create(self.req, FAKE_UUID1, body=body) diff --git a/nova/tests/unit/policies/test_attach_interfaces.py b/nova/tests/unit/policies/test_attach_interfaces.py index c8880df148..83d1e3080f 100644 --- a/nova/tests/unit/policies/test_attach_interfaces.py +++ b/nova/tests/unit/policies/test_attach_interfaces.py @@ -80,7 +80,12 @@ class AttachInterfacesPolicyTest(base.BasePolicyTest): "admin_state_up": True, "status": "ACTIVE", "mac_address": "bb:bb:bb:bb:bb:bb", - "fixed_ips": ["10.0.2.2"], + "fixed_ips": [ + { + "ip_address": "10.0.2.2", + "subnet_id": uuids.subnet_id, + }, + ], "device_id": server_id, }} self.common_policy_auth(self.project_reader_authorized_contexts, @@ -89,15 +94,29 @@ class AttachInterfacesPolicyTest(base.BasePolicyTest): self.req, server_id, port_id) @mock.patch('nova.compute.api.API.get') - @mock.patch('nova.api.openstack.compute.attach_interfaces' - '.InterfaceAttachmentController.show') + @mock.patch('nova.network.neutron.API.show_port') @mock.patch('nova.compute.api.API.attach_interface') def test_attach_interface(self, mock_interface, mock_port, mock_get): rule_name = "os_compute_api:os-attach-interfaces:create" + server_id = uuids.fake_id + mock_port.return_value = {'port': { + "id": uuids.port_id, + "network_id": uuids.fake_id, + "admin_state_up": True, + "status": "ACTIVE", + "mac_address": "bb:bb:bb:bb:bb:bb", + "fixed_ips": [ + { + "ip_address": "10.0.2.2", + "subnet_id": uuids.subnet_id, + }, + ], + "device_id": server_id, + }} body = {'interfaceAttachment': {'net_id': uuids.fake_id}} self.common_policy_auth(self.project_member_authorized_contexts, rule_name, self.controller.create, - self.req, uuids.fake_id, body=body) + self.req, server_id, body=body) @mock.patch('nova.compute.api.API.get') @mock.patch('nova.compute.api.API.detach_interface')