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:
parent
7d428ac24a
commit
125c17465f
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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.
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"interfaceAttachment": {
|
||||
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
|
||||
"tag": "foo"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "a26887c6-c47b-4654-abb5-dfadf7d3f803",
|
||||
"tag": "foo"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.48",
|
||||
"version": "2.49",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.48",
|
||||
"version": "2.49",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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'][
|
||||
|
@ -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())
|
||||
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"interfaceAttachment": {
|
||||
"port_id": "ce531f90-199f-48c0-816c-13e38010b442",
|
||||
"tag": "foo"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"volumeId": "%(volume_id)s",
|
||||
"tag": "%(tag)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"volumeAttachment": {
|
||||
"device": "%(device)s",
|
||||
"id": "%(volume_id)s",
|
||||
"serverId": "%(uuid)s",
|
||||
"volumeId": "%(volume_id)s"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user