Merge lp:~yamahata/nova/boot-from-volume-1 into lp:~hudson-openstack/nova/trunk

Proposed by Isaku Yamahata
Status: Superseded
Proposed branch: lp:~yamahata/nova/boot-from-volume-1
Merge into: lp:~hudson-openstack/nova/trunk
Prerequisite: lp:~yamahata/nova/boot-from-volume-0
Diff against target: 2129 lines (+1528/-95)
18 files modified
nova/api/ec2/__init__.py (+5/-2)
nova/api/ec2/cloud.py (+295/-34)
nova/api/ec2/ec2utils.py (+40/-0)
nova/compute/api.py (+70/-24)
nova/compute/manager.py (+11/-9)
nova/db/api.py (+7/-1)
nova/db/sqlalchemy/api.py (+17/-0)
nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py (+47/-0)
nova/db/sqlalchemy/models.py (+2/-0)
nova/image/s3.py (+41/-12)
nova/test.py (+12/-0)
nova/tests/image/test_s3.py (+122/-0)
nova/tests/test_api.py (+70/-0)
nova/tests/test_bdm.py (+233/-0)
nova/tests/test_cloud.py (+403/-11)
nova/tests/test_compute.py (+111/-0)
nova/tests/test_volume.py (+31/-0)
nova/volume/api.py (+11/-2)
To merge this branch: bzr merge lp:~yamahata/nova/boot-from-volume-1
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
Sandy Walsh (community) Approve
Review via email: mp+64825@code.launchpad.net

This proposal has been superseded by a proposal from 2011-06-25.

Commit message

This change adds the basic boot-from-volume support to the image service.
Specifically following API will supports --block-device-mapping with volume/snapshot and root device name
 - register image
 - describe image
 - create image(newly support)

Description of the change

This change adds the basic boot-from-volume support to the image service.
Specifically following API will supports --block-device-mapping with volume/snapshot and root device name
- register image
- describe image
- create image(newly support)

At the moment swap and ephemeral aren't supported yet. They will be supported with the next step

Next step
- describe instance attribute with euca command
- get metadata for bundle volume
- swap/ephemeral device support

To post a comment you must log in.
Revision history for this message
Isaku Yamahata (yamahata) wrote :
Download full text (72.6 KiB)

Now here is the unit tests(with some fixes).
So it's ready for merge now, I think.

The next step is to support the following.
- describe instance attribute
- get metadata for bundle volume
- swap/ephemeral device support.

On Wed, Jun 22, 2011 at 05:01:54AM -0000, Isaku Yamahata wrote:
> Isaku Yamahata has proposed merging lp:~yamahata/nova/boot-from-volume-1 into lp:nova with lp:~yamahata/nova/boot-from-volume-0 as a prerequisite.
>
> Requested reviews:
> Nova Core (nova-core)
>
> For more details, see:
> https://code.launchpad.net/~yamahata/nova/boot-from-volume-1/+merge/64825
>
> This is early review request before going further.
> If this direction is okay, I'll add unit tests and then move on to the next step.
>
> This change adds the basic boot-from-volume support to the image service.
> Specifically following API will supports --block-device-mapping with volume/snapshot and root device name
> - register image
> - describe image
> - create image(newly support)
>
> At the moment swap and ephemeral aren't supported. Are these wanted?
>
> NOTE
> - bundle volume is broken
>
> TODO
> - unit tests
>
> Next step
> - describe instance attribute with euca command
> - get metadata for bundle volume
> - swap/ephemeral device support(Is this wanted? or unnecessary?)
> --
> https://code.launchpad.net/~yamahata/nova/boot-from-volume-1/+merge/64825
> You are the owner of lp:~yamahata/nova/boot-from-volume-1.

> === modified file 'nova/api/ec2/__init__.py'
> --- nova/api/ec2/__init__.py 2011-06-15 16:46:24 +0000
> +++ nova/api/ec2/__init__.py 2011-06-22 04:55:48 +0000
> @@ -262,6 +262,8 @@
> 'TerminateInstances': ['projectmanager', 'sysadmin'],
> 'RebootInstances': ['projectmanager', 'sysadmin'],
> 'UpdateInstance': ['projectmanager', 'sysadmin'],
> + 'StartInstances': ['projectmanager', 'sysadmin'],
> + 'StopInstances': ['projectmanager', 'sysadmin'],
> 'DeleteVolume': ['projectmanager', 'sysadmin'],
> 'DescribeImages': ['all'],
> 'DeregisterImage': ['projectmanager', 'sysadmin'],
> @@ -269,6 +271,7 @@
> 'DescribeImageAttribute': ['all'],
> 'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
> 'UpdateImage': ['projectmanager', 'sysadmin'],
> + 'CreateImage': ['projectmanager', 'sysadmin'],
> },
> 'AdminController': {
> # All actions have the same permission: ['none'] (the default)
> @@ -325,13 +328,13 @@
> except exception.VolumeNotFound as ex:
> LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
> context=context)
> - ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
> + ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id)
> message = _('Volume %s not found') % ec2_id
> return self._error(req, context, type(ex).__name__, message)
> except exception.SnapshotNotFound as ex:
> LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
> context=context)
> - ...

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Impressive branch. I don't have a set up for testing it in depth, so I can't verify correctness.

I would like to see mocked out unit tests for each new method/function. Many of the _ internal methods have no tests at all.

Minor things:
+379/380 ... commented out?
+405 ... potential black hole?

review: Needs Fixing
Revision history for this message
Isaku Yamahata (yamahata) wrote :

Thank you for review.

On Wed, Jun 22, 2011 at 06:04:33PM -0000, Sandy Walsh wrote:

> I would like to see mocked out unit tests for each new method/function. Many of the _ internal methods have no tests at all.

Now I added more unit tests for those methods/functions.
I think they covers what you meant.
  - nova/api/ec2/cloud.py
  _parse_block_device_mapping(), _format_block_device_mapping(),
  _format_mappings(), _format_instance_bdm()

  - nova/compute/api.py
  _update_image_block_device_mapping(), _update_block_device_mapping()

  - nova/volume/api.py
  create_snapshot(),create_snapshot_force()

> Minor things:
> +379/380 ... commented out?

Removed them

> +405 ... potential black hole?

Implemented timeout. I adopted 1 hour to timeout.
Although I'm not sure how long it should be, the length wouldn't matter
so much because timeout is just for safety.

thanks,
--
yamahata

Revision history for this message
Sandy Walsh (sandy-walsh) wrote :

Awesome ... I have no other immediate feedback. I'll leave it to others closer to the domain.

Nice work Yamahata!

review: Approve
Revision history for this message
Vish Ishaya (vishvananda) wrote :

excited to get this in!

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :

No proposals found for merge of lp:~yamahata/nova/boot-from-volume-0 into lp:nova.

Revision history for this message
Vish Ishaya (vishvananda) wrote :

we seem to have lost the ability to merge branches that have an already merged prereq, so i'm rerequesting without the prereq.

lp:~yamahata/nova/boot-from-volume-1 updated
1070. By Isaku Yamahata

merge with trunk

1071. By Isaku Yamahata

sqlalchmey/migration: resolved version conflict

1072. By Isaku Yamahata

merge with trunk

1073. By Isaku Yamahata

sqlalchemy/migrate: resolved version conflict

1074. By Isaku Yamahata

nova/compute/api.py: fixed mismerge

1075. By Isaku Yamahata

tests/test_cloud: make an unit test, test_create_image, happy

1076. By Isaku Yamahata

image/fake: added teardown method

