Servers API for the new BDM format

This patch makes the nova API aware and able to accept the new block
device mapping format introduced in
If30afdb59d4c4268b97d3d10270df2cc729a0c4c when booting an instance.

It does so by introducing a new extension into the v2 API. There is no
v3 extension as part of this patch because volume extension is going
away in v3 and thus this functionality can be part of the core servers
extension. This will be done in a subsequent patch.

The compute API create method will still convert these back to the
legacy format for the time being until the compute API will know how to
take advantage of the new format.

As this change adds the new API extension, marking it as DocImpact so
that the changes and the API data format can be documented.

blueprint: improve-block-device-handling
Change-Id: I2c1b63e41deca26f727fb9ed912a55494db9c76c
This commit is contained in:
Nikola Dipanov 2013-07-18 16:55:52 +02:00
parent 84b730b70f
commit 8c3475706d
21 changed files with 589 additions and 57 deletions

View File

@ -64,14 +64,6 @@
"namespace": "http://docs.openstack.org/compute/ext/extended_status/api/v1.1",
"updated": "2011-11-03T00:00:00+00:00"
},
{
"alias": "os-extended-volumes",
"description": "Extended Volumes support.",
"links": [],
"name": "ExtendedVolumes",
"namespace": "http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1",
"updated": "2013-06-07T00:00:00+00:00"
},
{
"alias": "OS-EXT-VIF-NET",
"description": "Adds network id parameter to the virtual interface list.",
@ -160,6 +152,14 @@
"namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2",
"updated": "2013-01-04T00:00:00+00:00"
},
{
"alias": "os-block-device-mapping-v2-boot",
"description": "Allow boot with the new BDM data format.",
"links": [],
"name": "BlockDeviceMappingV2Boot",
"namespace": "http://docs.openstack.org/compute/ext/block_device_mapping_v2_boot/api/v2",
"updated": "2013-07-08T00:00:00+00:00"
},
{
"alias": "os-cell-capacities",
"description": "Adding functionality to get cell capacities.",
@ -280,6 +280,14 @@
"namespace": "http://docs.openstack.org/compute/ext/extended_services/api/v2",
"updated": "2013-05-17T00:00:00-00:00"
},
{
"alias": "os-extended-volumes",
"description": "Extended Volumes support.",
"links": [],
"name": "ExtendedVolumes",
"namespace": "http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1",
"updated": "2013-06-07T00:00:00+00:00"
},
{
"alias": "os-fixed-ips",
"description": "Fixed IPs support.",
@ -416,6 +424,14 @@
"namespace": "http://docs.openstack.org/compute/ext/keypairs/api/v1.1",
"updated": "2011-08-08T00:00:00+00:00"
},
{
"alias": "os-migrations",
"description": "Provide data on migrations.",
"links": [],
"name": "Migrations",
"namespace": "http://docs.openstack.org/compute/ext/migrations/api/v2.0",
"updated": "2013-05-30T00:00:00+00:00"
},
{
"alias": "os-multiple-create",
"description": "Allow multiple create in the Create Server v1.1 API.",
@ -591,14 +607,6 @@
"name": "Volumes",
"namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1",
"updated": "2011-03-25T00:00:00+00:00"
},
{
"alias": "os-migrations",
"description": "Provide data on migrations.",
"links": [],
"name": "Migrations",
"namespace": "http://docs.openstack.org/compute/ext/migrations/api/v2.0",
"updated": "2013-05-30T00:00:00+00:00"
}
]
}

View File

