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
This commit is contained in:
Artom Lifshitz 2016-09-25 10:00:43 -04:00
parent 7d428ac24a
commit 125c17465f
25 changed files with 356 additions and 21 deletions

View File

@ -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
--------

View File

@ -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
--------

View File

@ -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.

View File

@ -0,0 +1,6 @@
{
"interfaceAttachment": {
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
"tag": "foo"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"volumeAttachment": {
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
"tag": "foo"
}
}

View File

@ -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"
}
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.48",
"version": "2.49",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.48",
"version": "2.49",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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'][

View File

@ -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())

View File

@ -0,0 +1,6 @@
{
"interfaceAttachment": {
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
"tag": "foo"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"volumeAttachment": {
"volumeId": "%(volume_id)s",
"tag": "%(tag)s"
}
}

View File

@ -0,0 +1,8 @@
{
"volumeAttachment": {
"device": "%(device)s",
"id": "%(volume_id)s",
"serverId": "%(uuid)s",
"volumeId": "%(volume_id)s"
}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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.

View File

@ -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.