From 125c17465f3d8d15fc87f8776e63aebbc516ef6a Mon Sep 17 00:00:00 2001 From: Artom Lifshitz Date: Sun, 25 Sep 2016 10:00:43 -0400 Subject: [PATCH] API support for tagged device attachment This patch adds microversion 2.49, which supports tagged attachment of network interfaces and block devices. Change-Id: I8d3bbe7e9a21d2694d10ee89628deb333e6b0487 Implements: blueprint virt-device-tagged-attach-detach --- api-ref/source/os-interface.inc | 6 ++ api-ref/source/os-volume-attachments.inc | 6 ++ api-ref/source/parameters.yaml | 24 ++++++ .../v2.49/attach-interfaces-create-req.json | 6 ++ .../v2.49/attach-interfaces-create-resp.json | 14 ++++ .../v2.49/attach-volume-to-server-req.json | 6 ++ .../v2.49/attach-volume-to-server-resp.json | 8 ++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 3 +- .../openstack/compute/attach_interfaces.py | 7 +- .../compute/rest_api_version_history.rst | 15 ++++ .../compute/schemas/attach_interfaces.py | 6 ++ nova/api/openstack/compute/schemas/volumes.py | 3 + nova/api/openstack/compute/volumes.py | 6 +- .../attach-interfaces-create-req.json.tpl | 6 ++ .../attach-interfaces-create-resp.json.tpl | 14 ++++ .../attach-volume-to-server-req.json.tpl | 6 ++ .../attach-volume-to-server-resp.json.tpl | 8 ++ .../test_attach_interfaces.py | 73 ++++++++++++++++++- .../api_sample_tests/test_volumes.py | 30 ++++++++ .../compute/test_attach_interfaces.py | 52 +++++++++++-- .../api/openstack/compute/test_volumes.py | 48 +++++++++++- nova/virt/fake.py | 7 +- ...device-tagged-attach-53e214d3b3fdd183.yaml | 19 +++++ 25 files changed, 356 insertions(+), 21 deletions(-) create mode 100644 doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json create mode 100644 doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json create mode 100644 doc/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json create mode 100644 doc/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json.tpl create mode 100644 releasenotes/notes/virt-device-tagged-attach-53e214d3b3fdd183.yaml diff --git a/api-ref/source/os-interface.inc b/api-ref/source/os-interface.inc index 0ac8ee031346..62151cbc3ebc 100644 --- a/api-ref/source/os-interface.inc +++ b/api-ref/source/os-interface.inc @@ -71,6 +71,7 @@ Request - net_id: net_id - fixed_ips: fixed_ips - ip_address: ip_address_req + - tag: device_tag_nic_attachment **Example Create Interface: JSON request** @@ -84,6 +85,11 @@ Create interface with ``port_id``. .. literalinclude:: ../../doc/api_samples/os-attach-interfaces/attach-interfaces-create-req.json :language: javascript +**Example Create Tagged Interface (v2.49): JSON request** + +.. literalinclude:: ../../doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json + :language: javascript + Response -------- diff --git a/api-ref/source/os-volume-attachments.inc b/api-ref/source/os-volume-attachments.inc index 4ee52a60480e..9a3d7fd65386 100644 --- a/api-ref/source/os-volume-attachments.inc +++ b/api-ref/source/os-volume-attachments.inc @@ -67,12 +67,18 @@ Request - volumeAttachment: volumeAttachment_post - volumeId: volumeId - device: device + - tag: device_tag_bdm_attachment **Example Attach a volume to an instance: JSON request** .. literalinclude:: ../../doc/api_samples/os-volumes/attach-volume-to-server-req.json :language: javascript +**Example Attach a volume to an instance and tag it (v2.49): JSON request** + +.. literalinclude:: ../../doc/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json + :language: javascript + Response -------- diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 2e0672226fff..9cd638bb5d42 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1794,6 +1794,19 @@ device_tag_bdm: required: false type: string min_version: 2.32 +device_tag_bdm_attachment: + description: | + A device role tag that can be applied to a volume when attaching it to the + VM. The guest OS of a server that has devices tagged in this manner can + access hardware metadata about the tagged devices from the metadata API and + on the config drive, if enabled. + + .. note:: Tagged volume attachment is not supported for shelved-offloaded + instances. + in: body + required: false + type: string + min_version: 2.49 device_tag_nic: description: | A device role tag that can be applied to a network interface. The guest OS @@ -1807,6 +1820,17 @@ device_tag_nic: required: false type: string min_version: 2.32 +device_tag_nic_attachment: + description: | + A device role tag that can be applied to a network interface when attaching + it to the VM. The guest OS of a server that has devices tagged in this + manner can access hardware metadata about the tagged devices from the + metadata API and on the config + drive, if enabled. + in: body + required: false + type: string + min_version: 2.49 disabled_reason_body: description: | The reason for disabling a service. diff --git a/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json b/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json new file mode 100644 index 000000000000..977afc788cae --- /dev/null +++ b/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json @@ -0,0 +1,6 @@ +{ + "interfaceAttachment": { + "port_id": "ce531f90-199f-48c0-816c-13e38010b442", + "tag": "foo" + } +} diff --git a/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json b/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json new file mode 100644 index 000000000000..49f140448394 --- /dev/null +++ b/doc/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json @@ -0,0 +1,14 @@ +{ + "interfaceAttachment": { + "fixed_ips": [ + { + "ip_address": "192.168.1.3", + "subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef" + } + ], + "mac_addr": "fa:16:3e:4c:2c:30", + "net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6", + "port_id": "ce531f90-199f-48c0-816c-13e38010b442", + "port_state": "ACTIVE" + } +} diff --git a/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json b/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json new file mode 100644 index 000000000000..9f49b54d78cb --- /dev/null +++ b/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json @@ -0,0 +1,6 @@ +{ + "volumeAttachment": { + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "tag": "foo" + } +} diff --git a/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json b/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json new file mode 100644 index 000000000000..5f610bcaebea --- /dev/null +++ b/doc/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json @@ -0,0 +1,8 @@ +{ + "volumeAttachment": { + "device": "/dev/sdb", + "id": "a26887c6-c47b-4654-abb5-dfadf7d3f803", + "serverId": "84ffbfa0-daf4-4e23-bf4b-dc532c459d4e", + "volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803" + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 72c0930e1723..42afd5e0cadd 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.48", + "version": "2.49", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index a6fda6dbe89e..ef22bc396d4e 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.48", + "version": "2.49", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 58c0dd2216d0..03d07944373a 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -115,6 +115,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: the flavor extra-specs by policy, simply omit the field from the output. * 2.48 - Standardize VM diagnostics info. + * 2.49 - Support tagged attachment of network interfaces and block devices. """ # The minimum and maximum versions of the API supported @@ -123,7 +124,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.48" +_MAX_API_VERSION = "2.49" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which related to network, images and baremetal diff --git a/nova/api/openstack/compute/attach_interfaces.py b/nova/api/openstack/compute/attach_interfaces.py index e3341777b81b..c36c05759d0b 100644 --- a/nova/api/openstack/compute/attach_interfaces.py +++ b/nova/api/openstack/compute/attach_interfaces.py @@ -99,7 +99,8 @@ class InterfaceAttachmentController(wsgi.Controller): port_info['port'])} @extensions.expected_errors((400, 404, 409, 500, 501)) - @validation.schema(attach_interfaces.create) + @validation.schema(attach_interfaces.create, '2.0', '2.48') + @validation.schema(attach_interfaces.create_v249, '2.49') def create(self, req, server_id, body): """Attach an interface to an instance.""" context = req.environ['nova.context'] @@ -109,10 +110,12 @@ class InterfaceAttachmentController(wsgi.Controller): network_id = None port_id = None req_ip = None + tag = None if body: attachment = body['interfaceAttachment'] network_id = attachment.get('net_id', None) port_id = attachment.get('port_id', None) + tag = attachment.get('tag', None) try: req_ip = attachment['fixed_ips'][0]['ip_address'] except Exception: @@ -128,7 +131,7 @@ class InterfaceAttachmentController(wsgi.Controller): instance = common.get_instance(self.compute_api, context, server_id) try: vif = self.compute_api.attach_interface(context, - instance, network_id, port_id, req_ip) + instance, network_id, port_id, req_ip, tag=tag) except (exception.InterfaceAttachFailedNoNetwork, exception.NetworkAmbiguous, exception.NoMoreFixedIps, diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 349afdfb53c4..edcb0715d751 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -564,3 +564,18 @@ user documentation. standardized. It has a set of fields which each hypervisor will try to fill. If a hypervisor driver is unable to provide a specific field then this field will be reported as 'None'. + +2.49 +---- + + Continuing from device role tagging at server create time introduced in + version 2.32 and later fixed in 2.42, microversion 2.49 allows the attachment + of network interfaces and volumes with an optional ``tag`` parameter. This tag + is used to identify the virtual devices in the guest and is exposed in the + metadata API. Because the config drive cannot be updated while the guest is + running, it will only contain metadata of devices that were tagged at boot + time. Any changes made to devices while the instance is running - be it + detaching a tagged device or performing a tagged device attachment - will not + be reflected in the config drive. + + Tagged volume attachment is not supported for shelved-offloaded instances. diff --git a/nova/api/openstack/compute/schemas/attach_interfaces.py b/nova/api/openstack/compute/schemas/attach_interfaces.py index fa966181d167..1c06887dd3d5 100644 --- a/nova/api/openstack/compute/schemas/attach_interfaces.py +++ b/nova/api/openstack/compute/schemas/attach_interfaces.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from nova.api.validation import parameter_types @@ -44,3 +46,7 @@ create = { }, 'additionalProperties': False, } + +create_v249 = copy.deepcopy(create) +create_v249['properties']['interfaceAttachment'][ + 'properties']['tag'] = parameter_types.tag diff --git a/nova/api/openstack/compute/schemas/volumes.py b/nova/api/openstack/compute/schemas/volumes.py index f70ff08456ca..b08ea5f8e51c 100644 --- a/nova/api/openstack/compute/schemas/volumes.py +++ b/nova/api/openstack/compute/schemas/volumes.py @@ -83,6 +83,9 @@ create_volume_attachment = { 'required': ['volumeAttachment'], 'additionalProperties': False, } +create_volume_attachment_v249 = copy.deepcopy(create_volume_attachment) +create_volume_attachment_v249['properties']['volumeAttachment'][ + 'properties']['tag'] = parameter_types.tag update_volume_attachment = copy.deepcopy(create_volume_attachment) del update_volume_attachment['properties']['volumeAttachment'][ diff --git a/nova/api/openstack/compute/volumes.py b/nova/api/openstack/compute/volumes.py index a0908c361f0b..a8c539f81915 100644 --- a/nova/api/openstack/compute/volumes.py +++ b/nova/api/openstack/compute/volumes.py @@ -317,7 +317,8 @@ class VolumeAttachmentController(wsgi.Controller): # TODO(mriedem): This API should return a 202 instead of a 200 response. @extensions.expected_errors((400, 404, 409)) - @validation.schema(volumes_schema.create_volume_attachment) + @validation.schema(volumes_schema.create_volume_attachment, '2.0', '2.48') + @validation.schema(volumes_schema.create_volume_attachment_v249, '2.49') def create(self, req, server_id, body): """Attach a volume to an instance.""" context = req.environ['nova.context'] @@ -325,6 +326,7 @@ class VolumeAttachmentController(wsgi.Controller): volume_id = body['volumeAttachment']['volumeId'] device = body['volumeAttachment'].get('device') + tag = body['volumeAttachment'].get('tag') instance = common.get_instance(self.compute_api, context, server_id) @@ -335,7 +337,7 @@ class VolumeAttachmentController(wsgi.Controller): try: device = self.compute_api.attach_volume(context, instance, - volume_id, device) + volume_id, device, tag=tag) except (exception.InstanceUnknownCell, exception.VolumeNotFound) as e: raise exc.HTTPNotFound(explanation=e.format_message()) diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json.tpl new file mode 100644 index 000000000000..977afc788cae --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-req.json.tpl @@ -0,0 +1,6 @@ +{ + "interfaceAttachment": { + "port_id": "ce531f90-199f-48c0-816c-13e38010b442", + "tag": "foo" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json.tpl new file mode 100644 index 000000000000..9dff234366fa --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-attach-interfaces/v2.49/attach-interfaces-create-resp.json.tpl @@ -0,0 +1,14 @@ +{ + "interfaceAttachment": { + "fixed_ips": [ + { + "ip_address": "192.168.1.3", + "subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef" + } + ], + "mac_addr": "fa:16:3e:4c:2c:30", + "net_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6", + "port_id": "ce531f90-199f-48c0-816c-13e38010b442", + "port_state": "ACTIVE" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json.tpl new file mode 100644 index 000000000000..770be1f4b6b3 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-req.json.tpl @@ -0,0 +1,6 @@ +{ + "volumeAttachment": { + "volumeId": "%(volume_id)s", + "tag": "%(tag)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json.tpl new file mode 100644 index 000000000000..4730b3c197c3 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-volumes/v2.49/attach-volume-to-server-resp.json.tpl @@ -0,0 +1,8 @@ +{ + "volumeAttachment": { + "device": "%(device)s", + "id": "%(volume_id)s", + "serverId": "%(uuid)s", + "volumeId": "%(volume_id)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_attach_interfaces.py b/nova/tests/functional/api_sample_tests/test_attach_interfaces.py index f625783ea2ae..7bdb6dc8d3fa 100644 --- a/nova/tests/functional/api_sample_tests/test_attach_interfaces.py +++ b/nova/tests/functional/api_sample_tests/test_attach_interfaces.py @@ -68,7 +68,7 @@ class AttachInterfacesSampleJsonTest(test_servers.ServersSampleBase): def fake_attach_interface(self, context, instance, network_id, port_id, - requested_ip='192.168.1.3'): + requested_ip='192.168.1.3', tag=None): if not network_id: network_id = "fake_net_uuid" if not port_id: @@ -180,3 +180,74 @@ class AttachInterfacesSampleJsonTest(test_servers.ServersSampleBase): (instance_uuid, port_id)) self.assertEqual(202, response.status_code) self.assertEqual('', response.text) + + +class AttachInterfacesSampleV249JsonTest(test_servers.ServersSampleBase): + sample_dir = 'os-attach-interfaces' + microversion = '2.49' + scenarios = [('v2_49', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(AttachInterfacesSampleV249JsonTest, self).setUp() + + def fake_show_port(self, context, port_id=None): + if not port_id: + raise exception.PortNotFound(port_id=None) + port_data = { + "id": port_id, + "network_id": "3cb9bc59-5699-4588-a4b1-b87f96708bc6", + "admin_state_up": True, + "status": "ACTIVE", + "mac_address": "fa:16:3e:4c:2c:30", + "fixed_ips": [ + { + "ip_address": "192.168.1.3", + "subnet_id": "f8a6e8f8-c2ec-497c-9f23-da9616de54ef" + } + ], + "device_id": 'bece68a3-2f8b-4e66-9092-244493d6aba7', + } + port = {'port': port_data} + return port + + def fake_attach_interface(self, context, instance, + network_id, port_id, + requested_ip='192.168.1.3', tag=None): + if not network_id: + network_id = "fake_net_uuid" + if not port_id: + port_id = "fake_port_uuid" + vif = fake_network_cache_model.new_vif() + vif['id'] = port_id + vif['network']['id'] = network_id + vif['network']['subnets'][0]['ips'][0] = requested_ip + vif['tag'] = tag + return vif + + self.stub_out('nova.network.api.API.show_port', fake_show_port) + self.stub_out('nova.compute.api.API.attach_interface', + fake_attach_interface) + + def _stub_show_for_instance(self, instance_uuid, port_id): + show_port = network_api.API().show_port(None, port_id) + show_port['port']['device_id'] = instance_uuid + self.stub_out('nova.network.api.API.show_port', + lambda *a, **k: show_port) + + def test_create_interfaces(self, instance_uuid=None): + if instance_uuid is None: + instance_uuid = self._post_server() + subs = { + 'net_id': '3cb9bc59-5699-4588-a4b1-b87f96708bc6', + 'port_id': 'ce531f90-199f-48c0-816c-13e38010b442', + 'subnet_id': 'f8a6e8f8-c2ec-497c-9f23-da9616de54ef', + 'ip_address': '192.168.1.3', + 'port_state': 'ACTIVE', + 'mac_addr': 'fa:16:3e:4c:2c:30', + } + self._stub_show_for_instance(instance_uuid, subs['port_id']) + response = self._do_post('servers/%s/os-interface' + % instance_uuid, + 'attach-interfaces-create-req', subs) + self._verify_response('attach-interfaces-create-resp', subs, + response, 200) diff --git a/nova/tests/functional/api_sample_tests/test_volumes.py b/nova/tests/functional/api_sample_tests/test_volumes.py index 36004e05343f..3bb36ed667db 100644 --- a/nova/tests/functional/api_sample_tests/test_volumes.py +++ b/nova/tests/functional/api_sample_tests/test_volumes.py @@ -17,6 +17,7 @@ import datetime from nova import context from nova import objects +from nova.tests import fixtures from nova.tests.functional.api_sample_tests import api_sample_base from nova.tests.functional.api_sample_tests import test_servers from nova.tests.unit.api.openstack import fakes @@ -306,3 +307,32 @@ class VolumeAttachmentsSample(test_servers.ServersSampleBase): subs) self.assertEqual(202, response.status_code) self.assertEqual('', response.text) + + +class VolumeAttachmentsSampleV249(test_servers.ServersSampleBase): + sample_dir = "os-volumes" + microversion = '2.49' + scenarios = [('v2_49', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(VolumeAttachmentsSampleV249, self).setUp() + self.useFixture(fixtures.CinderFixture(self)) + + def test_attach_volume_to_server(self): + device_name = '/dev/sdb' + bdm = objects.BlockDeviceMapping() + bdm['device_name'] = device_name + volume = fakes.stub_volume_get(None, context.get_admin_context(), + 'a26887c6-c47b-4654-abb5-dfadf7d3f803') + subs = { + 'volume_id': volume['id'], + 'device': device_name, + 'tag': 'foo', + } + server_id = self._post_server() + response = self._do_post('servers/%s/os-volume_attachments' + % server_id, + 'attach-volume-to-server-req', subs) + + self._verify_response('attach-volume-to-server-resp', subs, + response, 200) 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 8e8f5f34d8a8..3b13cd78e0d0 100644 --- a/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py +++ b/nova/tests/unit/api/openstack/compute/test_attach_interfaces.py @@ -83,7 +83,7 @@ def fake_show_port(context, port_id, **kwargs): def fake_attach_interface(self, context, instance, network_id, port_id, - requested_ip='192.168.1.3'): + requested_ip='192.168.1.3', tag=None): if not network_id: # if no network_id is given when add a port to an instance, use the # first default network. @@ -222,7 +222,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): def test_attach_interface_instance_locked(self): def fake_attach_interface_to_locked_server(self, context, - instance, network_id, port_id, requested_ip): + instance, network_id, port_id, requested_ip, tag=None): raise exception.InstanceIsLocked(instance_uuid=FAKE_UUID1) self.stub_out('nova.compute.api.API.attach_interface', @@ -355,7 +355,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): body=body) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -374,7 +374,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): body=body) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -394,7 +394,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): body=body) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -410,7 +410,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): self.req, FAKE_UUID1, body={}) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -429,7 +429,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): body=body) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -446,7 +446,7 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): self.req, FAKE_UUID1, body={}) ctxt = self.req.environ['nova.context'] attach_mock.assert_called_once_with(ctxt, fake_instance, None, - None, None) + None, None, tag=None) get_mock.assert_called_once_with(ctxt, FAKE_UUID1, expected_attrs=None) @@ -471,6 +471,42 @@ class InterfaceAttachTestsV21(test.NoDBTestCase): self._test_attach_interface_with_invalid_parameter(param) +class InterfaceAttachTestsV249(test.NoDBTestCase): + controller_cls = attach_interfaces_v21.InterfaceAttachmentController + + def setUp(self): + super(InterfaceAttachTestsV249, self).setUp() + self.attachments = self.controller_cls() + self.req = fakes.HTTPRequest.blank('', version='2.49') + + def test_tagged_interface_attach_invalid_tag_comma(self): + body = {'interfaceAttachment': {'net_id': FAKE_NET_ID2, + 'tag': ','}} + self.assertRaises(exception.ValidationError, self.attachments.create, + self.req, FAKE_UUID1, body=body) + + def test_tagged_interface_attach_invalid_tag_slash(self): + body = {'interfaceAttachment': {'net_id': FAKE_NET_ID2, + 'tag': '/'}} + self.assertRaises(exception.ValidationError, self.attachments.create, + self.req, FAKE_UUID1, body=body) + + def test_tagged_interface_attach_invalid_tag_too_long(self): + tag = ''.join(map(str, range(10, 41))) + body = {'interfaceAttachment': {'net_id': FAKE_NET_ID2, + 'tag': tag}} + 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, + 'tag': 'foo'}} + with mock.patch.object(self.attachments, 'show'): + self.attachments.create(self.req, FAKE_UUID1, body=body) + + class AttachInterfacesPolicyEnforcementv21(test.NoDBTestCase): def setUp(self): diff --git a/nova/tests/unit/api/openstack/compute/test_volumes.py b/nova/tests/unit/api/openstack/compute/test_volumes.py index 9f22866a4bf2..bb319b578ff8 100644 --- a/nova/tests/unit/api/openstack/compute/test_volumes.py +++ b/nova/tests/unit/api/openstack/compute/test_volumes.py @@ -63,7 +63,7 @@ def fake_get_volume(self, context, id): } -def fake_attach_volume(self, context, instance, volume_id, device): +def fake_attach_volume(self, context, instance, volume_id, device, tag=None): pass @@ -494,7 +494,7 @@ class VolumeAttachTestsV21(test.NoDBTestCase): @mock.patch.object(compute_api.API, 'attach_volume', side_effect=exception.VolumeTaggedAttachNotSupported()) - def test_attach_volume_not_supported(self, mock_attach_volume): + def test_tagged_volume_attach_not_supported(self, mock_attach_volume): body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, 'device': '/dev/fake'}} self.assertRaises(webob.exc.HTTPBadRequest, self.attachments.create, @@ -551,7 +551,8 @@ class VolumeAttachTestsV21(test.NoDBTestCase): def test_attach_volume_to_locked_server(self): def fake_attach_volume_to_locked_server(self, context, instance, - volume_id, device=None): + volume_id, device=None, + tag=None): raise exception.InstanceIsLocked(instance_uuid=instance['uuid']) self.stubs.Set(compute_api.API, @@ -688,6 +689,47 @@ class VolumeAttachTestsV21(test.NoDBTestCase): body=body) +class VolumeAttachTestsV249(test.NoDBTestCase): + validation_error = exception.ValidationError + + def setUp(self): + super(VolumeAttachTestsV249, self).setUp() + self.attachments = volumes_v21.VolumeAttachmentController() + self.req = fakes.HTTPRequest.blank( + '/v2/servers/id/os-volume_attachments/uuid', + version='2.49') + + def test_tagged_volume_attach_invalid_tag_comma(self): + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake', + 'tag': ','}} + self.assertRaises(exception.ValidationError, self.attachments.create, + self.req, FAKE_UUID, body=body) + + def test_tagged_volume_attach_invalid_tag_slash(self): + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake', + 'tag': '/'}} + self.assertRaises(exception.ValidationError, self.attachments.create, + self.req, FAKE_UUID, body=body) + + def test_tagged_volume_attach_invalid_tag_too_long(self): + tag = ''.join(map(str, range(10, 41))) + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake', + 'tag': tag}} + self.assertRaises(exception.ValidationError, self.attachments.create, + self.req, FAKE_UUID, body=body) + + @mock.patch('nova.compute.api.API.attach_volume') + @mock.patch('nova.compute.api.API.get', fake_get_instance) + def test_tagged_volume_attach_valid_tag(self, _): + body = {'volumeAttachment': {'volumeId': FAKE_UUID_A, + 'device': '/dev/fake', + 'tag': 'foo'}} + self.attachments.create(self.req, FAKE_UUID, body=body) + + class CommonBadRequestTestCase(object): resource = None diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 80a9cb39f66a..5b551187010b 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -121,8 +121,11 @@ class FakeDriver(driver.ComputeDriver): capabilities = { "has_imagecache": True, "supports_recreate": True, - "supports_migrate_to_same_host": True - } + "supports_migrate_to_same_host": True, + "supports_attach_interface": True, + "supports_tagged_attach_interface": True, + "supports_tagged_attach_volume": True + } # Since we don't have a real hypervisor, pretend we have lots of # disk and ram so this driver can be used to test large instances. diff --git a/releasenotes/notes/virt-device-tagged-attach-53e214d3b3fdd183.yaml b/releasenotes/notes/virt-device-tagged-attach-53e214d3b3fdd183.yaml new file mode 100644 index 000000000000..5a6994b9d54c --- /dev/null +++ b/releasenotes/notes/virt-device-tagged-attach-53e214d3b3fdd183.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Microversion 2.49 brings device role tagging to the attach operation of + volumes and network interfaces. Both network interfaces and volumes can now + be attached with an optional ``tag`` parameter. The tag is then exposed to + the guest operating system through the metadata API. Unlike the original + device role tagging feature, tagged attach does not support the config + drive. Because the config drive was never designed to be dynamic, it only + contains device tags that were set at boot time with API 2.32. Any changes + made to tagged devices with API 2.49 while the server is running will only + be reflected in the metadata obtained from the metadata API. Because of + metadata caching, changes may take up to ``metadata_cache_expiration`` to + appear in the metadata API. The default value for + ``metadata_cache_expiration`` is 15 seconds. + + Tagged volume attachment is not supported for shelved-offloaded instances. + Tagged device attachment (both volumes and network interfaces) is not + supported for Cells V1 deployments.