@ -24,9 +24,6 @@
<extension alias="OS-EXT-STS" updated="2011-11-03T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_status/api/v1.1" name="ExtendedStatus">
<description>Extended Status support.</description>
</extension>
<extension alias="os-extended-volumes" updated="2013-06-07T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1" name="ExtendedVolumes">
<description>Extended Volumes support.</description>
</extension>
<extension alias="OS-EXT-VIF-NET" updated="2013-03-07T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended-virtual-interfaces-net/api/v1.1" name="ExtendedVIFNet">
<description>Adds network id parameter to the virtual interface list.</description>
</extension>
@ -66,6 +63,9 @@
<extension alias="os-baremetal-nodes" updated="2013-01-04T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2" name="BareMetalNodes">
<description>Admin-only bare-metal node administration.</description>
</extension>
<extension alias="os-block-device-mapping-v2-boot" updated="2013-07-08T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/block_device_mapping_v2_boot/api/v2" name="BlockDeviceMappingV2Boot">
<description>Allow boot with the new BDM data format.</description>
</extension>
<extension alias="os-cell-capacities" updated="2013-05-27T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/cell_capacities/api/v1.1" name="CellCapacities">
<description>Adding functionality to get cell capacities.</description>
</extension>
@ -123,6 +123,9 @@
<extension alias="os-extended-services" updated="2013-05-17T00:00:00-00:00" namespace="http://docs.openstack.org/compute/ext/extended_services/api/v2" name="ExtendedServices">
<description>Extended services support.</description>
</extension>
<extension alias="os-extended-volumes" updated="2013-06-07T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/extended_volumes/api/v1.1" name="ExtendedVolumes">
<description>Extended Volumes support.</description>
</extension>
<extension alias="os-fixed-ips" updated="2012-10-18T13:25:27-06:00" namespace="http://docs.openstack.org/compute/ext/fixed_ips/api/v2" name="FixedIPs">
<description>Fixed IPs support.</description>
</extension>
@ -176,6 +179,9 @@
<extension alias="os-keypairs" updated="2011-08-08T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/keypairs/api/v1.1" name="Keypairs">
<description>Keypair Support.</description>
</extension>
<extension alias="os-migrations" updated="2013-05-30T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/migrations/api/v2.0" name="Migrations">
<description>Provide data on migrations.</description>
</extension>
<extension alias="os-multiple-create" updated="2012-08-07T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/multiplecreate/api/v1.1" name="MultipleCreate">
<description>Allow multiple create in the Create Server v1.1 API.</description>
</extension>
@ -242,8 +248,4 @@
<extension alias="os-volumes" updated="2011-03-25T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes">
<description>Volumes support.</description>
</extension>
<extension alias="os-migrations" updated="2013-05-30T00:00:00+00:00"
namespace="http://docs.openstack.org/compute/ext/migrations/api/v2.0" name="Migrations">
<description>Provide data on migrations.</description>
</extension>
</extensions>

View File

@ -0,0 +1,33 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
],
"block_device_mapping_v2": [
{
"device_name": "/dev/sdb1",
"source_type": "blank",
"destination_type": "local",
"delete_on_termination": "True",
"guest_format": "swap",
"boot_index": "-1"
},
{
"device_name": "/dev/sda1",
"source_type": "volume",
"destination_type": "volume",
"uuid": "fake-volume-id-1",
"boot_index": "0"
}
]
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b" flavorRef="http://openstack.example.com/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
<block_device_mapping_v2>
<mapping device_name="/dev/sdb1" source_type="blank" destination_type="local" delete_on_termination="True" guest_format="swap" boot_index="-1"></mapping>
<mapping device_name="/dev/sda1" source_type="volume" destination_type="volume" uuid="fake-volume-id-1" boot_index="0"></mapping>
</block_device_mapping_v2>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "N4x7wFX6iN8D",
"id": "babd1af0-4fc6-4529-b32f-aad69811ccf5",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/babd1af0-4fc6-4529-b32f-aad69811ccf5",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/babd1af0-4fc6-4529-b32f-aad69811ccf5",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="fdbce07b-097e-4ab1-8141-b1c847861aa1" adminPass="zA62GVkFvN74">
<metadata/>
<atom:link href="http://openstack.example.com/v2/openstack/servers/fdbce07b-097e-4ab1-8141-b1c847861aa1" rel="self"/>
<atom:link href="http://openstack.example.com/openstack/servers/fdbce07b-097e-4ab1-8141-b1c847861aa1" rel="bookmark"/>
</server>

View File

@ -0,0 +1,23 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.api.openstack import extensions
class Block_device_mapping_v2_boot(extensions.ExtensionDescriptor):
"""Allow boot with the new BDM data format."""
name = "BlockDeviceMappingV2Boot"
alias = "os-block-device-mapping-v2-boot"
namespace = ("http://docs.openstack.org/compute/ext/"
"block_device_mapping_v2_boot/api/v2")
updated = "2013-07-08T00:00:00+00:00"