Unit tests may alter images in FakeImageService which has pre-defined images.
Since some unit tests depend on those images, so it needs to be cleaned up
after image alternation. Otherwise running many unit tests may fail.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'nova/api/ec2/__init__.py'
2--- nova/api/ec2/__init__.py 2011-06-15 16:46:24 +0000
3+++ nova/api/ec2/__init__.py 2011-06-24 10:12:38 +0000
4@@ -262,6 +262,8 @@
5 'TerminateInstances': ['projectmanager', 'sysadmin'],
6 'RebootInstances': ['projectmanager', 'sysadmin'],
7 'UpdateInstance': ['projectmanager', 'sysadmin'],
8+ 'StartInstances': ['projectmanager', 'sysadmin'],
9+ 'StopInstances': ['projectmanager', 'sysadmin'],
10 'DeleteVolume': ['projectmanager', 'sysadmin'],
11 'DescribeImages': ['all'],
12 'DeregisterImage': ['projectmanager', 'sysadmin'],
13@@ -269,6 +271,7 @@
14 'DescribeImageAttribute': ['all'],
15 'ModifyImageAttribute': ['projectmanager', 'sysadmin'],
16 'UpdateImage': ['projectmanager', 'sysadmin'],
17+ 'CreateImage': ['projectmanager', 'sysadmin'],
18 },
19 'AdminController': {
20 # All actions have the same permission: ['none'] (the default)
21@@ -325,13 +328,13 @@
22 except exception.VolumeNotFound as ex:
23 LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
24 context=context)
25- ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
26+ ec2_id = ec2utils.id_to_ec2_vol_id(ex.volume_id)
27 message = _('Volume %s not found') % ec2_id
28 return self._error(req, context, type(ex).__name__, message)
29 except exception.SnapshotNotFound as ex:
30 LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
31 context=context)
32- ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
33+ ec2_id = ec2utils.id_to_ec2_snap_id(ex.snapshot_id)
34 message = _('Snapshot %s not found') % ec2_id
35 return self._error(req, context, type(ex).__name__, message)
36 except exception.NotFound as ex:
37
38=== modified file 'nova/api/ec2/cloud.py'
39--- nova/api/ec2/cloud.py 2011-06-17 23:52:22 +0000
40+++ nova/api/ec2/cloud.py 2011-06-24 10:12:38 +0000
41@@ -27,6 +27,7 @@
42 import os
43 import urllib
44 import tempfile
45+import time
46 import shutil
47
48 from nova import compute
49@@ -75,6 +76,95 @@
50 return {'private_key': private_key, 'fingerprint': fingerprint}
51
52
53+# TODO(yamahata): hypervisor dependent default device name
54+_DEFAULT_ROOT_DEVICE_NAME = '/dev/sda1'
55+
56+
57+def _parse_block_device_mapping(bdm):
58+ """Parse BlockDeviceMappingItemType into flat hash
59+ BlockDevicedMapping.<N>.DeviceName
60+ BlockDevicedMapping.<N>.Ebs.SnapshotId
61+ BlockDevicedMapping.<N>.Ebs.VolumeSize
62+ BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
63+ BlockDevicedMapping.<N>.Ebs.NoDevice
64+ BlockDevicedMapping.<N>.VirtualName
65+ => remove .Ebs and allow volume id in SnapshotId
66+ """
67+ ebs = bdm.pop('ebs', None)
68+ if ebs:
69+ ec2_id = ebs.pop('snapshot_id', None)
70+ if ec2_id:
71+ id = ec2utils.ec2_id_to_id(ec2_id)
72+ if ec2_id.startswith('snap-'):
73+ bdm['snapshot_id'] = id
74+ elif ec2_id.startswith('vol-'):
75+ bdm['volume_id'] = id
76+ ebs.setdefault('delete_on_termination', True)
77+ bdm.update(ebs)
78+ return bdm
79+
80+
81+def _properties_get_mappings(properties):
82+ return ec2utils.mappings_prepend_dev(properties.get('mappings', []))
83+
84+
85+def _format_block_device_mapping(bdm):
86+ """Contruct BlockDeviceMappingItemType
87+ {'device_name': '...', 'snapshot_id': , ...}
88+ => BlockDeviceMappingItemType
89+ """
90+ keys = (('deviceName', 'device_name'),
91+ ('virtualName', 'virtual_name'))
92+ item = {}
93+ for name, k in keys:
94+ if k in bdm:
95+ item[name] = bdm[k]
96+ if bdm.get('no_device'):
97+ item['noDevice'] = True
98+ if ('snapshot_id' in bdm) or ('volume_id' in bdm):
99+ ebs_keys = (('snapshotId', 'snapshot_id'),
100+ ('snapshotId', 'volume_id'), # snapshotId is abused
101+ ('volumeSize', 'volume_size'),
102+ ('deleteOnTermination', 'delete_on_termination'))
103+ ebs = {}
104+ for name, k in ebs_keys:
105+ if k in bdm:
106+ if k == 'snapshot_id':
107+ ebs[name] = ec2utils.id_to_ec2_snap_id(bdm[k])
108+ elif k == 'volume_id':
109+ ebs[name] = ec2utils.id_to_ec2_vol_id(bdm[k])
110+ else:
111+ ebs[name] = bdm[k]
112+ assert 'snapshotId' in ebs
113+ item['ebs'] = ebs
114+ return item
115+
116+
117+def _format_mappings(properties, result):
118+ """Format multiple BlockDeviceMappingItemType"""
119+ mappings = [{'virtualName': m['virtual'], 'deviceName': m['device']}
120+ for m in _properties_get_mappings(properties)
121+ if (m['virtual'] == 'swap' or
122+ m['virtual'].startswith('ephemeral'))]
123+
124+ block_device_mapping = [_format_block_device_mapping(bdm) for bdm in
125+ properties.get('block_device_mapping', [])]
126+
127+ # NOTE(yamahata): overwrite mappings with block_device_mapping
128+ for bdm in block_device_mapping:
129+ for i in range(len(mappings)):
130+ if bdm['deviceName'] == mappings[i]['deviceName']:
131+ del mappings[i]
132+ break
133+ mappings.append(bdm)
134+
135+ # NOTE(yamahata): trim ebs.no_device == true. Is this necessary?
136+ mappings = [bdm for bdm in mappings if not (bdm.get('noDevice', False))]
137+
138+ if mappings:
139+ result['blockDeviceMapping'] = mappings
140+
141+
142 class CloudController(object):
143 """ CloudController provides the critical dispatch between
144 inbound API calls through the endpoint and messages
145@@ -177,7 +267,7 @@
146 # TODO(vish): replace with real data
147 'ami': 'sda1',
148 'ephemeral0': 'sda2',
149- 'root': '/dev/sda1',
150+ 'root': _DEFAULT_ROOT_DEVICE_NAME,
151 'swap': 'sda3'},
152 'hostname': hostname,
153 'instance-action': 'none',
154@@ -305,9 +395,8 @@
155
156 def _format_snapshot(self, context, snapshot):
157 s = {}
158- s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
159- s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
160- 'vol-%08x')
161+ s['snapshotId'] = ec2utils.id_to_ec2_snap_id(snapshot['id'])
162+ s['volumeId'] = ec2utils.id_to_ec2_vol_id(snapshot['volume_id'])
163 s['status'] = snapshot['status']
164 s['startTime'] = snapshot['created_at']
165 s['progress'] = snapshot['progress']
166@@ -641,7 +730,7 @@
167 instance_data = '%s[%s]' % (instance_ec2_id,
168 volume['instance']['host'])
169 v = {}
170- v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x')
171+ v['volumeId'] = ec2utils.id_to_ec2_vol_id(volume['id'])
172 v['status'] = volume['status']
173 v['size'] = volume['size']
174 v['availabilityZone'] = volume['availability_zone']
175@@ -663,8 +752,7 @@
176 else:
177 v['attachmentSet'] = [{}]
178 if volume.get('snapshot_id') != None:
179- v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
180- 'snap-%08x')
181+ v['snapshotId'] = ec2utils.id_to_ec2_snap_id(volume['snapshot_id'])
182 else:
183 v['snapshotId'] = None
184
185@@ -727,7 +815,7 @@
186 'instanceId': ec2utils.id_to_ec2_id(instance_id),
187 'requestId': context.request_id,
188 'status': volume['attach_status'],
189- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
190+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
191
192 def detach_volume(self, context, volume_id, **kwargs):
193 volume_id = ec2utils.ec2_id_to_id(volume_id)
194@@ -739,7 +827,7 @@
195 'instanceId': ec2utils.id_to_ec2_id(instance['id']),
196 'requestId': context.request_id,
197 'status': volume['attach_status'],
198- 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
199+ 'volumeId': ec2utils.id_to_ec2_vol_id(volume_id)}
200
201 def _convert_to_set(self, lst, label):
202 if lst is None or lst == []:
203@@ -763,6 +851,37 @@
204 assert len(i) == 1
205 return i[0]
206
207+ def _format_instance_bdm(self, context, instance_id, root_device_name,
208+ result):
209+ """Format InstanceBlockDeviceMappingResponseItemType"""
210+ root_device_type = 'instance-store'
211+ mapping = []
212+ for bdm in db.block_device_mapping_get_all_by_instance(context,
213+ instance_id):
214+ volume_id = bdm['volume_id']
215+ if (volume_id is None or bdm['no_device']):
216+ continue
217+
218+ if (bdm['device_name'] == root_device_name and
219+ (bdm['snapshot_id'] or bdm['volume_id'])):
220+ assert not bdm['virtual_name']
221+ root_device_type = 'ebs'
222+
223+ vol = self.volume_api.get(context, volume_id=volume_id)
224+ LOG.debug(_("vol = %s\n"), vol)
225+ # TODO(yamahata): volume attach time
226+ ebs = {'volumeId': volume_id,
227+ 'deleteOnTermination': bdm['delete_on_termination'],
228+ 'attachTime': vol['attach_time'] or '-',
229+ 'status': vol['status'], }
230+ res = {'deviceName': bdm['device_name'],
231+ 'ebs': ebs, }
232+ mapping.append(res)
233+
234+ if mapping:
235+ result['blockDeviceMapping'] = mapping
236+ result['rootDeviceType'] = root_device_type
237+
238 def _format_instances(self, context, instance_id=None, **kwargs):
239 # TODO(termie): this method is poorly named as its name does not imply
240 # that it will be making a variety of database calls
241@@ -824,6 +943,10 @@
242 i['amiLaunchIndex'] = instance['launch_index']
243 i['displayName'] = instance['display_name']
244 i['displayDescription'] = instance['display_description']
245+ i['rootDeviceName'] = (instance.get('root_device_name') or
246+ _DEFAULT_ROOT_DEVICE_NAME)
247+ self._format_instance_bdm(context, instance_id,
248+ i['rootDeviceName'], i)
249 host = instance['host']
250 zone = self._get_availability_zone_by_host(context, host)
251 i['placement'] = {'availabilityZone': zone}
252@@ -910,23 +1033,7 @@
253 ramdisk = self._get_image(context, kwargs['ramdisk_id'])
254 kwargs['ramdisk_id'] = ramdisk['id']
255 for bdm in kwargs.get('block_device_mapping', []):
256- # NOTE(yamahata)
257- # BlockDevicedMapping.<N>.DeviceName
258- # BlockDevicedMapping.<N>.Ebs.SnapshotId
259- # BlockDevicedMapping.<N>.Ebs.VolumeSize
260- # BlockDevicedMapping.<N>.Ebs.DeleteOnTermination
261- # BlockDevicedMapping.<N>.VirtualName
262- # => remove .Ebs and allow volume id in SnapshotId
263- ebs = bdm.pop('ebs', None)
264- if ebs:
265- ec2_id = ebs.pop('snapshot_id')
266- id = ec2utils.ec2_id_to_id(ec2_id)
267- if ec2_id.startswith('snap-'):
268- bdm['snapshot_id'] = id
269- elif ec2_id.startswith('vol-'):
270- bdm['volume_id'] = id
271- ebs.setdefault('delete_on_termination', True)
272- bdm.update(ebs)
273+ _parse_block_device_mapping(bdm)
274
275 image = self._get_image(context, kwargs['image_id'])
276
277@@ -1081,6 +1188,20 @@
278 i['imageType'] = display_mapping.get(image_type)
279 i['isPublic'] = image.get('is_public') == True
280 i['architecture'] = image['properties'].get('architecture')
281+
282+ properties = image['properties']
283+ root_device_name = ec2utils.properties_root_device_name(properties)
284+ root_device_type = 'instance-store'
285+ for bdm in properties.get('block_device_mapping', []):
286+ if (bdm.get('device_name') == root_device_name and
287+ ('snapshot_id' in bdm or 'volume_id' in bdm) and
288+ not bdm.get('no_device')):
289+ root_device_type = 'ebs'
290+ i['rootDeviceName'] = (root_device_name or _DEFAULT_ROOT_DEVICE_NAME)
291+ i['rootDeviceType'] = root_device_type
292+
293+ _format_mappings(properties, i)
294+
295 return i
296
297 def describe_images(self, context, image_id=None, **kwargs):
298@@ -1105,30 +1226,64 @@
299 self.image_service.delete(context, internal_id)
300 return {'imageId': image_id}
301
302+ def _register_image(self, context, metadata):
303+ image = self.image_service.create(context, metadata)
304+ image_type = self._image_type(image.get('container_format'))
305+ image_id = self.image_ec2_id(image['id'], image_type)
306+ return image_id
307+
308 def register_image(self, context, image_location=None, **kwargs):
309 if image_location is None and 'name' in kwargs:
310 image_location = kwargs['name']
311 metadata = {'properties': {'image_location': image_location}}
312- image = self.image_service.create(context, metadata)
313- image_type = self._image_type(image.get('container_format'))
314- image_id = self.image_ec2_id(image['id'],
315- image_type)
316+
317+ if 'root_device_name' in kwargs:
318+ metadata['properties']['root_device_name'] = \
319+ kwargs.get('root_device_name')
320+
321+ mappings = [_parse_block_device_mapping(bdm) for bdm in
322+ kwargs.get('block_device_mapping', [])]
323+ if mappings:
324+ metadata['properties']['block_device_mapping'] = mappings
325+
326+ image_id = self._register_image(context, metadata)
327 msg = _("Registered image %(image_location)s with"
328 " id %(image_id)s") % locals()
329 LOG.audit(msg, context=context)
330 return {'imageId': image_id}
331
332 def describe_image_attribute(self, context, image_id, attribute, **kwargs):
333- if attribute != 'launchPermission':
334+ def _block_device_mapping_attribute(image, result):
335+ _format_mappings(image['properties'], result)
336+
337+ def _launch_permission_attribute(image, result):
338+ result['launchPermission'] = []
339+ if image['is_public']:
340+ result['launchPermission'].append({'group': 'all'})
341+
342+ def _root_device_name_attribute(image, result):
343+ result['rootDeviceName'] = \
344+ ec2utils.properties_root_device_name(image['properties'])
345+ if result['rootDeviceName'] is None:
346+ result['rootDeviceName'] = _DEFAULT_ROOT_DEVICE_NAME
347+
348+ supported_attributes = {
349+ 'blockDeviceMapping': _block_device_mapping_attribute,
350+ 'launchPermission': _launch_permission_attribute,
351+ 'rootDeviceName': _root_device_name_attribute,
352+ }
353+
354+ fn = supported_attributes.get(attribute)
355+ if fn is None:
356 raise exception.ApiError(_('attribute not supported: %s')
357 % attribute)
358 try:
359 image = self._get_image(context, image_id)
360 except exception.NotFound:
361 raise exception.ImageNotFound(image_id=image_id)
362- result = {'imageId': image_id, 'launchPermission': []}
363- if image['is_public']:
364- result['launchPermission'].append({'group': 'all'})
365+
366+ result = {'imageId': image_id}
367+ fn(image, result)
368 return result
369
370 def modify_image_attribute(self, context, image_id, attribute,
371@@ -1159,3 +1314,109 @@
372 internal_id = ec2utils.ec2_id_to_id(image_id)
373 result = self.image_service.update(context, internal_id, dict(kwargs))
374 return result
375+
376+ # TODO(yamahata): race condition
377+ # At the moment there is no way to prevent others from
378+ # manipulating instances/volumes/snapshots.
379+ # As other code doesn't take it into consideration, here we don't
380+ # care of it for now. Ostrich algorithm
381+ def create_image(self, context, instance_id, **kwargs):
382+ # NOTE(yamahata): name/description are ignored by register_image(),
383+ # do so here
384+ no_reboot = kwargs.get('no_reboot', False)
385+
386+ ec2_instance_id = instance_id
387+ instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
388+ instance = self.compute_api.get(context, instance_id)
389+
390+ # stop the instance if necessary
391+ restart_instance = False
392+ if not no_reboot:
393+ state_description = instance['state_description']
394+
395+ # if the instance is in subtle state, refuse to proceed.
396+ if state_description not in ('running', 'stopping', 'stopped'):
397+ raise exception.InstanceNotRunning(instance_id=ec2_instance_id)
398+
399+ if state_description == 'running':
400+ restart_instance = True
401+ self.compute_api.stop(context, instance_id=instance_id)
402+
403+ # wait instance for really stopped
404+ start_time = time.time()
405+ while state_description != 'stopped':
406+ time.sleep(1)
407+ instance = self.compute_api.get(context, instance_id)
408+ state_description = instance['state_description']
409+ # NOTE(yamahata): timeout and error. 1 hour for now for safety.
410+ # Is it too short/long?
411+ # Or is there any better way?
412+ timeout = 1 * 60 * 60 * 60
413+ if time.time() > start_time + timeout:
414+ raise exception.ApiError(
415+ _('Couldn\'t stop instance with in %d sec') % timeout)
416+
417+ src_image = self._get_image(context, instance['image_ref'])
418+ properties = src_image['properties']
419+ if instance['root_device_name']:
420+ properties['root_device_name'] = instance['root_device_name']
421+
422+ mapping = []
423+ bdms = db.block_device_mapping_get_all_by_instance(context,
424+ instance_id)
425+ for bdm in bdms:
426+ if bdm.no_device:
427+ continue
428+ m = {}
429+ for attr in ('device_name', 'snapshot_id', 'volume_id',
430+ 'volume_size', 'delete_on_termination', 'no_device',
431+ 'virtual_name'):
432+ val = getattr(bdm, attr)
433+ if val is not None:
434+ m[attr] = val
435+
436+ volume_id = m.get('volume_id')
437+ if m.get('snapshot_id') and volume_id:
438+ # create snapshot based on volume_id
439+ vol = self.volume_api.get(context, volume_id=volume_id)
440+ # NOTE(yamahata): Should we wait for snapshot creation?
441+ # Linux LVM snapshot creation completes in
442+ # short time, it doesn't matter for now.
443+ snapshot = self.volume_api.create_snapshot_force(
444+ context, volume_id=volume_id, name=vol['display_name'],
445+ description=vol['display_description'])
446+ m['snapshot_id'] = snapshot['id']
447+ del m['volume_id']
448+
449+ if m:
450+ mapping.append(m)
451+
452+ for m in _properties_get_mappings(properties):
453+ virtual_name = m['virtual']
454+ if virtual_name in ('ami', 'root'):
455+ continue
456+
457+ assert (virtual_name == 'swap' or
458+ virtual_name.startswith('ephemeral'))
459+ device_name = m['device']
460+ if device_name in [b['device_name'] for b in mapping
461+ if not b.get('no_device', False)]:
462+ continue
463+
464+ # NOTE(yamahata): swap and ephemeral devices are specified in
465+ # AMI, but disabled for this instance by user.
466+ # So disable those device by no_device.
467+ mapping.append({'device_name': device_name, 'no_device': True})
468+
469+ if mapping:
470+ properties['block_device_mapping'] = mapping
471+
472+ for attr in ('status', 'location', 'id'):
473+ src_image.pop(attr, None)
474+
475+ image_id = self._register_image(context, src_image)
476+
477+ if restart_instance:
478+ self.compute_api.start(context, instance_id=instance_id)
479+
480+ return {'imageId': image_id}
481
482=== modified file 'nova/api/ec2/ec2utils.py'
483--- nova/api/ec2/ec2utils.py 2011-06-15 06:08:23 +0000
484+++ nova/api/ec2/ec2utils.py 2011-06-24 10:12:38 +0000
485@@ -34,6 +34,17 @@
486 return template % instance_id
487
488
489+def id_to_ec2_snap_id(instance_id):
490+ """Convert an snapshot ID (int) to an ec2 snapshot ID
491+ (snap-[base 16 number])"""
492+ return id_to_ec2_id(instance_id, 'snap-%08x')
493+
494+
495+def id_to_ec2_vol_id(instance_id):
496+ """Convert an volume ID (int) to an ec2 volume ID (vol-[base 16 number])"""
497+ return id_to_ec2_id(instance_id, 'vol-%08x')
498+
499+
500 _c2u = re.compile('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))')
501
502
503@@ -124,3 +135,32 @@
504 args[key] = value
505
506 return args
507+
508+
509+def properties_root_device_name(properties):
510+ """get root device name from image meta data.
511+ If it isn't specified, return None.
512+ """
513+ root_device_name = None
514+
515+ # NOTE(yamahata): see image_service.s3.s3create()
516+ for bdm in properties.get('mappings', []):
517+ if bdm['virtual'] == 'root':
518+ root_device_name = bdm['device']
519+
520+ # NOTE(yamahata): register_image's command line can override
521+ # <machine>.manifest.xml
522+ if 'root_device_name' in properties:
523+ root_device_name = properties['root_device_name']
524+
525+ return root_device_name
526+
527+
528+def mappings_prepend_dev(mappings):
529+ """Prepend '/dev/' to 'device' entry of swap/ephemeral virtual type"""
530+ for m in mappings:
531+ virtual = m['virtual']
532+ if ((virtual == 'swap' or virtual.startswith('ephemeral')) and
533+ (not m['device'].startswith('/'))):
534+ m['device'] = '/dev/' + m['device']
535+ return mappings
536
537=== modified file 'nova/compute/api.py'
538--- nova/compute/api.py 2011-06-23 21:44:29 +0000
539+++ nova/compute/api.py 2011-06-24 10:12:38 +0000
540@@ -32,6 +32,7 @@
541 from nova import rpc
542 from nova import utils
543 from nova import volume
544+from nova.api.ec2 import ec2utils
545 from nova.compute import instance_types
546 from nova.compute import power_state
547 from nova.compute.utils import terminate_volumes
548@@ -223,6 +224,9 @@
549 if reservation_id is None:
550 reservation_id = utils.generate_uid('r')
551
552+ root_device_name = ec2utils.properties_root_device_name(
553+ image['properties'])
554+
555 base_options = {
556 'reservation_id': reservation_id,
557 'image_ref': image_href,
558@@ -247,11 +251,61 @@
559 'availability_zone': availability_zone,
560 'os_type': os_type,
561 'architecture': architecture,
562- 'vm_mode': vm_mode}
563-
564- return (num_instances, base_options, security_groups)
565-
566- def create_db_entry_for_new_instance(self, context, base_options,
567+ 'vm_mode': vm_mode,
568+ 'root_device_name': root_device_name}
569+
570+ return (num_instances, base_options, security_groups, image)
571+
572+ def _update_image_block_device_mapping(self, elevated_context, instance_id,
573+ mappings):
574+ """tell vm driver to create ephemeral/swap device at boot time by
575+ updating BlockDeviceMapping
576+ """
577+ for bdm in ec2utils.mappings_prepend_dev(mappings):
578+ LOG.debug(_("bdm %s"), bdm)
579+
580+ virtual_name = bdm['virtual']
581+ if virtual_name == 'ami' or virtual_name == 'root':
582+ continue
583+
584+ assert (virtual_name == 'swap' or
585+ virtual_name.startswith('ephemeral'))
586+ values = {
587+ 'instance_id': instance_id,
588+ 'device_name': bdm['device'],
589+ 'virtual_name': virtual_name, }
590+ self.db.block_device_mapping_update_or_create(elevated_context,
591+ values)
592+
593+ def _update_block_device_mapping(self, elevated_context, instance_id,
594+ block_device_mapping):
595+ """tell vm driver to attach volume at boot time by updating
596+ BlockDeviceMapping
597+ """
598+ for bdm in block_device_mapping:
599+ LOG.debug(_('bdm %s'), bdm)
600+ assert 'device_name' in bdm
601+
602+ values = {'instance_id': instance_id}
603+ for key in ('device_name', 'delete_on_termination', 'virtual_name',
604+ 'snapshot_id', 'volume_id', 'volume_size',
605+ 'no_device'):
606+ values[key] = bdm.get(key)
607+
608+ # NOTE(yamahata): NoDevice eliminates devices defined in image
609+ # files by command line option.
610+ # (--block-device-mapping)
611+ if bdm.get('virtual_name') == 'NoDevice':
612+ values['no_device'] = True
613+ for k in ('delete_on_termination', 'volume_id',
614+ 'snapshot_id', 'volume_id', 'volume_size',
615+ 'virtual_name'):
616+ values[k] = None
617+
618+ self.db.block_device_mapping_update_or_create(elevated_context,
619+ values)
620+
621+ def create_db_entry_for_new_instance(self, context, image, base_options,
622 security_groups, block_device_mapping, num=1):
623 """Create an entry in the DB for this new instance,
624 including any related table updates (such as security
625@@ -272,22 +326,14 @@
626 instance_id,
627 security_group_id)
628
629- # NOTE(yamahata)
630- # tell vm driver to attach volume at boot time by updating
631- # BlockDeviceMapping
632- for bdm in block_device_mapping:
633- LOG.debug(_('bdm %s'), bdm)
634- assert 'device_name' in bdm
635- values = {
636- 'instance_id': instance_id,
637- 'device_name': bdm['device_name'],
638- 'delete_on_termination': bdm.get('delete_on_termination'),
639- 'virtual_name': bdm.get('virtual_name'),
640- 'snapshot_id': bdm.get('snapshot_id'),
641- 'volume_id': bdm.get('volume_id'),
642- 'volume_size': bdm.get('volume_size'),
643- 'no_device': bdm.get('no_device')}
644- self.db.block_device_mapping_create(elevated, values)
645+ # BlockDeviceMapping table
646+ self._update_image_block_device_mapping(elevated, instance_id,
647+ image['properties'].get('mappings', []))
648+ self._update_block_device_mapping(elevated, instance_id,
649+ image['properties'].get('block_device_mapping', []))
650+ # override via command line option
651+ self._update_block_device_mapping(elevated, instance_id,
652+ block_device_mapping)
653
654 # Set sane defaults if not specified
655 updates = dict(hostname=self.hostname_factory(instance_id))
656@@ -347,7 +393,7 @@
657 """Provision the instances by passing the whole request to
658 the Scheduler for execution. Returns a Reservation ID
659 related to the creation of all of these instances."""
660- num_instances, base_options, security_groups = \
661+ num_instances, base_options, security_groups, image = \
662 self._check_create_parameters(
663 context, instance_type,
664 image_href, kernel_id, ramdisk_id,
665@@ -383,7 +429,7 @@
666 Returns a list of instance dicts.
667 """
668
669- num_instances, base_options, security_groups = \
670+ num_instances, base_options, security_groups, image = \
671 self._check_create_parameters(
672 context, instance_type,
673 image_href, kernel_id, ramdisk_id,
674@@ -398,7 +444,7 @@
675 instances = []
676 LOG.debug(_("Going to run %s instances..."), num_instances)
677 for num in range(num_instances):
678- instance = self.create_db_entry_for_new_instance(context,
679+ instance = self.create_db_entry_for_new_instance(context, image,
680 base_options, security_groups,
681 block_device_mapping, num=num)
682 instances.append(instance)
683
684=== modified file 'nova/compute/manager.py'
685--- nova/compute/manager.py 2011-06-23 17:01:18 +0000
686+++ nova/compute/manager.py 2011-06-24 10:12:38 +0000
687@@ -232,6 +232,17 @@
688 for bdm in self.db.block_device_mapping_get_all_by_instance(
689 context, instance_id):
690 LOG.debug(_("setting up bdm %s"), bdm)
691+
692+ if bdm['no_device']:
693+ continue
694+ if bdm['virtual_name']:
695+ # TODO(yamahata):
696+ # block devices for swap and ephemeralN will be
697+ # created by virt driver locally in compute node.
698+ assert (bdm['virtual_name'] == 'swap' or
699+ bdm['virtual_name'].startswith('ephemeral'))
700+ continue
701+
702 if ((bdm['snapshot_id'] is not None) and
703 (bdm['volume_id'] is None)):
704 # TODO(yamahata): default name and description
705@@ -264,15 +275,6 @@
706 block_device_mapping.append({'device_path': dev_path,
707 'mount_device':
708 bdm['device_name']})
709- elif bdm['virtual_name'] is not None:
710- # TODO(yamahata): ephemeral/swap device support
711- LOG.debug(_('block_device_mapping: '
712- 'ephemeral device is not supported yet'))
713- else:
714- # TODO(yamahata): NoDevice support
715- assert bdm['no_device']
716- LOG.debug(_('block_device_mapping: '
717- 'no device is not supported yet'))
718
719 return block_device_mapping
720
721
722=== modified file 'nova/db/api.py'
723--- nova/db/api.py 2011-06-23 17:01:18 +0000
724+++ nova/db/api.py 2011-06-24 10:12:38 +0000
725@@ -936,10 +936,16 @@
726
727
728 def block_device_mapping_update(context, bdm_id, values):
729- """Create an entry of block device mapping"""
730+ """Update an entry of block device mapping"""
731 return IMPL.block_device_mapping_update(context, bdm_id, values)
732
733
734+def block_device_mapping_update_or_create(context, values):
735+ """Update an entry of block device mapping.
736+ If not existed, create a new entry"""
737+ return IMPL.block_device_mapping_update_or_create(context, values)
738+
739+
740 def block_device_mapping_get_all_by_instance(context, instance_id):
741 """Get all block device mapping belonging to a instance"""
742 return IMPL.block_device_mapping_get_all_by_instance(context, instance_id)
743
744=== modified file 'nova/db/sqlalchemy/api.py'
745--- nova/db/sqlalchemy/api.py 2011-06-23 17:01:18 +0000
746+++ nova/db/sqlalchemy/api.py 2011-06-24 10:12:38 +0000
747@@ -1933,6 +1933,23 @@
748
749
750 @require_context
751+def block_device_mapping_update_or_create(context, values):
752+ session = get_session()
753+ with session.begin():
754+ result = session.query(models.BlockDeviceMapping).\
755+ filter_by(instance_id=values['instance_id']).\
756+ filter_by(device_name=values['device_name']).\
757+ filter_by(deleted=False).\
758+ first()
759+ if not result:
760+ bdm_ref = models.BlockDeviceMapping()
761+ bdm_ref.update(values)
762+ bdm_ref.save(session=session)
763+ else:
764+ result.update(values)
765+
766+
767+@require_context
768 def block_device_mapping_get_all_by_instance(context, instance_id):
769 session = get_session()
770 result = session.query(models.BlockDeviceMapping).\
771
772=== added file 'nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py'
773--- nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py 1970-01-01 00:00:00 +0000
774+++ nova/db/sqlalchemy/migrate_repo/versions/027_add_root_device_name.py 2011-06-24 10:12:38 +0000
775@@ -0,0 +1,47 @@
776+# Copyright 2011 OpenStack LLC.
777+# Copyright 2011 Isaku Yamahata
778+#
779+# Licensed under the Apache License, Version 2.0 (the "License"); you may
780+# not use this file except in compliance with the License. You may obtain
781+# a copy of the License at
782+#
783+# http://www.apache.org/licenses/LICENSE-2.0
784+#
785+# Unless required by applicable law or agreed to in writing, software
786+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
787+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
788+# License for the specific language governing permissions and limitations
789+# under the License.
790+
791+from sqlalchemy import Column, Integer, MetaData, Table, String
792+
793+meta = MetaData()
794+
795+
796+# Just for the ForeignKey and column creation to succeed, these are not the
797+# actual definitions of instances or services.
798+instances = Table('instances', meta,
799+ Column('id', Integer(), primary_key=True, nullable=False),
800+ )
801+
802+#
803+# New Column
804+#
805+root_device_name = Column(
806+ 'root_device_name',
807+ String(length=255, convert_unicode=False, assert_unicode=None,
808+ unicode_error=None, _warn_on_bytestring=False),
809+ nullable=True)
810+
811+
812+def upgrade(migrate_engine):
813+ # Upgrade operations go here. Don't create your own engine;
814+ # bind migrate_engine to your metadata
815+ meta.bind = migrate_engine
816+ instances.create_column(root_device_name)
817+
818+
819+def downgrade(migrate_engine):
820+ # Operations to reverse the above upgrade go here.
821+ meta.bind = migrate_engine
822+ instances.drop_column('root_device_name')
823
824=== modified file 'nova/db/sqlalchemy/models.py'
825--- nova/db/sqlalchemy/models.py 2011-06-23 17:01:18 +0000
826+++ nova/db/sqlalchemy/models.py 2011-06-24 10:12:38 +0000
827@@ -236,6 +236,8 @@
828 vm_mode = Column(String(255))
829 uuid = Column(String(36))
830
831+ root_device_name = Column(String(255))
832+
833 # TODO(vish): see Ewan's email about state improvements, probably
834 # should be in a driver base class or some such
835 # vmstate_state = running, halted, suspended, paused
836
837=== modified file 'nova/image/s3.py'
838--- nova/image/s3.py 2011-06-01 03:16:22 +0000
839+++ nova/image/s3.py 2011-06-24 10:12:38 +0000
840@@ -102,18 +102,7 @@
841 key.get_contents_to_filename(local_filename)
842 return local_filename
843
844- def _s3_create(self, context, metadata):
845- """Gets a manifext from s3 and makes an image."""
846-
847- image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
848-
849- image_location = metadata['properties']['image_location']
850- bucket_name = image_location.split('/')[0]
851- manifest_path = image_location[len(bucket_name) + 1:]
852- bucket = self._conn(context).get_bucket(bucket_name)
853- key = bucket.get_key(manifest_path)
854- manifest = key.get_contents_as_string()
855-
856+ def _s3_parse_manifest(self, context, metadata, manifest):
857 manifest = ElementTree.fromstring(manifest)
858 image_format = 'ami'
859 image_type = 'machine'
860@@ -141,6 +130,28 @@
861 except Exception:
862 arch = 'x86_64'
863
864+ # NOTE(yamahata):
865+ # EC2 ec2-budlne-image --block-device-mapping accepts
866+ # <virtual name>=<device name> where
867+ # virtual name = {ami, root, swap, ephemeral<N>}
868+ # where N is no negative integer
869+ # device name = the device name seen by guest kernel.
870+ # They are converted into
871+ # block_device_mapping/mapping/{virtual, device}
872+ #
873+ # Do NOT confuse this with ec2-register's block device mapping
874+ # argument.
875+ mappings = []
876+ try:
877+ block_device_mapping = manifest.findall('machine_configuration/'
878+ 'block_device_mapping/'
879+ 'mapping')
880+ for bdm in block_device_mapping:
881+ mappings.append({'virtual': bdm.find('virtual').text,
882+ 'device': bdm.find('device').text})
883+ except Exception:
884+ mappings = []
885+
886 properties = metadata['properties']
887 properties['project_id'] = context.project_id
888 properties['architecture'] = arch
889@@ -151,6 +162,9 @@
890 if ramdisk_id:
891 properties['ramdisk_id'] = ec2utils.ec2_id_to_id(ramdisk_id)
892
893+ if mappings:
894+ properties['mappings'] = mappings
895+
896 metadata.update({'disk_format': image_format,
897 'container_format': image_format,
898 'status': 'queued',
899@@ -158,6 +172,21 @@
900 'properties': properties})
901 metadata['properties']['image_state'] = 'pending'
902 image = self.service.create(context, metadata)
903+ return manifest, image
904+
905+ def _s3_create(self, context, metadata):
906+ """Gets a manifext from s3 and makes an image."""
907+
908+ image_path = tempfile.mkdtemp(dir=FLAGS.image_decryption_dir)
909+
910+ image_location = metadata['properties']['image_location']
911+ bucket_name = image_location.split('/')[0]
912+ manifest_path = image_location[len(bucket_name) + 1:]
913+ bucket = self._conn(context).get_bucket(bucket_name)
914+ key = bucket.get_key(manifest_path)
915+ manifest = key.get_contents_as_string()
916+
917+ manifest, image = self._s3_parse_manifest(context, metadata, manifest)
918 image_id = image['id']
919
920 def delayed_create():
921
922=== modified file 'nova/test.py'
923--- nova/test.py 2011-06-03 15:11:01 +0000
924+++ nova/test.py 2011-06-24 10:12:38 +0000
925@@ -252,3 +252,15 @@
926 for d1, d2 in zip(L1, L2):
927 self.assertDictMatch(d1, d2, approx_equal=approx_equal,
928 tolerance=tolerance)
929+
930+ def assertSubDictMatch(self, sub_dict, super_dict):
931+ """Assert a sub_dict is subset of super_dict."""
932+ self.assertTrue(set(sub_dict.keys()).issubset(set(super_dict.keys())))
933+ for k, sub_value in sub_dict.items():
934+ super_value = super_dict[k]
935+ if isinstance(sub_value, dict):
936+ self.assertSubDictMatch(sub_value, super_value)
937+ elif 'DONTCARE' in (sub_value, super_value):
938+ continue
939+ else:
940+ self.assertEqual(sub_value, super_value)
941
942=== added file 'nova/tests/image/test_s3.py'
943--- nova/tests/image/test_s3.py 1970-01-01 00:00:00 +0000
944+++ nova/tests/image/test_s3.py 2011-06-24 10:12:38 +0000
945@@ -0,0 +1,122 @@
946+# vim: tabstop=4 shiftwidth=4 softtabstop=4
947+
948+# Copyright 2011 Isaku Yamahata
949+# All Rights Reserved.
950+#
951+# Licensed under the Apache License, Version 2.0 (the "License"); you may
952+# not use this file except in compliance with the License. You may obtain
953+# a copy of the License at
954+#
955+# http://www.apache.org/licenses/LICENSE-2.0
956+#
957+# Unless required by applicable law or agreed to in writing, software
958+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
959+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
960+# License for the specific language governing permissions and limitations
961+# under the License.
962+
963+from nova import context
964+from nova import flags
965+from nova import test
966+from nova.image import s3
967+
968+FLAGS = flags.FLAGS
969+
970+
971+ami_manifest_xml = """<?xml version="1.0" ?>
972+<manifest>
973+ <version>2011-06-17</version>
974+ <bundler>
975+ <name>test-s3</name>
976+ <version>0</version>
977+ <release>0</release>
978+ </bundler>
979+ <machine_configuration>
980+ <architecture>x86_64</architecture>
981+ <block_device_mapping>
982+ <mapping>
983+ <virtual>ami</virtual>
984+ <device>sda1</device>
985+ </mapping>
986+ <mapping>
987+ <virtual>root</virtual>
988+ <device>/dev/sda1</device>
989+ </mapping>
990+ <mapping>
991+ <virtual>ephemeral0</virtual>
992+ <device>sda2</device>
993+ </mapping>
994+ <mapping>
995+ <virtual>swap</virtual>
996+ <device>sda3</device>
997+ </mapping>
998+ </block_device_mapping>
999+ </machine_configuration>
1000+</manifest>
1001+"""
1002+
1003+
1004+class TestS3ImageService(test.TestCase):
1005+ def setUp(self):
1006+ super(TestS3ImageService, self).setUp()
1007+ self.orig_image_service = FLAGS.image_service
1008+ FLAGS.image_service = 'nova.image.fake.FakeImageService'
1009+ self.image_service = s3.S3ImageService()
1010+ self.context = context.RequestContext(None, None)
1011+
1012+ def tearDown(self):
1013+ super(TestS3ImageService, self).tearDown()
1014+ FLAGS.image_service = self.orig_image_service
1015+
1016+ def _assertEqualList(self, list0, list1, keys):
1017+ self.assertEqual(len(list0), len(list1))
1018+ key = keys[0]
1019+ for x in list0:
1020+ self.assertEqual(len(x), len(keys))
1021+ self.assertTrue(key in x)
1022+ for y in list1:
1023+ self.assertTrue(key in y)
1024+ if x[key] == y[key]:
1025+ for k in keys:
1026+ self.assertEqual(x[k], y[k])
1027+
1028+ def test_s3_create(self):
1029+ metadata = {'properties': {
1030+ 'root_device_name': '/dev/sda1',
1031+ 'block_device_mapping': [
1032+ {'device_name': '/dev/sda1',
1033+ 'snapshot_id': 'snap-12345678',
1034+ 'delete_on_termination': True},
1035+ {'device_name': '/dev/sda2',
1036+ 'virutal_name': 'ephemeral0'},
1037+ {'device_name': '/dev/sdb0',
1038+ 'no_device': True}]}}
1039+ _manifest, image = self.image_service._s3_parse_manifest(
1040+ self.context, metadata, ami_manifest_xml)
1041+ image_id = image['id']
1042+
1043+ ret_image = self.image_service.show(self.context, image_id)
1044+ self.assertTrue('properties' in ret_image)
1045+ properties = ret_image['properties']
1046+
1047+ self.assertTrue('mappings' in properties)
1048+ mappings = properties['mappings']
1049+ expected_mappings = [
1050+ {"device": "sda1", "virtual": "ami"},
1051+ {"device": "/dev/sda1", "virtual": "root"},
1052+ {"device": "sda2", "virtual": "ephemeral0"},
1053+ {"device": "sda3", "virtual": "swap"}]
1054+ self._assertEqualList(mappings, expected_mappings,
1055+ ['device', 'virtual'])
1056+
1057+ self.assertTrue('block_device_mapping', properties)
1058+ block_device_mapping = properties['block_device_mapping']
1059+ expected_bdm = [
1060+ {'device_name': '/dev/sda1',
1061+ 'snapshot_id': 'snap-12345678',
1062+ 'delete_on_termination': True},
1063+ {'device_name': '/dev/sda2',
1064+ 'virutal_name': 'ephemeral0'},
1065+ {'device_name': '/dev/sdb0',
1066+ 'no_device': True}]
1067+ self.assertEqual(block_device_mapping, expected_bdm)
1068
1069=== modified file 'nova/tests/test_api.py'
1070--- nova/tests/test_api.py 2011-06-15 05:41:29 +0000
1071+++ nova/tests/test_api.py 2011-06-24 10:12:38 +0000
1072@@ -92,7 +92,9 @@
1073 conv = ec2utils._try_convert
1074 self.assertEqual(conv('None'), None)
1075 self.assertEqual(conv('True'), True)
1076+ self.assertEqual(conv('true'), True)
1077 self.assertEqual(conv('False'), False)
1078+ self.assertEqual(conv('false'), False)
1079 self.assertEqual(conv('0'), 0)
1080 self.assertEqual(conv('42'), 42)
1081 self.assertEqual(conv('3.14'), 3.14)
1082@@ -107,6 +109,8 @@
1083 def test_ec2_id_to_id(self):
1084 self.assertEqual(ec2utils.ec2_id_to_id('i-0000001e'), 30)
1085 self.assertEqual(ec2utils.ec2_id_to_id('ami-1d'), 29)
1086+ self.assertEqual(ec2utils.ec2_id_to_id('snap-0000001c'), 28)
1087+ self.assertEqual(ec2utils.ec2_id_to_id('vol-0000001b'), 27)
1088
1089 def test_bad_ec2_id(self):
1090 self.assertRaises(exception.InvalidEc2Id,
1091@@ -116,6 +120,72 @@
1092 def test_id_to_ec2_id(self):
1093 self.assertEqual(ec2utils.id_to_ec2_id(30), 'i-0000001e')
1094 self.assertEqual(ec2utils.id_to_ec2_id(29, 'ami-%08x'), 'ami-0000001d')
1095+ self.assertEqual(ec2utils.id_to_ec2_snap_id(28), 'snap-0000001c')
1096+ self.assertEqual(ec2utils.id_to_ec2_vol_id(27), 'vol-0000001b')
1097+
1098+ def test_dict_from_dotted_str(self):
1099+ in_str = [('BlockDeviceMapping.1.DeviceName', '/dev/sda1'),
1100+ ('BlockDeviceMapping.1.Ebs.SnapshotId', 'snap-0000001c'),
1101+ ('BlockDeviceMapping.1.Ebs.VolumeSize', '80'),
1102+ ('BlockDeviceMapping.1.Ebs.DeleteOnTermination', 'false'),
1103+ ('BlockDeviceMapping.2.DeviceName', '/dev/sdc'),
1104+ ('BlockDeviceMapping.2.VirtualName', 'ephemeral0')]
1105+ expected_dict = {
1106+ 'block_device_mapping': {
1107+ '1': {'device_name': '/dev/sda1',
1108+ 'ebs': {'snapshot_id': 'snap-0000001c',
1109+ 'volume_size': 80,
1110+ 'delete_on_termination': False}},
1111+ '2': {'device_name': '/dev/sdc',
1112+ 'virtual_name': 'ephemeral0'}}}
1113+ out_dict = ec2utils.dict_from_dotted_str(in_str)
1114+
1115+ self.assertDictMatch(out_dict, expected_dict)
1116+
1117+ def test_properties_root_defice_name(self):
1118+ mappings = [{"device": "/dev/sda1", "virtual": "root"}]
1119+ properties0 = {'mappings': mappings}
1120+ properties1 = {'root_device_name': '/dev/sdb', 'mappings': mappings}
1121+
1122+ root_device_name = ec2utils.properties_root_device_name(properties0)
1123+ self.assertEqual(root_device_name, '/dev/sda1')
1124+
1125+ root_device_name = ec2utils.properties_root_device_name(properties1)
1126+ self.assertEqual(root_device_name, '/dev/sdb')
1127+
1128+ def test_mapping_prepend_dev(self):
1129+ mappings = [
1130+ {'virtual': 'ami',
1131+ 'device': 'sda1'},
1132+ {'virtual': 'root',
1133+ 'device': '/dev/sda1'},
1134+
1135+ {'virtual': 'swap',
1136+ 'device': 'sdb1'},
1137+ {'virtual': 'swap',
1138+ 'device': '/dev/sdb2'},
1139+
1140+ {'virtual': 'ephemeral0',
1141+ 'device': 'sdc1'},
1142+ {'virtual': 'ephemeral1',
1143+ 'device': '/dev/sdc1'}]
1144+ expected_result = [
1145+ {'virtual': 'ami',
1146+ 'device': 'sda1'},
1147+ {'virtual': 'root',
1148+ 'device': '/dev/sda1'},
1149+
1150+ {'virtual': 'swap',
1151+ 'device': '/dev/sdb1'},
1152+ {'virtual': 'swap',
1153+ 'device': '/dev/sdb2'},
1154+
1155+ {'virtual': 'ephemeral0',
1156+ 'device': '/dev/sdc1'},
1157+ {'virtual': 'ephemeral1',
1158+ 'device': '/dev/sdc1'}]
1159+ self.assertDictListMatch(ec2utils.mappings_prepend_dev(mappings),
1160+ expected_result)
1161
1162
1163 class ApiEc2TestCase(test.TestCase):
1164
1165=== added file 'nova/tests/test_bdm.py'
1166--- nova/tests/test_bdm.py 1970-01-01 00:00:00 +0000
1167+++ nova/tests/test_bdm.py 2011-06-24 10:12:38 +0000
1168@@ -0,0 +1,233 @@
1169+# vim: tabstop=4 shiftwidth=4 softtabstop=4
1170+
1171+# Copyright 2011 Isaku Yamahata
1172+# All Rights Reserved.
1173+#
1174+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1175+# not use this file except in compliance with the License. You may obtain
1176+# a copy of the License at
1177+#
1178+# http://www.apache.org/licenses/LICENSE-2.0
1179+#
1180+# Unless required by applicable law or agreed to in writing, software
1181+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1182+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1183+# License for the specific language governing permissions and limitations
1184+# under the License.
1185+
1186+"""
1187+Tests for Block Device Mapping Code.
1188+"""
1189+
1190+from nova.api.ec2 import cloud
1191+from nova import test
1192+
1193+
1194+class BlockDeviceMappingEc2CloudTestCase(test.TestCase):
1195+ """Test Case for Block Device Mapping"""
1196+
1197+ def setUp(self):
1198+ super(BlockDeviceMappingEc2CloudTestCase, self).setUp()
1199+
1200+ def tearDown(self):
1201+ super(BlockDeviceMappingEc2CloudTestCase, self).tearDown()
1202+
1203+ def _assertApply(self, action, bdm_list):
1204+ for bdm, expected_result in bdm_list:
1205+ self.assertDictMatch(action(bdm), expected_result)
1206+
1207+ def test_parse_block_device_mapping(self):
1208+ bdm_list = [
1209+ ({'device_name': '/dev/fake0',
1210+ 'ebs': {'snapshot_id': 'snap-12345678',
1211+ 'volume_size': 1}},
1212+ {'device_name': '/dev/fake0',
1213+ 'snapshot_id': 0x12345678,
1214+ 'volume_size': 1,
1215+ 'delete_on_termination': True}),
1216+
1217+ ({'device_name': '/dev/fake1',
1218+ 'ebs': {'snapshot_id': 'snap-23456789',
1219+ 'delete_on_termination': False}},
1220+ {'device_name': '/dev/fake1',
1221+ 'snapshot_id': 0x23456789,
1222+ 'delete_on_termination': False}),
1223+
1224+ ({'device_name': '/dev/fake2',
1225+ 'ebs': {'snapshot_id': 'vol-87654321',
1226+ 'volume_size': 2}},
1227+ {'device_name': '/dev/fake2',
1228+ 'volume_id': 0x87654321,
1229+ 'volume_size': 2,
1230+ 'delete_on_termination': True}),
1231+
1232+ ({'device_name': '/dev/fake3',
1233+ 'ebs': {'snapshot_id': 'vol-98765432',
1234+ 'delete_on_termination': False}},
1235+ {'device_name': '/dev/fake3',
1236+ 'volume_id': 0x98765432,
1237+ 'delete_on_termination': False}),
1238+
1239+ ({'device_name': '/dev/fake4',
1240+ 'ebs': {'no_device': True}},
1241+ {'device_name': '/dev/fake4',
1242+ 'no_device': True}),
1243+
1244+ ({'device_name': '/dev/fake5',
1245+ 'virtual_name': 'ephemeral0'},
1246+ {'device_name': '/dev/fake5',
1247+ 'virtual_name': 'ephemeral0'}),
1248+
1249+ ({'device_name': '/dev/fake6',
1250+ 'virtual_name': 'swap'},
1251+ {'device_name': '/dev/fake6',
1252+ 'virtual_name': 'swap'}),
1253+ ]
1254+ self._assertApply(cloud._parse_block_device_mapping, bdm_list)
1255+
1256+ def test_format_block_device_mapping(self):
1257+ bdm_list = [
1258+ ({'device_name': '/dev/fake0',
1259+ 'snapshot_id': 0x12345678,
1260+ 'volume_size': 1,
1261+ 'delete_on_termination': True},
1262+ {'deviceName': '/dev/fake0',
1263+ 'ebs': {'snapshotId': 'snap-12345678',
1264+ 'volumeSize': 1,
1265+ 'deleteOnTermination': True}}),
1266+
1267+ ({'device_name': '/dev/fake1',
1268+ 'snapshot_id': 0x23456789},
1269+ {'deviceName': '/dev/fake1',
1270+ 'ebs': {'snapshotId': 'snap-23456789'}}),
1271+
1272+ ({'device_name': '/dev/fake2',
1273+ 'snapshot_id': 0x23456789,
1274+ 'delete_on_termination': False},
1275+ {'deviceName': '/dev/fake2',
1276+ 'ebs': {'snapshotId': 'snap-23456789',
1277+ 'deleteOnTermination': False}}),
1278+
1279+ ({'device_name': '/dev/fake3',
1280+ 'volume_id': 0x12345678,
1281+ 'volume_size': 1,
1282+ 'delete_on_termination': True},
1283+ {'deviceName': '/dev/fake3',
1284+ 'ebs': {'snapshotId': 'vol-12345678',
1285+ 'volumeSize': 1,
1286+ 'deleteOnTermination': True}}),
1287+
1288+ ({'device_name': '/dev/fake4',
1289+ 'volume_id': 0x23456789},
1290+ {'deviceName': '/dev/fake4',
1291+ 'ebs': {'snapshotId': 'vol-23456789'}}),
1292+
1293+ ({'device_name': '/dev/fake5',
1294+ 'volume_id': 0x23456789,
1295+ 'delete_on_termination': False},
1296+ {'deviceName': '/dev/fake5',
1297+ 'ebs': {'snapshotId': 'vol-23456789',
1298+ 'deleteOnTermination': False}}),
1299+ ]
1300+ self._assertApply(cloud._format_block_device_mapping, bdm_list)
1301+
1302+ def test_format_mapping(self):
1303+ properties = {
1304+ 'mappings': [
1305+ {'virtual': 'ami',
1306+ 'device': 'sda1'},
1307+ {'virtual': 'root',
1308+ 'device': '/dev/sda1'},
1309+
1310+ {'virtual': 'swap',
1311+ 'device': 'sdb1'},
1312+ {'virtual': 'swap',
1313+ 'device': 'sdb2'},
1314+ {'virtual': 'swap',
1315+ 'device': 'sdb3'},
1316+ {'virtual': 'swap',
1317+ 'device': 'sdb4'},
1318+
1319+ {'virtual': 'ephemeral0',
1320+ 'device': 'sdc1'},
1321+ {'virtual': 'ephemeral1',
1322+ 'device': 'sdc2'},
1323+ {'virtual': 'ephemeral2',
1324+ 'device': 'sdc3'},
1325+ ],
1326+
1327+ 'block_device_mapping': [
1328+ # root
1329+ {'device_name': '/dev/sda1',
1330+ 'snapshot_id': 0x12345678,
1331+ 'delete_on_termination': False},
1332+
1333+
1334+ # overwrite swap
1335+ {'device_name': '/dev/sdb2',
1336+ 'snapshot_id': 0x23456789,
1337+ 'delete_on_termination': False},
1338+ {'device_name': '/dev/sdb3',
1339+ 'snapshot_id': 0x3456789A},
1340+ {'device_name': '/dev/sdb4',
1341+ 'no_device': True},
1342+
1343+ # overwrite ephemeral
1344+ {'device_name': '/dev/sdc2',
1345+ 'snapshot_id': 0x3456789A,
1346+ 'delete_on_termination': False},
1347+ {'device_name': '/dev/sdc3',
1348+ 'snapshot_id': 0x456789AB},
1349+ {'device_name': '/dev/sdc4',
1350+ 'no_device': True},
1351+
1352+ # volume
1353+ {'device_name': '/dev/sdd1',
1354+ 'snapshot_id': 0x87654321,
1355+ 'delete_on_termination': False},
1356+ {'device_name': '/dev/sdd2',
1357+ 'snapshot_id': 0x98765432},
1358+ {'device_name': '/dev/sdd3',
1359+ 'snapshot_id': 0xA9875463},
1360+ {'device_name': '/dev/sdd4',
1361+ 'no_device': True}]}
1362+
1363+ expected_result = {
1364+ 'blockDeviceMapping': [
1365+ # root
1366+ {'deviceName': '/dev/sda1',
1367+ 'ebs': {'snapshotId': 'snap-12345678',
1368+ 'deleteOnTermination': False}},
1369+
1370+ # swap
1371+ {'deviceName': '/dev/sdb1',
1372+ 'virtualName': 'swap'},
1373+ {'deviceName': '/dev/sdb2',
1374+ 'ebs': {'snapshotId': 'snap-23456789',
1375+ 'deleteOnTermination': False}},
1376+ {'deviceName': '/dev/sdb3',
1377+ 'ebs': {'snapshotId': 'snap-3456789a'}},
1378+
1379+ # ephemeral
1380+ {'deviceName': '/dev/sdc1',
1381+ 'virtualName': 'ephemeral0'},
1382+ {'deviceName': '/dev/sdc2',
1383+ 'ebs': {'snapshotId': 'snap-3456789a',
1384+ 'deleteOnTermination': False}},
1385+ {'deviceName': '/dev/sdc3',
1386+ 'ebs': {'snapshotId': 'snap-456789ab'}},
1387+
1388+ # volume
1389+ {'deviceName': '/dev/sdd1',
1390+ 'ebs': {'snapshotId': 'snap-87654321',
1391+ 'deleteOnTermination': False}},
1392+ {'deviceName': '/dev/sdd2',
1393+ 'ebs': {'snapshotId': 'snap-98765432'}},
1394+ {'deviceName': '/dev/sdd3',
1395+ 'ebs': {'snapshotId': 'snap-a9875463'}}]}
1396+
1397+ result = {}
1398+ cloud._format_mappings(properties, result)
1399+ print result
1400+ self.assertEqual(result['blockDeviceMapping'].sort(),
1401+ expected_result['blockDeviceMapping'].sort())
1402
1403=== modified file 'nova/tests/test_cloud.py'
1404--- nova/tests/test_cloud.py 2011-06-17 23:52:22 +0000
1405+++ nova/tests/test_cloud.py 2011-06-24 10:12:38 +0000
1406@@ -171,7 +171,7 @@
1407 vol2 = db.volume_create(self.context, {})
1408 result = self.cloud.describe_volumes(self.context)
1409 self.assertEqual(len(result['volumeSet']), 2)
1410- volume_id = ec2utils.id_to_ec2_id(vol2['id'], 'vol-%08x')
1411+ volume_id = ec2utils.id_to_ec2_vol_id(vol2['id'])
1412 result = self.cloud.describe_volumes(self.context,
1413 volume_id=[volume_id])
1414 self.assertEqual(len(result['volumeSet']), 1)
1415@@ -187,7 +187,7 @@
1416 snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
1417 'volume_size': vol['size'],
1418 'status': "available"})
1419- snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
1420+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id'])
1421
1422 result = self.cloud.create_volume(self.context,
1423 snapshot_id=snapshot_id)
1424@@ -224,7 +224,7 @@
1425 snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']})
1426 result = self.cloud.describe_snapshots(self.context)
1427 self.assertEqual(len(result['snapshotSet']), 2)
1428- snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x')
1429+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap2['id'])
1430 result = self.cloud.describe_snapshots(self.context,
1431 snapshot_id=[snapshot_id])
1432 self.assertEqual(len(result['snapshotSet']), 1)
1433@@ -238,7 +238,7 @@
1434 def test_create_snapshot(self):
1435 """Makes sure create_snapshot works."""
1436 vol = db.volume_create(self.context, {'status': "available"})
1437- volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
1438+ volume_id = ec2utils.id_to_ec2_vol_id(vol['id'])
1439
1440 result = self.cloud.create_snapshot(self.context,
1441 volume_id=volume_id)
1442@@ -255,7 +255,7 @@
1443 vol = db.volume_create(self.context, {'status': "available"})
1444 snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
1445 'status': "available"})
1446- snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
1447+ snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id'])
1448
1449 result = self.cloud.delete_snapshot(self.context,
1450 snapshot_id=snapshot_id)
1451@@ -294,6 +294,185 @@
1452 db.service_destroy(self.context, comp1['id'])
1453 db.service_destroy(self.context, comp2['id'])
1454
1455+ def _block_device_mapping_create(self, instance_id, mappings):
1456+ volumes = []
1457+ for bdm in mappings:
1458+ db.block_device_mapping_create(self.context, bdm)
1459+ if 'volume_id' in bdm:
1460+ values = {'id': bdm['volume_id']}
1461+ for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'),
1462+ ('snapshot_size', 'volume_size'),
1463+ ('delete_on_termination',
1464+ 'delete_on_termination')]:
1465+ if bdm_key in bdm:
1466+ values[vol_key] = bdm[bdm_key]
1467+ vol = db.volume_create(self.context, values)
1468+ db.volume_attached(self.context, vol['id'],
1469+ instance_id, bdm['device_name'])
1470+ volumes.append(vol)
1471+ return volumes
1472+
1473+ def _setUpBlockDeviceMapping(self):
1474+ inst1 = db.instance_create(self.context,
1475+ {'image_ref': 1,
1476+ 'root_device_name': '/dev/sdb1'})
1477+ inst2 = db.instance_create(self.context,
1478+ {'image_ref': 2,
1479+ 'root_device_name': '/dev/sdc1'})
1480+
1481+ instance_id = inst1['id']
1482+ mappings0 = [
1483+ {'instance_id': instance_id,
1484+ 'device_name': '/dev/sdb1',
1485+ 'snapshot_id': '1',
1486+ 'volume_id': '2'},
1487+ {'instance_id': instance_id,
1488+ 'device_name': '/dev/sdb2',
1489+ 'volume_id': '3',
1490+ 'volume_size': 1},
1491+ {'instance_id': instance_id,
1492+ 'device_name': '/dev/sdb3',
1493+ 'delete_on_termination': True,
1494+ 'snapshot_id': '4',
1495+ 'volume_id': '5'},
1496+ {'instance_id': instance_id,
1497+ 'device_name': '/dev/sdb4',
1498+ 'delete_on_termination': False,
1499+ 'snapshot_id': '6',
1500+ 'volume_id': '7'},
1501+ {'instance_id': instance_id,
1502+ 'device_name': '/dev/sdb5',
1503+ 'snapshot_id': '8',
1504+ 'volume_id': '9',
1505+ 'volume_size': 0},
1506+ {'instance_id': instance_id,
1507+ 'device_name': '/dev/sdb6',
1508+ 'snapshot_id': '10',
1509+ 'volume_id': '11',
1510+ 'volume_size': 1},
1511+ {'instance_id': instance_id,
1512+ 'device_name': '/dev/sdb7',
1513+ 'no_device': True},
1514+ {'instance_id': instance_id,
1515+ 'device_name': '/dev/sdb8',
1516+ 'virtual_name': 'swap'},
1517+ {'instance_id': instance_id,
1518+ 'device_name': '/dev/sdb9',
1519+ 'virtual_name': 'ephemeral3'}]
1520+
1521+ volumes = self._block_device_mapping_create(instance_id, mappings0)
1522+ return (inst1, inst2, volumes)
1523+
1524+ def _tearDownBlockDeviceMapping(self, inst1, inst2, volumes):
1525+ for vol in volumes:
1526+ db.volume_destroy(self.context, vol['id'])
1527+ for id in (inst1['id'], inst2['id']):
1528+ for bdm in db.block_device_mapping_get_all_by_instance(
1529+ self.context, id):
1530+ db.block_device_mapping_destroy(self.context, bdm['id'])
1531+ db.instance_destroy(self.context, inst2['id'])
1532+ db.instance_destroy(self.context, inst1['id'])
1533+
1534+ _expected_instance_bdm1 = {
1535+ 'instanceId': 'i-00000001',
1536+ 'rootDeviceName': '/dev/sdb1',
1537+ 'rootDeviceType': 'ebs'}
1538+
1539+ _expected_block_device_mapping0 = [
1540+ {'deviceName': '/dev/sdb1',
1541+ 'ebs': {'status': 'in-use',
1542+ 'deleteOnTermination': False,
1543+ 'volumeId': 2,
1544+ }},
1545+ {'deviceName': '/dev/sdb2',
1546+ 'ebs': {'status': 'in-use',
1547+ 'deleteOnTermination': False,
1548+ 'volumeId': 3,
1549+ }},
1550+ {'deviceName': '/dev/sdb3',
1551+ 'ebs': {'status': 'in-use',
1552+ 'deleteOnTermination': True,
1553+ 'volumeId': 5,
1554+ }},
1555+ {'deviceName': '/dev/sdb4',
1556+ 'ebs': {'status': 'in-use',
1557+ 'deleteOnTermination': False,
1558+ 'volumeId': 7,
1559+ }},
1560+ {'deviceName': '/dev/sdb5',
1561+ 'ebs': {'status': 'in-use',
1562+ 'deleteOnTermination': False,
1563+ 'volumeId': 9,
1564+ }},
1565+ {'deviceName': '/dev/sdb6',
1566+ 'ebs': {'status': 'in-use',
1567+ 'deleteOnTermination': False,
1568+ 'volumeId': 11, }}]
1569+ # NOTE(yamahata): swap/ephemeral device case isn't supported yet.
1570+
1571+ _expected_instance_bdm2 = {
1572+ 'instanceId': 'i-00000002',
1573+ 'rootDeviceName': '/dev/sdc1',
1574+ 'rootDeviceType': 'instance-store'}
1575+
1576+ def test_format_instance_bdm(self):
1577+ (inst1, inst2, volumes) = self._setUpBlockDeviceMapping()
1578+
1579+ result = {}
1580+ self.cloud._format_instance_bdm(self.context, inst1['id'], '/dev/sdb1',
1581+ result)
1582+ self.assertSubDictMatch(
1583+ {'rootDeviceType': self._expected_instance_bdm1['rootDeviceType']},
1584+ result)
1585+ self._assertEqualBlockDeviceMapping(
1586+ self._expected_block_device_mapping0, result['blockDeviceMapping'])
1587+
1588+ result = {}
1589+ self.cloud._format_instance_bdm(self.context, inst2['id'], '/dev/sdc1',
1590+ result)
1591+ self.assertSubDictMatch(
1592+ {'rootDeviceType': self._expected_instance_bdm2['rootDeviceType']},
1593+ result)
1594+
1595+ self._tearDownBlockDeviceMapping(inst1, inst2, volumes)
1596+
1597+ def _assertInstance(self, instance_id):
1598+ ec2_instance_id = ec2utils.id_to_ec2_id(instance_id)
1599+ result = self.cloud.describe_instances(self.context,
1600+ instance_id=[ec2_instance_id])
1601+ result = result['reservationSet'][0]
1602+ self.assertEqual(len(result['instancesSet']), 1)
1603+ result = result['instancesSet'][0]
1604+ self.assertEqual(result['instanceId'], ec2_instance_id)
1605+ return result
1606+
1607+ def _assertEqualBlockDeviceMapping(self, expected, result):
1608+ self.assertEqual(len(expected), len(result))
1609+ for x in expected:
1610+ found = False
1611+ for y in result:
1612+ if x['deviceName'] == y['deviceName']:
1613+ self.assertSubDictMatch(x, y)
1614+ found = True
1615+ break
1616+ self.assertTrue(found)
1617+
1618+ def test_describe_instances_bdm(self):
1619+ """Make sure describe_instances works with root_device_name and
1620+ block device mappings
1621+ """
1622+ (inst1, inst2, volumes) = self._setUpBlockDeviceMapping()
1623+
1624+ result = self._assertInstance(inst1['id'])
1625+ self.assertSubDictMatch(self._expected_instance_bdm1, result)
1626+ self._assertEqualBlockDeviceMapping(
1627+ self._expected_block_device_mapping0, result['blockDeviceMapping'])
1628+
1629+ result = self._assertInstance(inst2['id'])
1630+ self.assertSubDictMatch(self._expected_instance_bdm2, result)
1631+
1632+ self._tearDownBlockDeviceMapping(inst1, inst2, volumes)
1633+
1634 def test_describe_images(self):
1635 describe_images = self.cloud.describe_images
1636
1637@@ -323,6 +502,161 @@
1638 self.assertRaises(exception.ImageNotFound, describe_images,
1639 self.context, ['ami-fake'])
1640
1641+ def assertDictListUnorderedMatch(self, L1, L2, key):
1642+ self.assertEqual(len(L1), len(L2))
1643+ for d1 in L1:
1644+ self.assertTrue(key in d1)
1645+ for d2 in L2:
1646+ self.assertTrue(key in d2)
1647+ if d1[key] == d2[key]:
1648+ self.assertDictMatch(d1, d2)
1649+
1650+ def _setUpImageSet(self, create_volumes_and_snapshots=False):
1651+ mappings1 = [
1652+ {'device': '/dev/sda1', 'virtual': 'root'},
1653+
1654+ {'device': 'sdb0', 'virtual': 'ephemeral0'},
1655+ {'device': 'sdb1', 'virtual': 'ephemeral1'},
1656+ {'device': 'sdb2', 'virtual': 'ephemeral2'},
1657+ {'device': 'sdb3', 'virtual': 'ephemeral3'},
1658+ {'device': 'sdb4', 'virtual': 'ephemeral4'},
1659+
1660+ {'device': 'sdc0', 'virtual': 'swap'},
1661+ {'device': 'sdc1', 'virtual': 'swap'},
1662+ {'device': 'sdc2', 'virtual': 'swap'},
1663+ {'device': 'sdc3', 'virtual': 'swap'},
1664+ {'device': 'sdc4', 'virtual': 'swap'}]
1665+ block_device_mapping1 = [
1666+ {'device_name': '/dev/sdb1', 'snapshot_id': 01234567},
1667+ {'device_name': '/dev/sdb2', 'volume_id': 01234567},
1668+ {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'},
1669+ {'device_name': '/dev/sdb4', 'no_device': True},
1670+
1671+ {'device_name': '/dev/sdc1', 'snapshot_id': 12345678},
1672+ {'device_name': '/dev/sdc2', 'volume_id': 12345678},
1673+ {'device_name': '/dev/sdc3', 'virtual_name': 'ephemeral6'},
1674+ {'device_name': '/dev/sdc4', 'no_device': True}]
1675+ image1 = {
1676+ 'id': 1,
1677+ 'properties': {
1678+ 'kernel_id': 1,
1679+ 'type': 'machine',
1680+ 'image_state': 'available',
1681+ 'mappings': mappings1,
1682+ 'block_device_mapping': block_device_mapping1,
1683+ }
1684+ }
1685+
1686+ mappings2 = [{'device': '/dev/sda1', 'virtual': 'root'}]
1687+ block_device_mapping2 = [{'device_name': '/dev/sdb1',
1688+ 'snapshot_id': 01234567}]
1689+ image2 = {
1690+ 'id': 2,
1691+ 'properties': {
1692+ 'kernel_id': 2,
1693+ 'type': 'machine',
1694+ 'root_device_name': '/dev/sdb1',
1695+ 'mappings': mappings2,
1696+ 'block_device_mapping': block_device_mapping2}}
1697+
1698+ def fake_show(meh, context, image_id):
1699+ for i in [image1, image2]:
1700+ if i['id'] == image_id:
1701+ return i
1702+ raise exception.ImageNotFound(image_id=image_id)
1703+
1704+ def fake_detail(meh, context):
1705+ return [image1, image2]
1706+
1707+ self.stubs.Set(fake._FakeImageService, 'show', fake_show)
1708+ self.stubs.Set(fake._FakeImageService, 'detail', fake_detail)
1709+
1710+ volumes = []
1711+ snapshots = []
1712+ if create_volumes_and_snapshots:
1713+ for bdm in block_device_mapping1:
1714+ if 'volume_id' in bdm:
1715+ vol = self._volume_create(bdm['volume_id'])
1716+ volumes.append(vol['id'])
1717+ if 'snapshot_id' in bdm:
1718+ snap = db.snapshot_create(self.context,
1719+ {'id': bdm['snapshot_id'],
1720+ 'volume_id': 76543210,
1721+ 'status': "available",
1722+ 'volume_size': 1})
1723+ snapshots.append(snap['id'])
1724+ return (volumes, snapshots)
1725+
1726+ def _assertImageSet(self, result, root_device_type, root_device_name):
1727+ self.assertEqual(1, len(result['imagesSet']))
1728+ result = result['imagesSet'][0]
1729+ self.assertTrue('rootDeviceType' in result)
1730+ self.assertEqual(result['rootDeviceType'], root_device_type)
1731+ self.assertTrue('rootDeviceName' in result)
1732+ self.assertEqual(result['rootDeviceName'], root_device_name)
1733+ self.assertTrue('blockDeviceMapping' in result)
1734+
1735+ return result
1736+
1737+ _expected_root_device_name1 = '/dev/sda1'
1738+ # NOTE(yamahata): noDevice doesn't make sense when returning mapping
1739+ # It makes sense only when user overriding existing
1740+ # mapping.
1741+ _expected_bdms1 = [
1742+ {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'},
1743+ {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId':
1744+ 'snap-00053977'}},
1745+ {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId':
1746+ 'vol-00053977'}},
1747+ {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'},
1748+ # {'deviceName': '/dev/sdb4', 'noDevice': True},
1749+
1750+ {'deviceName': '/dev/sdc0', 'virtualName': 'swap'},
1751+ {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId':
1752+ 'snap-00bc614e'}},
1753+ {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId':
1754+ 'vol-00bc614e'}},
1755+ {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'},
1756+ # {'deviceName': '/dev/sdc4', 'noDevice': True}
1757+ ]
1758+
1759+ _expected_root_device_name2 = '/dev/sdb1'
1760+ _expected_bdms2 = [{'deviceName': '/dev/sdb1',
1761+ 'ebs': {'snapshotId': 'snap-00053977'}}]
1762+
1763+ # NOTE(yamahata):
1764+ # InstanceBlockDeviceMappingItemType
1765+ # rootDeviceType
1766+ # rootDeviceName
1767+ # blockDeviceMapping
1768+ # deviceName
1769+ # virtualName
1770+ # ebs
1771+ # snapshotId
1772+ # volumeSize
1773+ # deleteOnTermination
1774+ # noDevice
1775+ def test_describe_image_mapping(self):
1776+ """test for rootDeviceName and blockDeiceMapping"""
1777+ describe_images = self.cloud.describe_images
1778+ self._setUpImageSet()
1779+
1780+ result = describe_images(self.context, ['ami-00000001'])
1781+ result = self._assertImageSet(result, 'instance-store',
1782+ self._expected_root_device_name1)
1783+
1784+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
1785+ self._expected_bdms1, 'deviceName')
1786+
1787+ result = describe_images(self.context, ['ami-00000002'])
1788+ result = self._assertImageSet(result, 'ebs',
1789+ self._expected_root_device_name2)
1790+
1791+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
1792+ self._expected_bdms2, 'deviceName')
1793+
1794+ self.stubs.UnsetAll()
1795+
1796 def test_describe_image_attribute(self):
1797 describe_image_attribute = self.cloud.describe_image_attribute
1798
1799@@ -336,6 +670,32 @@
1800 'launchPermission')
1801 self.assertEqual([{'group': 'all'}], result['launchPermission'])
1802
1803+ def test_describe_image_attribute_root_device_name(self):
1804+ describe_image_attribute = self.cloud.describe_image_attribute
1805+ self._setUpImageSet()
1806+
1807+ result = describe_image_attribute(self.context, 'ami-00000001',
1808+ 'rootDeviceName')
1809+ self.assertEqual(result['rootDeviceName'],
1810+ self._expected_root_device_name1)
1811+ result = describe_image_attribute(self.context, 'ami-00000002',
1812+ 'rootDeviceName')
1813+ self.assertEqual(result['rootDeviceName'],
1814+ self._expected_root_device_name2)
1815+
1816+ def test_describe_image_attribute_block_device_mapping(self):
1817+ describe_image_attribute = self.cloud.describe_image_attribute
1818+ self._setUpImageSet()
1819+
1820+ result = describe_image_attribute(self.context, 'ami-00000001',
1821+ 'blockDeviceMapping')
1822+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
1823+ self._expected_bdms1, 'deviceName')
1824+ result = describe_image_attribute(self.context, 'ami-00000002',
1825+ 'blockDeviceMapping')
1826+ self.assertDictListUnorderedMatch(result['blockDeviceMapping'],
1827+ self._expected_bdms2, 'deviceName')
1828+
1829 def test_modify_image_attribute(self):
1830 modify_image_attribute = self.cloud.modify_image_attribute
1831
1832@@ -561,7 +921,7 @@
1833 def test_update_of_volume_display_fields(self):
1834 vol = db.volume_create(self.context, {})
1835 self.cloud.update_volume(self.context,
1836- ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'),
1837+ ec2utils.id_to_ec2_vol_id(vol['id']),
1838 display_name='c00l v0lum3')
1839 vol = db.volume_get(self.context, vol['id'])
1840 self.assertEqual('c00l v0lum3', vol['display_name'])
1841@@ -570,7 +930,7 @@
1842 def test_update_of_volume_wont_update_private_fields(self):
1843 vol = db.volume_create(self.context, {})
1844 self.cloud.update_volume(self.context,
1845- ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x'),
1846+ ec2utils.id_to_ec2_vol_id(vol['id']),
1847 mountpoint='/not/here')
1848 vol = db.volume_get(self.context, vol['id'])
1849 self.assertEqual(None, vol['mountpoint'])
1850@@ -647,11 +1007,13 @@
1851
1852 self._restart_compute_service()
1853
1854- def _volume_create(self):
1855+ def _volume_create(self, volume_id=None):
1856 kwargs = {'status': 'available',
1857 'host': self.volume.host,
1858 'size': 1,
1859 'attach_status': 'detached', }
1860+ if volume_id:
1861+ kwargs['id'] = volume_id
1862 return db.volume_create(self.context, kwargs)
1863
1864 def _assert_volume_attached(self, vol, instance_id, mountpoint):
1865@@ -679,10 +1041,10 @@
1866 'max_count': 1,
1867 'block_device_mapping': [{'device_name': '/dev/vdb',
1868 'volume_id': vol1['id'],
1869- 'delete_on_termination': False, },
1870+ 'delete_on_termination': False},
1871 {'device_name': '/dev/vdc',
1872 'volume_id': vol2['id'],
1873- 'delete_on_termination': True, },
1874+ 'delete_on_termination': True},
1875 ]}
1876 ec2_instance_id = self._run_instance_wait(**kwargs)
1877 instance_id = ec2utils.ec2_id_to_id(ec2_instance_id)
1878@@ -812,7 +1174,7 @@
1879 def test_run_with_snapshot(self):
1880 """Makes sure run/stop/start instance with snapshot works."""
1881 vol = self._volume_create()
1882- ec2_volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
1883+ ec2_volume_id = ec2utils.id_to_ec2_vol_id(vol['id'])
1884
1885 ec2_snapshot1_id = self._create_snapshot(ec2_volume_id)
1886 snapshot1_id = ec2utils.ec2_id_to_id(ec2_snapshot1_id)
1887@@ -871,3 +1233,33 @@
1888 self.cloud.delete_snapshot(self.context, snapshot_id)
1889 greenthread.sleep(0.3)
1890 db.volume_destroy(self.context, vol['id'])
1891+
1892+ def test_create_image(self):
1893+ """Make sure that CreateImage works"""
1894+ # enforce periodic tasks run in short time to avoid wait for 60s.
1895+ self._restart_compute_service(periodic_interval=0.3)
1896+
1897+ (volumes, snapshots) = self._setUpImageSet(
1898+ create_volumes_and_snapshots=True)
1899+
1900+ kwargs = {'image_id': 'ami-1',
1901+ 'instance_type': FLAGS.default_instance_type,
1902+ 'max_count': 1}
1903+ ec2_instance_id = self._run_instance_wait(**kwargs)
1904+
1905+ # TODO(yamahata): s3._s3_create() can't be tested easily by unit test
1906+ # as there is no unit test for s3.create()
1907+ ## result = self.cloud.create_image(self.context, ec2_instance_id,
1908+ ## no_reboot=True)
1909+ ## ec2_image_id = result['imageId']
1910+ ## created_image = self.cloud.describe_images(self.context,
1911+ ## [ec2_image_id])
1912+
1913+ self.cloud.terminate_instances(self.context, [ec2_instance_id])
1914+ for vol in volumes:
1915+ db.volume_destroy(self.context, vol)
1916+ for snap in snapshots:
1917+ db.snapshot_destroy(self.context, snap)
1918+ # TODO(yamahata): clean up snapshot created by CreateImage.
1919+
1920+ self._restart_compute_service()
1921
1922=== modified file 'nova/tests/test_compute.py'
1923--- nova/tests/test_compute.py 2011-06-20 20:55:16 +0000
1924+++ nova/tests/test_compute.py 2011-06-24 10:12:38 +0000
1925@@ -721,3 +721,114 @@
1926 LOG.info(_("After force-killing instances: %s"), instances)
1927 self.assertEqual(len(instances), 1)
1928 self.assertEqual(power_state.SHUTOFF, instances[0]['state'])
1929+
1930+ @staticmethod
1931+ def _parse_db_block_device_mapping(bdm_ref):
1932+ attr_list = ('delete_on_termination', 'device_name', 'no_device',
1933+ 'virtual_name', 'volume_id', 'volume_size', 'snapshot_id')
1934+ bdm = {}
1935+ for attr in attr_list:
1936+ val = bdm_ref.get(attr, None)
1937+ if val:
1938+ bdm[attr] = val
1939+
1940+ return bdm
1941+
1942+ def test_update_block_device_mapping(self):
1943+ instance_id = self._create_instance()
1944+ mappings = [
1945+ {'virtual': 'ami', 'device': 'sda1'},
1946+ {'virtual': 'root', 'device': '/dev/sda1'},
1947+
1948+ {'virtual': 'swap', 'device': 'sdb1'},
1949+ {'virtual': 'swap', 'device': 'sdb2'},
1950+ {'virtual': 'swap', 'device': 'sdb3'},
1951+ {'virtual': 'swap', 'device': 'sdb4'},
1952+
1953+ {'virtual': 'ephemeral0', 'device': 'sdc1'},
1954+ {'virtual': 'ephemeral1', 'device': 'sdc2'},
1955+ {'virtual': 'ephemeral2', 'device': 'sdc3'}]
1956+ block_device_mapping = [
1957+ # root
1958+ {'device_name': '/dev/sda1',
1959+ 'snapshot_id': 0x12345678,
1960+ 'delete_on_termination': False},
1961+
1962+
1963+ # overwrite swap
1964+ {'device_name': '/dev/sdb2',
1965+ 'snapshot_id': 0x23456789,
1966+ 'delete_on_termination': False},
1967+ {'device_name': '/dev/sdb3',
1968+ 'snapshot_id': 0x3456789A},
1969+ {'device_name': '/dev/sdb4',
1970+ 'no_device': True},
1971+
1972+ # overwrite ephemeral
1973+ {'device_name': '/dev/sdc2',
1974+ 'snapshot_id': 0x456789AB,
1975+ 'delete_on_termination': False},
1976+ {'device_name': '/dev/sdc3',
1977+ 'snapshot_id': 0x56789ABC},
1978+ {'device_name': '/dev/sdc4',
1979+ 'no_device': True},
1980+
1981+ # volume
1982+ {'device_name': '/dev/sdd1',
1983+ 'snapshot_id': 0x87654321,
1984+ 'delete_on_termination': False},
1985+ {'device_name': '/dev/sdd2',
1986+ 'snapshot_id': 0x98765432},
1987+ {'device_name': '/dev/sdd3',
1988+ 'snapshot_id': 0xA9875463},
1989+ {'device_name': '/dev/sdd4',
1990+ 'no_device': True}]
1991+
1992+ self.compute_api._update_image_block_device_mapping(
1993+ self.context, instance_id, mappings)
1994+
1995+ bdms = [self._parse_db_block_device_mapping(bdm_ref)
1996+ for bdm_ref in db.block_device_mapping_get_all_by_instance(
1997+ self.context, instance_id)]
1998+ expected_result = [
1999+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
2000+ {'virtual_name': 'swap', 'device_name': '/dev/sdb2'},
2001+ {'virtual_name': 'swap', 'device_name': '/dev/sdb3'},
2002+ {'virtual_name': 'swap', 'device_name': '/dev/sdb4'},
2003+ {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'},
2004+ {'virtual_name': 'ephemeral1', 'device_name': '/dev/sdc2'},
2005+ {'virtual_name': 'ephemeral2', 'device_name': '/dev/sdc3'}]
2006+ bdms.sort()
2007+ expected_result.sort()
2008+ self.assertDictListMatch(bdms, expected_result)
2009+
2010+ self.compute_api._update_block_device_mapping(
2011+ self.context, instance_id, block_device_mapping)
2012+ bdms = [self._parse_db_block_device_mapping(bdm_ref)
2013+ for bdm_ref in db.block_device_mapping_get_all_by_instance(
2014+ self.context, instance_id)]
2015+ expected_result = [
2016+ {'snapshot_id': 0x12345678, 'device_name': '/dev/sda1'},
2017+
2018+ {'virtual_name': 'swap', 'device_name': '/dev/sdb1'},
2019+ {'snapshot_id': 0x23456789, 'device_name': '/dev/sdb2'},
2020+ {'snapshot_id': 0x3456789A, 'device_name': '/dev/sdb3'},
2021+ {'no_device': True, 'device_name': '/dev/sdb4'},
2022+
2023+ {'virtual_name': 'ephemeral0', 'device_name': '/dev/sdc1'},
2024+ {'snapshot_id': 0x456789AB, 'device_name': '/dev/sdc2'},
2025+ {'snapshot_id': 0x56789ABC, 'device_name': '/dev/sdc3'},
2026+ {'no_device': True, 'device_name': '/dev/sdc4'},
2027+
2028+ {'snapshot_id': 0x87654321, 'device_name': '/dev/sdd1'},
2029+ {'snapshot_id': 0x98765432, 'device_name': '/dev/sdd2'},
2030+ {'snapshot_id': 0xA9875463, 'device_name': '/dev/sdd3'},
2031+ {'no_device': True, 'device_name': '/dev/sdd4'}]
2032+ bdms.sort()
2033+ expected_result.sort()
2034+ self.assertDictListMatch(bdms, expected_result)
2035+
2036+ for bdm in db.block_device_mapping_get_all_by_instance(
2037+ self.context, instance_id):
2038+ db.block_device_mapping_destroy(self.context, bdm['id'])
2039+ self.compute.terminate_instance(self.context, instance_id)
2040
2041=== modified file 'nova/tests/test_volume.py'
2042--- nova/tests/test_volume.py 2011-05-27 05:13:17 +0000
2043+++ nova/tests/test_volume.py 2011-06-24 10:12:38 +0000
2044@@ -27,8 +27,10 @@
2045 from nova import db
2046 from nova import flags
2047 from nova import log as logging
2048+from nova import rpc
2049 from nova import test
2050 from nova import utils
2051+from nova import volume
2052
2053 FLAGS = flags.FLAGS
2054 LOG = logging.getLogger('nova.tests.volume')
2055@@ -43,6 +45,11 @@
2056 self.flags(connection_type='fake')
2057 self.volume = utils.import_object(FLAGS.volume_manager)
2058 self.context = context.get_admin_context()
2059+ self.instance_id = db.instance_create(self.context, {})['id']
2060+
2061+ def tearDown(self):
2062+ db.instance_destroy(self.context, self.instance_id)
2063+ super(VolumeTestCase, self).tearDown()
2064
2065 @staticmethod
2066 def _create_volume(size='0', snapshot_id=None):
2067@@ -224,6 +231,30 @@
2068 snapshot_id)
2069 self.volume.delete_volume(self.context, volume_id)
2070
2071+ def test_create_snapshot_force(self):
2072+ """Test snapshot in use can be created forcibly."""
2073+
2074+ def fake_cast(ctxt, topic, msg):
2075+ pass
2076+ self.stubs.Set(rpc, 'cast', fake_cast)
2077+
2078+ volume_id = self._create_volume()
2079+ self.volume.create_volume(self.context, volume_id)
2080+ db.volume_attached(self.context, volume_id, self.instance_id,
2081+ '/dev/sda1')
2082+
2083+ volume_api = volume.api.API()
2084+ self.assertRaises(exception.ApiError,
2085+ volume_api.create_snapshot,
2086+ self.context, volume_id,
2087+ 'fake_name', 'fake_description')
2088+ snapshot_ref = volume_api.create_snapshot_force(self.context,
2089+ volume_id,
2090+ 'fake_name',
2091+ 'fake_description')
2092+ db.snapshot_destroy(self.context, snapshot_ref['id'])
2093+ db.volume_destroy(self.context, volume_id)
2094+
2095
2096 class DriverTestCase(test.TestCase):
2097 """Base Test class for Drivers."""
2098
2099=== modified file 'nova/volume/api.py'
2100--- nova/volume/api.py 2011-06-15 16:46:24 +0000
2101+++ nova/volume/api.py 2011-06-24 10:12:38 +0000
2102@@ -140,9 +140,10 @@
2103 {"method": "remove_volume",
2104 "args": {'volume_id': volume_id}})
2105
2106- def create_snapshot(self, context, volume_id, name, description):
2107+ def _create_snapshot(self, context, volume_id, name, description,
2108+ force=False):
2109 volume = self.get(context, volume_id)
2110- if volume['status'] != "available":
2111+ if ((not force) and (volume['status'] != "available")):
2112 raise exception.ApiError(_("Volume status must be available"))
2113
2114 options = {
2115@@ -164,6 +165,14 @@
2116 "snapshot_id": snapshot['id']}})
2117 return snapshot
2118
2119+ def create_snapshot(self, context, volume_id, name, description):
2120+ return self._create_snapshot(context, volume_id, name, description,
2121+ False)
2122+
2123+ def create_snapshot_force(self, context, volume_id, name, description):
2124+ return self._create_snapshot(context, volume_id, name, description,
2125+ True)
2126+
2127 def delete_snapshot(self, context, snapshot_id):
2128 snapshot = self.get_snapshot(context, snapshot_id)
2129 if snapshot['status'] != "available":