View File

@ -27,6 +27,7 @@ from nova.api.openstack.compute import ips
from nova.api.openstack.compute.views import servers as views_servers
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import block_device
from nova import compute
from nova.compute import flavors
from nova import exception
@ -219,6 +220,11 @@ class CommonDeserializer(wsgi.MetadataXMLDeserializer):
if block_device_mapping is not None:
server["block_device_mapping"] = block_device_mapping
block_device_mapping_v2 = self._extract_block_device_mapping_v2(
server_node)
if block_device_mapping_v2 is not None:
server["block_device_mapping_v2"] = block_device_mapping_v2
# NOTE(vish): Support this incorrect version because it was in the code
# base for a while and we don't want to accidentally break
# anyone that might be using it.
@ -261,6 +267,21 @@ class CommonDeserializer(wsgi.MetadataXMLDeserializer):
else:
return None
def _extract_block_device_mapping_v2(self, server_node):
"""Marshal the new block_device_mappings."""
node = self.find_first_child_named(server_node,
"block_device_mapping_v2")
if node:
block_device_mapping = []
for child in self.extract_elements(node):
if child.nodeName != "mapping":
continue
block_device_mapping.append(
dict((attr, child.getAttribute(attr))
for attr in block_device.bdm_new_api_fields
if child.getAttribute(attr)))
return block_device_mapping
def _extract_scheduler_hints(self, server_node):
"""Marshal the scheduler hints attribute of a parsed request."""
node = self.find_first_child_named_in_namespace(server_node,
@ -831,6 +852,8 @@ class Controller(wsgi.Controller):
availability_zone = server_dict.get('availability_zone')
block_device_mapping = None
block_device_mapping_v2 = None
legacy_bdm = True
if self.ext_mgr.is_loaded('os-volumes'):
block_device_mapping = server_dict.get('block_device_mapping', [])
for bdm in block_device_mapping:
@ -842,6 +865,28 @@ class Controller(wsgi.Controller):
bdm['delete_on_termination'] = strutils.bool_from_string(
bdm['delete_on_termination'])
if self.ext_mgr.is_loaded('os-block-device-mapping-v2-boot'):
# Consider the new data format for block device mapping
block_device_mapping_v2 = server_dict.get(
'block_device_mapping_v2', [])
# NOTE (ndipanov): Disable usage of both legacy and new
# block device format in the same request
if block_device_mapping and block_device_mapping_v2:
expl = _('Using different block_device_mapping syntaxes '
'is not allowed in the same request.')
raise exc.HTTPBadRequest(explanation=expl)
# Assume legacy format
legacy_bdm = not bool(block_device_mapping_v2)
# This will also do basic validation of block_devices
block_device_mapping_v2 = [
block_device.BlockDeviceDict.from_api(bdm_dict)
for bdm_dict in block_device_mapping_v2]
block_device_mapping = (block_device_mapping or
block_device_mapping_v2)
ret_resv_id = False
# min_count and max_count are optional. If they exist, they may come
# in as strings. Verify that they are valid integers and > 0.
@ -898,7 +943,8 @@ class Controller(wsgi.Controller):
config_drive=config_drive,
block_device_mapping=block_device_mapping,
auto_disk_config=auto_disk_config,
scheduler_hints=scheduler_hints)
scheduler_hints=scheduler_hints,
legacy_bdm=legacy_bdm)
except exception.QuotaError as error:
raise exc.HTTPRequestEntityTooLarge(
explanation=error.format_message(),
@ -1145,8 +1191,12 @@ class Controller(wsgi.Controller):
"""
image_ref = data['server'].get('imageRef')
bdm = data['server'].get('block_device_mapping')
bdm_v2 = data['server'].get('block_device_mapping_v2')
if not image_ref and bdm and self.ext_mgr.is_loaded('os-volumes'):
if (not image_ref and (
(bdm and self.ext_mgr.is_loaded('os-volumes')) or
(bdm_v2 and
self.ext_mgr.is_loaded('os-block-device-mapping-v2-boot')))):
return ''
else:
image_href = self._image_ref_from_req_data(data)

View File

@ -54,6 +54,17 @@ bdm_db_inherited_fields = set(['created_at', 'updated_at',
'deleted_at', 'deleted'])
bdm_new_non_api_fields = set(['volume_id', 'snapshot_id',
'image_id', 'connection_info'])
bdm_new_api_only_fields = set(['uuid'])
bdm_new_api_fields = ((bdm_new_fields - bdm_new_non_api_fields) |
bdm_new_api_only_fields)
class BlockDeviceDict(dict):
"""Represents a Block Device Mapping in Nova."""
@ -126,6 +137,27 @@ class BlockDeviceDict(dict):
return cls(new_bdm, non_computable_fields)
@classmethod
def from_api(cls, api_dict):
"""Transform the API format of data to the internally used one.
Only validate if the source_type field makes sense.
"""
if not api_dict.get('no_device'):
source_type = api_dict.get('source_type')
device_uuid = api_dict.get('uuid')
if source_type not in ('volume', 'image', 'snapshot', 'blank'):
raise exception.InvalidBDMFormat()
elif source_type != 'blank':
if not device_uuid:
raise exception.InvalidBDMFormat()
api_dict[source_type + '_id'] = device_uuid
api_dict.pop('uuid', None)
return cls(api_dict)
def legacy(self):
copy_over_fields = bdm_legacy_fields - set(['virtual_name'])
copy_over_fields |= (bdm_db_only_fields |

View File

@ -1098,7 +1098,7 @@ class API(base.Base):
injected_files=None, admin_password=None,
block_device_mapping=None, access_ip_v4=None,
access_ip_v6=None, requested_networks=None, config_drive=None,
auto_disk_config=None, scheduler_hints=None):
auto_disk_config=None, scheduler_hints=None, legacy_bdm=True):
"""
Provision instances, sending instance information to the
scheduler. The scheduler will determine where the instance(s)
@ -1107,6 +1107,15 @@ class API(base.Base):
Returns a tuple of (instances, reservation_id)
"""
# NOTE(ndipanov): Deal with block_device_mapping format before
# doing any work. We assume that the API class
# will be dealing with only one version of block
# devices whenever possible to keep the code
# simple. Currently this is the legacy format.
if block_device_mapping and not legacy_bdm:
block_device_mapping = block_device.legacy_mapping(
block_device_mapping)
self._check_create_policies(context, availability_zone,
requested_networks, block_device_mapping)

View File

@ -44,28 +44,6 @@ FAKE_UUID_D = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
IMAGE_UUID = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'
def fake_compute_api_create(cls, context, instance_type, image_href, **kwargs):
global _block_device_mapping_seen
_block_device_mapping_seen = kwargs.get('block_device_mapping')
inst_type = flavors.get_flavor_by_flavor_id(2)
resv_id = None
return ([{'id': 1,
'display_name': 'test_server',
'uuid': FAKE_UUID,
'instance_type': dict(inst_type),
'access_ip_v4': '1.2.3.4',
'access_ip_v6': 'fead::1234',
'image_ref': IMAGE_UUID,
'user_id': 'fake',
'project_id': 'fake',
'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0),
'updated_at': datetime.datetime(2010, 11, 11, 11, 0, 0),
'progress': 0,
'fixed_ips': []
}], resv_id)
def fake_get_instance(self, context, instance_id, want_objects=False):
return {'uuid': instance_id}
@ -120,12 +98,40 @@ class BootFromVolumeTest(test.TestCase):
def setUp(self):
super(BootFromVolumeTest, self).setUp()
self.stubs.Set(compute_api.API, 'create', fake_compute_api_create)
self.stubs.Set(compute_api.API, 'create',
self._get_fake_compute_api_create())
fakes.stub_out_nw_api(self.stubs)
self._block_device_mapping_seen = None
self._legacy_bdm_seen = True
self.flags(
osapi_compute_extension=[
'nova.api.openstack.compute.contrib.select_extensions'],
osapi_compute_ext_list=['Volumes'])
osapi_compute_ext_list=['Volumes', 'Block_device_mapping_v2_boot'])
def _get_fake_compute_api_create(self):
def _fake_compute_api_create(cls, context, instance_type,
image_href, **kwargs):
self._block_device_mapping_seen = kwargs.get(
'block_device_mapping')
self._legacy_bdm_seen = kwargs.get('legacy_bdm')
inst_type = flavors.get_flavor_by_flavor_id(2)
resv_id = None
return ([{'id': 1,
'display_name': 'test_server',
'uuid': FAKE_UUID,
'instance_type': dict(inst_type),
'access_ip_v4': '1.2.3.4',
'access_ip_v6': 'fead::1234',
'image_ref': IMAGE_UUID,
'user_id': 'fake',
'project_id': 'fake',
'created_at': datetime.datetime(2010, 10, 10, 12, 0, 0),
'updated_at': datetime.datetime(2010, 11, 11, 11, 0, 0),
'progress': 0,
'fixed_ips': []
}], resv_id)
return _fake_compute_api_create
def test_create_root_volume(self):
body = dict(server=dict(
@ -138,8 +144,6 @@ class BootFromVolumeTest(test.TestCase):
delete_on_termination=False,
)]
))
global _block_device_mapping_seen
_block_device_mapping_seen = None
req = webob.Request.blank('/v2/fake/os-volumes_boot')
req.method = 'POST'
req.body = jsonutils.dumps(body)
@ -150,9 +154,40 @@ class BootFromVolumeTest(test.TestCase):
server = jsonutils.loads(res.body)['server']
self.assertEqual(FAKE_UUID, server['id'])
self.assertEqual(CONF.password_length, len(server['adminPass']))
self.assertEqual(len(_block_device_mapping_seen), 1)
self.assertEqual(_block_device_mapping_seen[0]['volume_id'], 1)
self.assertEqual(_block_device_mapping_seen[0]['device_name'],
self.assertEqual(len(self._block_device_mapping_seen), 1)
self.assertTrue(self._legacy_bdm_seen)
self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1)
self.assertEqual(self._block_device_mapping_seen[0]['device_name'],
'/dev/vda')
def test_create_root_volume_bdm_v2(self):
body = dict(server=dict(
name='test_server', imageRef=IMAGE_UUID,
flavorRef=2, min_count=1, max_count=1,
block_device_mapping_v2=[dict(
source_type='volume',
uuid=1,
device_name='/dev/vda',
boot_index=0,
delete_on_termination=False,
)]
))
req = webob.Request.blank('/v2/fake/os-volumes_boot')
req.method = 'POST'
req.body = jsonutils.dumps(body)
req.headers['content-type'] = 'application/json'
res = req.get_response(fakes.wsgi_app(
init_only=('os-volumes_boot', 'servers')))
self.assertEqual(res.status_int, 202)
server = jsonutils.loads(res.body)['server']
self.assertEqual(FAKE_UUID, server['id'])
self.assertEqual(CONF.password_length, len(server['adminPass']))
self.assertEqual(len(self._block_device_mapping_seen), 1)
self.assertFalse(self._legacy_bdm_seen)
self.assertEqual(self._block_device_mapping_seen[0]['volume_id'], 1)
self.assertEqual(self._block_device_mapping_seen[0]['boot_index'],
0)
self.assertEqual(self._block_device_mapping_seen[0]['device_name'],
'/dev/vda')

View File

@ -34,6 +34,7 @@ from nova.api.openstack.compute import servers
from nova.api.openstack.compute import views
from nova.api.openstack import extensions
from nova.api.openstack import xmlutil
from nova import block_device
from nova.compute import api as compute_api
from nova.compute import flavors
from nova.compute import task_states
@ -2454,6 +2455,18 @@ class ServersControllerCreateTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest,
self._test_create_extra, {}, no_image=True)
def test_create_instance_with_bdm_v2_enabled_no_image(self):
self.ext_mgr.extensions = {'os-block-device-mapping-v2-boot': 'fake'}
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertNotIn('imageRef', kwargs)
return old_create(*args, **kwargs)
self.stubs.Set(compute_api.API, 'create', create)
self.assertRaises(webob.exc.HTTPBadRequest,
self._test_create_extra, {}, no_image=True)
def test_create_instance_with_volumes_enabled_and_bdms_no_image(self):
"""
Test that the create works if there is no image supplied but
@ -2494,6 +2507,40 @@ class ServersControllerCreateTest(test.TestCase):
self.mox.ReplayAll()
self._test_create_extra(params, no_image=True)
def test_create_instance_with_bdm_v2_enabled_and_bdms_no_image(self):
self.ext_mgr.extensions = {
'os-volumes': 'fake',
'os-block-device-mapping-v2-boot': 'fake'}
bdm_v2 = [{
'no_device': None,
'source_type': 'volume',
'destination_type': 'volume',
'uuid': self.volume_id,
'device_name': 'vda',
'delete_on_termination': False,
}]
params = {'block_device_mapping_v2': bdm_v2}
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertThat(block_device.BlockDeviceDict(bdm_v2[0]),
matchers.DictMatches(
kwargs['block_device_mapping'][0]))
self.assertNotIn('imageRef', kwargs)
return old_create(*args, **kwargs)
self.mox.StubOutWithMock(compute_api.API, '_validate_bdm')
self.mox.StubOutWithMock(compute_api.API, '_get_volume_image_metadata')
compute_api.API._validate_bdm(mox.IgnoreArg(),
mox.IgnoreArg()).AndReturn(True)
compute_api.API._get_volume_image_metadata(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn({})
self.mox.ReplayAll()
self.stubs.Set(compute_api.API, 'create', create)
self._test_create_extra(params, no_image=True)
def test_create_instance_with_volumes_disabled(self):
bdm = [{'device_name': 'foo'}]
params = {'block_device_mapping': bdm}
@ -2554,6 +2601,7 @@ class ServersControllerCreateTest(test.TestCase):
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertTrue(kwargs['legacy_bdm'])
self.assertEqual(kwargs['block_device_mapping'], self.bdm)
return old_create(*args, **kwargs)
@ -2615,6 +2663,96 @@ class ServersControllerCreateTest(test.TestCase):
self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
self._test_create_extra(params)
def test_create_instance_bdm_v2(self):
self.ext_mgr.extensions = {'os-volumes': 'fake',
'os-block-device-mapping-v2-boot': 'fake'}
bdm_v2 = [{'source_type': 'volume',
'uuid': 'fake_vol'}]
bdm_v2_expected = [{'source_type': 'volume',
'volume_id': 'fake_vol'}]
params = {'block_device_mapping_v2': bdm_v2}
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertFalse(kwargs['legacy_bdm'])
for expected, received in zip(bdm_v2_expected,
kwargs['block_device_mapping']):
self.assertThat(block_device.BlockDeviceDict(expected),
matchers.DictMatches(received))
return old_create(*args, **kwargs)
def _validate_bdm(*args, **kwargs):
pass
self.stubs.Set(compute_api.API, 'create', create)
self.stubs.Set(compute_api.API, '_validate_bdm', _validate_bdm)
self._test_create_extra(params)
def test_create_instance_decide_format_legacy(self):
self.ext_mgr.extensions = {'os-volumes': 'fake',
'os-block-device-mapping-v2-boot': 'fake'}
bdm = [{'device_name': 'foo1',
'volume_id': 'fake_vol',
'delete_on_termination': 1}]
expected_legacy_flag = True
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertEqual(kwargs['legacy_bdm'], expected_legacy_flag)
return old_create(*args, **kwargs)
def _validate_bdm(*args, **kwargs):
pass
self.stubs.Set(compute_api.API, 'create', create)
self.stubs.Set(compute_api.API, '_validate_bdm',
_validate_bdm)
self._test_create_extra({})
params = {'block_device_mapping': bdm}
self._test_create_extra(params)
def test_create_instance_decide_format_new(self):
self.ext_mgr.extensions = {'os-volumes': 'fake',
'os-block-device-mapping-v2-boot': 'fake'}
bdm_v2 = [{'source_type': 'volume',
'device_name': 'fake_dev',
'uuid': 'fake_vol'}]
old_create = compute_api.API.create
expected_legacy_flag = False
def create(*args, **kwargs):
self.assertEqual(kwargs['legacy_bdm'], expected_legacy_flag)
return old_create(*args, **kwargs)
def _validate_bdm(*args, **kwargs):
pass
self.stubs.Set(compute_api.API, 'create', create)
self.stubs.Set(compute_api.API, '_validate_bdm',
_validate_bdm)
params = {'block_device_mapping_v2': bdm_v2}
self._test_create_extra(params)
def test_create_instance_both_bdm_formats(self):
self.ext_mgr.extensions = {'os-volumes': 'fake',
'os-block-device-mapping-v2-boot': 'fake'}
bdm = [{'device_name': 'foo'}]
bdm_v2 = [{'source_type': 'volume',
'uuid': 'fake_vol'}]
params = {'block_device_mapping': bdm,
'block_device_mapping_v2': bdm_v2}
self.assertRaises(webob.exc.HTTPBadRequest,
self._test_create_extra, params)
def test_create_instance_with_user_data_enabled(self):
self.ext_mgr.extensions = {'os-user-data': 'fake'}
user_data = 'fake'

View File

@ -649,6 +649,10 @@ def stub_volume_get_all_by_project(self, context, search_opts=None):
return [stub_volume_get(self, context, '1')]
def stub_volume_check_attach(self, context, *args, **param):
pass
def stub_snapshot(id, **kwargs):
snapshot = {
'id': id,

View File

@ -160,6 +160,14 @@
"namespace": "http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-block-device-mapping-v2-boot",
"description": "%(text)s",
"links": [],
"name": "BlockDeviceMappingV2Boot",
"namespace": "http://docs.openstack.org/compute/ext/block_device_mapping_v2_boot/api/v2",
"updated": "%(timestamp)s"
},
{
"alias": "os-cells",
"description": "%(text)s",

View File

@ -60,6 +60,9 @@
<extension alias="os-baremetal-nodes" name="BareMetalNodes" namespace="http://docs.openstack.org/compute/ext/baremetal_nodes/api/v2" updated="%(timestamp)s">
<description>%(text)s</description>
</extension>
<extension alias="os-block-device-mapping-v2-boot" name="BlockDeviceMappingV2Boot" namespace="http://docs.openstack.org/compute/ext/block_device_mapping_v2_boot/api/v2" updated="%(timestamp)s">
<description>%(text)s</description>
</extension>
<extension alias="os-cells" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/cells/api/v1.1" name="Cells">
<description>%(text)s</description>
</extension>

View File

@ -0,0 +1,33 @@
{
"server" : {
"name" : "new-server-test",
"imageRef" : "%(host)s/openstack/images/%(image_id)s",
"flavorRef" : "%(host)s/openstack/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
},
"personality" : [
{
"path" : "/etc/banner.txt",
"contents" : "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBpdCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5kIGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVsc2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4gQnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRoZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlvdSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vyc2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6b25zLiINCg0KLVJpY2hhcmQgQmFjaA=="
}
],
"block_device_mapping_v2": [
{
"device_name": "/dev/sdb1",
"source_type": "blank",
"destination_type": "local",
"delete_on_termination": "True",
"guest_format": "swap",
"boot_index": "-1"
},
{
"device_name": "/dev/sda1",
"source_type": "volume",
"destination_type": "volume",
"uuid": "fake-volume-id-1",
"boot_index": "0"
}
]
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<server xmlns="http://docs.openstack.org/compute/api/v1.1" imageRef="%(host)s/openstack/images/%(image_id)s" flavorRef="%(host)s/openstack/flavors/1" name="new-server-test">
<metadata>
<meta key="My Server Name">Apache1</meta>
</metadata>
<personality>
<file path="/etc/banner.txt">
ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp
dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k
IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs
c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g
QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo
ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv
dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy
c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6
b25zLiINCg0KLVJpY2hhcmQgQmFjaA==
</file>
</personality>
<block_device_mapping_v2>
<mapping device_name="/dev/sdb1" source_type="blank" destination_type="local" delete_on_termination="True" guest_format="swap" boot_index="-1"></mapping>
<mapping device_name="/dev/sda1" source_type="volume" destination_type="volume" uuid="fake-volume-id-1" boot_index="0"></mapping>
</block_device_mapping_v2>
</server>

View File

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(host)s/v2/openstack/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns:atom="http://www.w3.org/2005/Atom" xmlns="http://docs.openstack.org/compute/api/v1.1" id="%(id)s" adminPass="%(password)s">
<metadata/>
<atom:link href="%(host)s/v2/openstack/servers/%(uuid)s" rel="self"/>
<atom:link href="%(host)s/openstack/servers/%(uuid)s" rel="bookmark"/>
</server>

View File

@ -3117,6 +3117,30 @@ class BareMetalNodesXmlTest(BareMetalNodesJsonTest):
ctype = 'xml'
class BlockDeviceMappingV2BootJsonTest(ServersSampleBase):
extension_name = ('nova.api.openstack.compute.contrib.'
'block_device_mapping_v2_boot.'
'Block_device_mapping_v2_boot')
def _get_flags(self):
f = super(BlockDeviceMappingV2BootJsonTest, self)._get_flags()
f['osapi_compute_extension'] = CONF.osapi_compute_extension[:]
# We need the volumes extension as well
f['osapi_compute_extension'].append(
'nova.api.openstack.compute.contrib.volumes.Volumes')
return f
def test_servers_post_with_bdm_v2(self):
self.stubs.Set(cinder.API, 'get', fakes.stub_volume_get)
self.stubs.Set(cinder.API, 'check_attach',
fakes.stub_volume_check_attach)
return self._post_server()
class BlockDeviceMappingV2BootXmlTest(BlockDeviceMappingV2BootJsonTest):
ctype = 'xml'
class FloatingIPPoolsSampleJsonTests(ApiSampleTestBase):
extension_name = ("nova.api.openstack.compute.contrib.floating_ip_pools."
"Floating_ip_pools")

View File

@ -136,6 +136,37 @@ class TestBlockDeviceDict(test.TestCase):
BDM = block_device.BlockDeviceDict
self.api_mapping = [
{'id': 1, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sdb1',
'source_type': 'blank',
'destination_type': 'local',
'delete_on_termination': True,
'guest_format': 'swap',
'boot_index': -1},
{'id': 2, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sdc1',
'source_type': 'blank',
'destination_type': 'local',
'delete_on_termination': True,
'boot_index': -1},
{'id': 3, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sda1',
'source_type': 'volume',
'destination_type': 'volume',
'uuid': 'fake-volume-id-1',
'boot_index': -1},
{'id': 4, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sda2',
'source_type': 'snapshot',
'destination_type': 'volume',
'uuid': 'fake-snapshot-id-1',
'boot_index': -1},
{'id': 5, 'instance_uuid': 'fake-instance',
'no_device': True,
'device_name': '/dev/vdc'},
]
self.new_mapping = [
BDM({'id': 1, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sdb1',
@ -154,7 +185,7 @@ class TestBlockDeviceDict(test.TestCase):
'device_name': '/dev/sda1',
'source_type': 'volume',
'destination_type': 'volume',
'volume_id': 'fake-folume-id-1',
'volume_id': 'fake-volume-id-1',
'connection_info': "{'fake': 'connection_info'}",
'boot_index': -1}),
BDM({'id': 4, 'instance_uuid': 'fake-instance',
@ -181,7 +212,7 @@ class TestBlockDeviceDict(test.TestCase):
'virtual_name': 'ephemeral0'},
{'id': 3, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sda1',
'volume_id': 'fake-folume-id-1',
'volume_id': 'fake-volume-id-1',
'connection_info': "{'fake': 'connection_info'}"},
{'id': 4, 'instance_uuid': 'fake-instance',
'device_name': '/dev/sda2',
@ -238,6 +269,15 @@ class TestBlockDeviceDict(test.TestCase):
block_device.BlockDeviceDict.from_legacy(legacy),
matchers.IsSubDictOf(new))
def test_from_api(self):
for api, new in zip(self.api_mapping, self.new_mapping):
new['connection_info'] = None
if new['snapshot_id']:
new['volume_id'] = None
self.assertThat(
block_device.BlockDeviceDict.from_api(api),
matchers.IsSubDictOf(new))
def test_legacy(self):
for legacy, new in zip(self.legacy_mapping, self.new_mapping):
self.assertThat(