Merge lp:~morita-kazutaka/nova/snapshot-volume into lp:~hudson-openstack/nova/trunk

Proposed by MORITA Kazutaka
Status: Merged
Approved by: Soren Hansen
Approved revision: 1020
Merged at revision: 1120
Proposed branch: lp:~morita-kazutaka/nova/snapshot-volume
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 766 lines (+536/-36)
13 files modified
Authors (+1/-0)
nova/api/ec2/__init__.py (+6/-0)
nova/api/ec2/cloud.py (+49/-8)
nova/db/api.py (+39/-0)
nova/db/sqlalchemy/api.py (+76/-0)
nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py (+70/-0)
nova/db/sqlalchemy/models.py (+25/-0)
nova/exception.py (+8/-0)
nova/tests/test_cloud.py (+46/-0)
nova/tests/test_volume.py (+28/-0)
nova/volume/api.py (+44/-0)
nova/volume/driver.py (+95/-28)
nova/volume/manager.py (+49/-0)
To merge this branch: bzr merge lp:~morita-kazutaka/nova/snapshot-volume
Reviewer Review Type Date Requested Status
Brian Waldon (community) Approve
Jay Pipes (community) Approve
Mark Washenberger (community) Approve
Vish Ishaya (community) Approve
Review via email: mp+61071@code.launchpad.net

Description of the change

This adds a volume snapshot support with the EC2 api.

To post a comment you must log in.
Revision history for this message
Mark Washenberger (markwash) wrote :

I haven't really waded into the volume stuff yet, but I thought I'd give a few comments from a first pass.

Minor nits:
- you should bump your database migration version up to the latest in trunk + 1
- we recently made pylint a little happier with our migrations by switching from

265 +from sqlalchemy import *
266 +from migrate import *

to

from sqlalchemy import Column, Table # ...

you might want to change your branch to do the same.

- I see you've added a sensible test to the database api functions you added. Is it possible to add some tests to the ec2 api functionality? What about the volume api and manager functionality?

- There is a blueprint coming down the pipe to get rid of database messaging. I wonder if it would be possible to adopt something along the lines of that approach in this merge. The part that concerns me slightly is the fact that both the volume api and volume manager are writing to the database. Perhaps more information could be added to the rpc cast so that the db writes in the volume api are unnecessary?

Again, this is my first foray into the volume code, so forgive me if I'm off-base. Thanks.

1014. By MORITA Kazutaka

Merge trunk

1015. By MORITA Kazutaka

Avoid wildcard import.

1016. By MORITA Kazutaka

Add a unitest to test EC2 snapshot APIs.

1017. By MORITA Kazutaka

Fix comments.

Revision history for this message
MORITA Kazutaka (morita-kazutaka) wrote :

Thanks for your comments!

> I haven't really waded into the volume stuff yet, but I thought I'd give a few
> comments from a first pass.
>
> Minor nits:
> - you should bump your database migration version up to the latest in trunk +
> 1

I've merged the latest branch and incremented the version number.

> - we recently made pylint a little happier with our migrations by switching
> from
>
> 265 +from sqlalchemy import *
> 266 +from migrate import *
>
> to
>
> from sqlalchemy import Column, Table # ...
>
> you might want to change your branch to do the same.

Done.

>
> - I see you've added a sensible test to the database api functions you added.
> Is it possible to add some tests to the ec2 api functionality? What about the
> volume api and manager functionality?

I've added a test for EC2 snapshot APIs. It also covers the volume api
functionality. I think all functionalities in this proposal are covered now.

>
> - There is a blueprint coming down the pipe to get rid of database messaging.
> I wonder if it would be possible to adopt something along the lines of that
> approach in this merge. The part that concerns me slightly is the fact that
> both the volume api and volume manager are writing to the database. Perhaps
> more information could be added to the rpc cast so that the db writes in the
> volume api are unnecessary?

Yes, we can remove DB messaging in this proposal.
I think of starting the work after Vish finishes his work on nova-volume:
  https://code.launchpad.net/~vishvananda/nova/no-db-messaging/+merge/61189

Thanks,

Revision history for this message
Mark Washenberger (markwash) wrote :

Thanks for adding the tests. I'm not sure that the database teardown steps in those tests are necessary, but it appears to be the convention for the tests nearby. . . hmm. . .

> Yes, we can remove DB messaging in this proposal.
> I think of starting the work after Vish finishes his work on nova-volume:
> https://code.launchpad.net/~vishvananda/nova/no-db-messaging/+merge/61189

I agree--the db messaging proposal is not as far along as I had initially suspected, so I would agree we should worry about addressing this concern later.

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

Agreed. This all looks excellent to me. We can refactor for no-db-messaging later.

review: Approve
Revision history for this message
Mark Washenberger (markwash) wrote :

I ran this through our smoke tests and it comes out clean. So I'm happy.

review: Approve
Revision history for this message
Jay Pipes (jaypipes) wrote :

Agreed, this work looks excellent.

review: Approve
Revision history for this message
Brian Waldon (bcwaldon) wrote :

Looking good! I'm more than happy to send this in once a couple minor things are fixed:

108: This description doesn't seem right. Is this a copy/paste error?

Should the "snapshot_name_template" be used in places like line 23, 58, 93, 388 and 419? This is more of a question, as I do not actually know the answer :)

review: Needs Fixing
Revision history for this message
Brian Waldon (bcwaldon) wrote :

There are also quite a few pep8 violations that will need to be resolved before we can merge into trunk.

1018. By MORITA Kazutaka

Fix a description of 'snapshot_name_template'.

1019. By MORITA Kazutaka

Fix pep8 violations.

1020. By MORITA Kazutaka

Merge trunk

Revision history for this message
MORITA Kazutaka (morita-kazutaka) wrote :

> Looking good! I'm more than happy to send this in once a couple minor things
> are fixed:
>
> 108: This description doesn't seem right. Is this a copy/paste error?

Fixed, thanks!

>
> Should the "snapshot_name_template" be used in places like line 23, 58, 93,
> 388 and 419? This is more of a question, as I do not actually know the answer
> :)

The name "snap-xxxxxxxx" is the notation of EC2 snapshot id, so I think we
shouldn't use "snapshot_name_template" for those.

I've also fixed pep8 errors.

Thanks,

Revision history for this message
Brian Waldon (bcwaldon) wrote :

> > Looking good! I'm more than happy to send this in once a couple minor things
> > are fixed:
> >
> > 108: This description doesn't seem right. Is this a copy/paste error?
>
> Fixed, thanks!
>
> >
> > Should the "snapshot_name_template" be used in places like line 23, 58, 93,
> > 388 and 419? This is more of a question, as I do not actually know the
> answer
> > :)
>
> The name "snap-xxxxxxxx" is the notation of EC2 snapshot id, so I think we
> shouldn't use "snapshot_name_template" for those.
>
> I've also fixed pep8 errors.

Sounds good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Authors'
--- Authors 2011-05-24 16:36:04 +0000
+++ Authors 2011-05-27 04:49:04 +0000
@@ -30,6 +30,7 @@
30Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>30Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
31Hisaki Ohara <hisaki.ohara@intel.com>31Hisaki Ohara <hisaki.ohara@intel.com>
32Ilya Alekseyev <ialekseev@griddynamics.com>32Ilya Alekseyev <ialekseev@griddynamics.com>
33Isaku Yamahata <yamahata@valinux.co.jp>
33Jason Koelker <jason@koelker.net>34Jason Koelker <jason@koelker.net>
34Jay Pipes <jaypipes@gmail.com>35Jay Pipes <jaypipes@gmail.com>
35Jesse Andrews <anotherjesse@gmail.com>36Jesse Andrews <anotherjesse@gmail.com>
3637
=== modified file 'nova/api/ec2/__init__.py'
--- nova/api/ec2/__init__.py 2011-05-23 21:15:41 +0000
+++ nova/api/ec2/__init__.py 2011-05-27 04:49:04 +0000
@@ -327,6 +327,12 @@
327 ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')327 ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
328 message = _('Volume %s not found') % ec2_id328 message = _('Volume %s not found') % ec2_id
329 return self._error(req, context, type(ex).__name__, message)329 return self._error(req, context, type(ex).__name__, message)
330 except exception.SnapshotNotFound as ex:
331 LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
332 context=context)
333 ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
334 message = _('Snapshot %s not found') % ec2_id
335 return self._error(req, context, type(ex).__name__, message)
330 except exception.NotFound as ex:336 except exception.NotFound as ex:
331 LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)337 LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)
332 return self._error(req, context, type(ex).__name__, unicode(ex))338 return self._error(req, context, type(ex).__name__, unicode(ex))
333339
=== modified file 'nova/api/ec2/cloud.py'
--- nova/api/ec2/cloud.py 2011-05-20 06:51:29 +0000
+++ nova/api/ec2/cloud.py 2011-05-27 04:49:04 +0000
@@ -283,14 +283,50 @@
283 owner=None,283 owner=None,
284 restorable_by=None,284 restorable_by=None,
285 **kwargs):285 **kwargs):
286 return {'snapshotSet': [{'snapshotId': 'fixme',286 if snapshot_id:
287 'volumeId': 'fixme',287 snapshots = []
288 'status': 'fixme',288 for ec2_id in snapshot_id:
289 'startTime': 'fixme',289 internal_id = ec2utils.ec2_id_to_id(ec2_id)
290 'progress': 'fixme',290 snapshot = self.volume_api.get_snapshot(
291 'ownerId': 'fixme',291 context,
292 'volumeSize': 0,292 snapshot_id=internal_id)
293 'description': 'fixme'}]}293 snapshots.append(snapshot)
294 else:
295 snapshots = self.volume_api.get_all_snapshots(context)
296 snapshots = [self._format_snapshot(context, s) for s in snapshots]
297 return {'snapshotSet': snapshots}
298
299 def _format_snapshot(self, context, snapshot):
300 s = {}
301 s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
302 s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
303 'vol-%08x')
304 s['status'] = snapshot['status']
305 s['startTime'] = snapshot['created_at']
306 s['progress'] = snapshot['progress']
307 s['ownerId'] = snapshot['project_id']
308 s['volumeSize'] = snapshot['volume_size']
309 s['description'] = snapshot['display_description']
310
311 s['display_name'] = snapshot['display_name']
312 s['display_description'] = snapshot['display_description']
313 return s
314
315 def create_snapshot(self, context, volume_id, **kwargs):
316 LOG.audit(_("Create snapshot of volume %s"), volume_id,
317 context=context)
318 volume_id = ec2utils.ec2_id_to_id(volume_id)
319 snapshot = self.volume_api.create_snapshot(
320 context,
321 volume_id=volume_id,
322 name=kwargs.get('display_name'),
323 description=kwargs.get('display_description'))
324 return self._format_snapshot(context, snapshot)
325
326 def delete_snapshot(self, context, snapshot_id, **kwargs):
327 snapshot_id = ec2utils.ec2_id_to_id(snapshot_id)
328 self.volume_api.delete_snapshot(context, snapshot_id=snapshot_id)
329 return True
294330
295 def describe_key_pairs(self, context, key_name=None, **kwargs):331 def describe_key_pairs(self, context, key_name=None, **kwargs):
296 key_pairs = db.key_pair_get_all_by_user(context, context.user_id)332 key_pairs = db.key_pair_get_all_by_user(context, context.user_id)
@@ -619,6 +655,11 @@
619 'volumeId': v['volumeId']}]655 'volumeId': v['volumeId']}]
620 else:656 else:
621 v['attachmentSet'] = [{}]657 v['attachmentSet'] = [{}]
658 if volume.get('snapshot_id') != None:
659 v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
660 'snap-%08x')
661 else:
662 v['snapshotId'] = None
622663
623 v['display_name'] = volume['display_name']664 v['display_name'] = volume['display_name']
624 v['display_description'] = volume['display_description']665 v['display_description'] = volume['display_description']
625666
=== modified file 'nova/db/api.py'
--- nova/db/api.py 2011-05-19 18:08:15 +0000
+++ nova/db/api.py 2011-05-27 04:49:04 +0000
@@ -47,6 +47,8 @@
47 'Template string to be used to generate instance names')47 'Template string to be used to generate instance names')
48flags.DEFINE_string('volume_name_template', 'volume-%08x',48flags.DEFINE_string('volume_name_template', 'volume-%08x',
49 'Template string to be used to generate instance names')49 'Template string to be used to generate instance names')
50flags.DEFINE_string('snapshot_name_template', 'snapshot-%08x',
51 'Template string to be used to generate snapshot names')
5052
5153
52IMPL = utils.LazyPluggable(FLAGS['db_backend'],54IMPL = utils.LazyPluggable(FLAGS['db_backend'],
@@ -881,6 +883,43 @@
881####################883####################
882884
883885
886def snapshot_create(context, values):
887 """Create a snapshot from the values dictionary."""
888 return IMPL.snapshot_create(context, values)
889
890
891def snapshot_destroy(context, snapshot_id):
892 """Destroy the snapshot or raise if it does not exist."""
893 return IMPL.snapshot_destroy(context, snapshot_id)
894
895
896def snapshot_get(context, snapshot_id):
897 """Get a snapshot or raise if it does not exist."""
898 return IMPL.snapshot_get(context, snapshot_id)
899
900
901def snapshot_get_all(context):
902 """Get all snapshots."""
903 return IMPL.snapshot_get_all(context)
904
905
906def snapshot_get_all_by_project(context, project_id):
907 """Get all snapshots belonging to a project."""
908 return IMPL.snapshot_get_all_by_project(context, project_id)
909
910
911def snapshot_update(context, snapshot_id, values):
912 """Set the given properties on an snapshot and update it.
913
914 Raises NotFound if snapshot does not exist.
915
916 """
917 return IMPL.snapshot_update(context, snapshot_id, values)
918
919
920####################
921
922
884def security_group_get_all(context):923def security_group_get_all(context):
885 """Get all security groups."""924 """Get all security groups."""
886 return IMPL.security_group_get_all(context)925 return IMPL.security_group_get_all(context)
887926
=== modified file 'nova/db/sqlalchemy/api.py'
--- nova/db/sqlalchemy/api.py 2011-05-18 23:30:58 +0000
+++ nova/db/sqlalchemy/api.py 2011-05-27 04:49:04 +0000
@@ -1790,6 +1790,82 @@
17901790
17911791
1792@require_context1792@require_context
1793def snapshot_create(context, values):
1794 snapshot_ref = models.Snapshot()
1795 snapshot_ref.update(values)
1796
1797 session = get_session()
1798 with session.begin():
1799 snapshot_ref.save(session=session)
1800 return snapshot_ref
1801
1802
1803@require_admin_context
1804def snapshot_destroy(context, snapshot_id):
1805 session = get_session()
1806 with session.begin():
1807 session.query(models.Snapshot).\
1808 filter_by(id=snapshot_id).\
1809 update({'deleted': 1,
1810 'deleted_at': datetime.datetime.utcnow(),
1811 'updated_at': literal_column('updated_at')})
1812
1813
1814@require_context
1815def snapshot_get(context, snapshot_id, session=None):
1816 if not session:
1817 session = get_session()
1818 result = None
1819
1820 if is_admin_context(context):
1821 result = session.query(models.Snapshot).\
1822 filter_by(id=snapshot_id).\
1823 filter_by(deleted=can_read_deleted(context)).\
1824 first()
1825 elif is_user_context(context):
1826 result = session.query(models.Snapshot).\
1827 filter_by(project_id=context.project_id).\
1828 filter_by(id=snapshot_id).\
1829 filter_by(deleted=False).\
1830 first()
1831 if not result:
1832 raise exception.SnapshotNotFound(snapshot_id=snapshot_id)
1833
1834 return result
1835
1836
1837@require_admin_context
1838def snapshot_get_all(context):
1839 session = get_session()
1840 return session.query(models.Snapshot).\
1841 filter_by(deleted=can_read_deleted(context)).\
1842 all()
1843
1844
1845@require_context
1846def snapshot_get_all_by_project(context, project_id):
1847 authorize_project_context(context, project_id)
1848
1849 session = get_session()
1850 return session.query(models.Snapshot).\
1851 filter_by(project_id=project_id).\
1852 filter_by(deleted=can_read_deleted(context)).\
1853 all()
1854
1855
1856@require_context
1857def snapshot_update(context, snapshot_id, values):
1858 session = get_session()
1859 with session.begin():
1860 snapshot_ref = snapshot_get(context, snapshot_id, session=session)
1861 snapshot_ref.update(values)
1862 snapshot_ref.save(session=session)
1863
1864
1865###################
1866
1867
1868@require_context
1793def security_group_get_all(context):1869def security_group_get_all(context):
1794 session = get_session()1870 session = get_session()
1795 return session.query(models.SecurityGroup).\1871 return session.query(models.SecurityGroup).\
17961872
=== added file 'nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py'
--- nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py 1970-01-01 00:00:00 +0000
+++ nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py 2011-05-27 04:49:04 +0000
@@ -0,0 +1,70 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 MORITA Kazutaka.
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18from sqlalchemy import Column, Table, MetaData
19from sqlalchemy import Integer, DateTime, Boolean, String
20
21from nova import log as logging
22
23meta = MetaData()
24
25snapshots = Table('snapshots', meta,
26 Column('created_at', DateTime(timezone=False)),
27 Column('updated_at', DateTime(timezone=False)),
28 Column('deleted_at', DateTime(timezone=False)),
29 Column('deleted', Boolean(create_constraint=True, name=None)),
30 Column('id', Integer(), primary_key=True, nullable=False),
31 Column('volume_id', Integer(), nullable=False),
32 Column('user_id',
33 String(length=255, convert_unicode=False, assert_unicode=None,
34 unicode_error=None, _warn_on_bytestring=False)),
35 Column('project_id',
36 String(length=255, convert_unicode=False, assert_unicode=None,
37 unicode_error=None, _warn_on_bytestring=False)),
38 Column('status',
39 String(length=255, convert_unicode=False, assert_unicode=None,
40 unicode_error=None, _warn_on_bytestring=False)),
41 Column('progress',
42 String(length=255, convert_unicode=False, assert_unicode=None,
43 unicode_error=None, _warn_on_bytestring=False)),
44 Column('volume_size', Integer()),
45 Column('scheduled_at', DateTime(timezone=False)),
46 Column('display_name',
47 String(length=255, convert_unicode=False, assert_unicode=None,
48 unicode_error=None, _warn_on_bytestring=False)),
49 Column('display_description',
50 String(length=255, convert_unicode=False, assert_unicode=None,
51 unicode_error=None, _warn_on_bytestring=False)))
52
53
54def upgrade(migrate_engine):
55 # Upgrade operations go here. Don't create your own engine;
56 # bind migrate_engine to your metadata
57 meta.bind = migrate_engine
58
59 try:
60 snapshots.create()
61 except Exception:
62 logging.info(repr(snapshots))
63 logging.exception('Exception while creating table')
64 meta.drop_all(tables=[snapshots])
65 raise
66
67
68def downgrade(migrate_engine):
69 # Operations to reverse the above upgrade go here.
70 snapshots.drop()
071
=== modified file 'nova/db/sqlalchemy/models.py'
--- nova/db/sqlalchemy/models.py 2011-05-17 20:50:12 +0000
+++ nova/db/sqlalchemy/models.py 2011-05-27 04:49:04 +0000
@@ -329,6 +329,31 @@
329 hard_limit = Column(Integer, nullable=True)329 hard_limit = Column(Integer, nullable=True)
330330
331331
332class Snapshot(BASE, NovaBase):
333 """Represents a block storage device that can be attached to a vm."""
334 __tablename__ = 'snapshots'
335 id = Column(Integer, primary_key=True, autoincrement=True)
336
337 @property
338 def name(self):
339 return FLAGS.snapshot_name_template % self.id
340
341 @property
342 def volume_name(self):
343 return FLAGS.volume_name_template % self.volume_id
344
345 user_id = Column(String(255))
346 project_id = Column(String(255))
347
348 volume_id = Column(Integer)
349 status = Column(String(255))
350 progress = Column(String(255))
351 volume_size = Column(Integer)
352
353 display_name = Column(String(255))
354 display_description = Column(String(255))
355
356
332class ExportDevice(BASE, NovaBase):357class ExportDevice(BASE, NovaBase):
333 """Represates a shelf and blade that a volume can be exported on."""358 """Represates a shelf and blade that a volume can be exported on."""
334 __tablename__ = 'export_devices'359 __tablename__ = 'export_devices'
335360
=== modified file 'nova/exception.py'
--- nova/exception.py 2011-05-18 19:46:39 +0000
+++ nova/exception.py 2011-05-27 04:49:04 +0000
@@ -271,6 +271,14 @@
271 message = _("Volume not found for instance %(instance_id)s.")271 message = _("Volume not found for instance %(instance_id)s.")
272272
273273
274class SnapshotNotFound(NotFound):
275 message = _("Snapshot %(snapshot_id)s could not be found.")
276
277
278class VolumeIsBusy(Error):
279 message = _("deleting volume %(volume_name)s that has snapshot")
280
281
274class ExportDeviceNotFoundForVolume(NotFound):282class ExportDeviceNotFoundForVolume(NotFound):
275 message = _("No export device found for volume %(volume_id)s.")283 message = _("No export device found for volume %(volume_id)s.")
276284
277285
=== modified file 'nova/tests/test_cloud.py'
--- nova/tests/test_cloud.py 2011-05-25 22:43:04 +0000
+++ nova/tests/test_cloud.py 2011-05-27 04:49:04 +0000
@@ -186,6 +186,52 @@
186 db.service_destroy(self.context, service1['id'])186 db.service_destroy(self.context, service1['id'])
187 db.service_destroy(self.context, service2['id'])187 db.service_destroy(self.context, service2['id'])
188188
189 def test_describe_snapshots(self):
190 """Makes sure describe_snapshots works and filters results."""
191 vol = db.volume_create(self.context, {})
192 snap1 = db.snapshot_create(self.context, {'volume_id': vol['id']})
193 snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']})
194 result = self.cloud.describe_snapshots(self.context)
195 self.assertEqual(len(result['snapshotSet']), 2)
196 snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x')
197 result = self.cloud.describe_snapshots(self.context,
198 snapshot_id=[snapshot_id])
199 self.assertEqual(len(result['snapshotSet']), 1)
200 self.assertEqual(
201 ec2utils.ec2_id_to_id(result['snapshotSet'][0]['snapshotId']),
202 snap2['id'])
203 db.snapshot_destroy(self.context, snap1['id'])
204 db.snapshot_destroy(self.context, snap2['id'])
205 db.volume_destroy(self.context, vol['id'])
206
207 def test_create_snapshot(self):
208 """Makes sure create_snapshot works."""
209 vol = db.volume_create(self.context, {'status': "available"})
210 volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
211
212 result = self.cloud.create_snapshot(self.context,
213 volume_id=volume_id)
214 snapshot_id = result['snapshotId']
215 result = self.cloud.describe_snapshots(self.context)
216 self.assertEqual(len(result['snapshotSet']), 1)
217 self.assertEqual(result['snapshotSet'][0]['snapshotId'], snapshot_id)
218
219 db.snapshot_destroy(self.context, ec2utils.ec2_id_to_id(snapshot_id))
220 db.volume_destroy(self.context, vol['id'])
221
222 def test_delete_snapshot(self):
223 """Makes sure delete_snapshot works."""
224 vol = db.volume_create(self.context, {'status': "available"})
225 snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
226 'status': "available"})
227 snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
228
229 result = self.cloud.delete_snapshot(self.context,
230 snapshot_id=snapshot_id)
231 self.assertTrue(result)
232
233 db.volume_destroy(self.context, vol['id'])
234
189 def test_describe_instances(self):235 def test_describe_instances(self):
190 """Makes sure describe_instances works and filters results."""236 """Makes sure describe_instances works and filters results."""
191 inst1 = db.instance_create(self.context, {'reservation_id': 'a',237 inst1 = db.instance_create(self.context, {'reservation_id': 'a',
192238
=== modified file 'nova/tests/test_volume.py'
--- nova/tests/test_volume.py 2011-04-21 19:50:04 +0000
+++ nova/tests/test_volume.py 2011-05-27 04:49:04 +0000
@@ -176,6 +176,34 @@
176 # This will allow us to test cross-node interactions176 # This will allow us to test cross-node interactions
177 pass177 pass
178178
179 @staticmethod
180 def _create_snapshot(volume_id, size='0'):
181 """Create a snapshot object."""
182 snap = {}
183 snap['volume_size'] = size
184 snap['user_id'] = 'fake'
185 snap['project_id'] = 'fake'
186 snap['volume_id'] = volume_id
187 snap['status'] = "creating"
188 return db.snapshot_create(context.get_admin_context(), snap)['id']
189
190 def test_create_delete_snapshot(self):
191 """Test snapshot can be created and deleted."""
192 volume_id = self._create_volume()
193 self.volume.create_volume(self.context, volume_id)
194 snapshot_id = self._create_snapshot(volume_id)
195 self.volume.create_snapshot(self.context, volume_id, snapshot_id)
196 self.assertEqual(snapshot_id,
197 db.snapshot_get(context.get_admin_context(),
198 snapshot_id).id)
199
200 self.volume.delete_snapshot(self.context, snapshot_id)
201 self.assertRaises(exception.NotFound,
202 db.snapshot_get,
203 self.context,
204 snapshot_id)
205 self.volume.delete_volume(self.context, volume_id)
206
179207
180class DriverTestCase(test.TestCase):208class DriverTestCase(test.TestCase):
181 """Base Test class for Drivers."""209 """Base Test class for Drivers."""
182210
=== modified file 'nova/volume/api.py'
--- nova/volume/api.py 2011-03-31 08:39:00 +0000
+++ nova/volume/api.py 2011-05-27 04:49:04 +0000
@@ -90,6 +90,15 @@
90 return self.db.volume_get_all(context)90 return self.db.volume_get_all(context)
91 return self.db.volume_get_all_by_project(context, context.project_id)91 return self.db.volume_get_all_by_project(context, context.project_id)
9292
93 def get_snapshot(self, context, snapshot_id):
94 rv = self.db.snapshot_get(context, snapshot_id)
95 return dict(rv.iteritems())
96
97 def get_all_snapshots(self, context):
98 if context.is_admin:
99 return self.db.snapshot_get_all(context)
100 return self.db.snapshot_get_all_by_project(context, context.project_id)
101
93 def check_attach(self, context, volume_id):102 def check_attach(self, context, volume_id):
94 volume = self.get(context, volume_id)103 volume = self.get(context, volume_id)
95 # TODO(vish): abstract status checking?104 # TODO(vish): abstract status checking?
@@ -110,3 +119,38 @@
110 self.db.queue_get_for(context, FLAGS.compute_topic, host),119 self.db.queue_get_for(context, FLAGS.compute_topic, host),
111 {"method": "remove_volume",120 {"method": "remove_volume",
112 "args": {'volume_id': volume_id}})121 "args": {'volume_id': volume_id}})
122
123 def create_snapshot(self, context, volume_id, name, description):
124 volume = self.get(context, volume_id)
125 if volume['status'] != "available":
126 raise exception.ApiError(_("Volume status must be available"))
127
128 options = {
129 'volume_id': volume_id,
130 'user_id': context.user_id,
131 'project_id': context.project_id,
132 'status': "creating",
133 'progress': '0%',
134 'volume_size': volume['size'],
135 'display_name': name,
136 'display_description': description}
137
138 snapshot = self.db.snapshot_create(context, options)
139 rpc.cast(context,
140 FLAGS.scheduler_topic,
141 {"method": "create_snapshot",
142 "args": {"topic": FLAGS.volume_topic,
143 "volume_id": volume_id,
144 "snapshot_id": snapshot['id']}})
145 return snapshot
146
147 def delete_snapshot(self, context, snapshot_id):
148 snapshot = self.get_snapshot(context, snapshot_id)
149 if snapshot['status'] != "available":
150 raise exception.ApiError(_("Snapshot status must be available"))
151 self.db.snapshot_update(context, snapshot_id, {'status': 'deleting'})
152 rpc.cast(context,
153 FLAGS.scheduler_topic,
154 {"method": "delete_snapshot",
155 "args": {"topic": FLAGS.volume_topic,
156 "snapshot_id": snapshot_id}})
113157
=== modified file 'nova/volume/driver.py'
--- nova/volume/driver.py 2011-04-13 17:19:25 +0000
+++ nova/volume/driver.py 2011-05-27 04:49:04 +0000
@@ -90,42 +90,91 @@
90 raise exception.Error(_("volume group %s doesn't exist")90 raise exception.Error(_("volume group %s doesn't exist")
91 % FLAGS.volume_group)91 % FLAGS.volume_group)
9292
93 def create_volume(self, volume):93 def _create_volume(self, volume_name, sizestr):
94 """Creates a logical volume. Can optionally return a Dictionary of
95 changes to the volume object to be persisted."""
96 if int(volume['size']) == 0:
97 sizestr = '100M'
98 else:
99 sizestr = '%sG' % volume['size']
100 self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',94 self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',
101 volume['name'],95 volume_name, FLAGS.volume_group)
102 FLAGS.volume_group)96
10397 def _copy_volume(self, srcstr, deststr, size_in_g):
104 def delete_volume(self, volume):98 self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,
105 """Deletes a logical volume."""99 'count=%d' % (size_in_g * 1024), 'bs=1M')
100
101 def _volume_not_present(self, volume_name):
102 path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
106 try:103 try:
107 self._try_execute('sudo', 'lvdisplay',104 self._try_execute('sudo', 'lvdisplay', path_name)
108 '%s/%s' %
109 (FLAGS.volume_group,
110 volume['name']))
111 except Exception as e:105 except Exception as e:
112 # If the volume isn't present, then don't attempt to delete106 # If the volume isn't present
113 return True107 return True
108 return False
114109
110 def _delete_volume(self, volume, size_in_g):
111 """Deletes a logical volume."""
115 # zero out old volumes to prevent data leaking between users112 # zero out old volumes to prevent data leaking between users
116 # TODO(ja): reclaiming space should be done lazy and low priority113 # TODO(ja): reclaiming space should be done lazy and low priority
117 self._execute('sudo', 'dd', 'if=/dev/zero',114 self._copy_volume('/dev/zero', self.local_path(volume), size_in_g)
118 'of=%s' % self.local_path(volume),
119 'count=%d' % (volume['size'] * 1024),
120 'bs=1M')
121 self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %115 self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %
122 (FLAGS.volume_group,116 (FLAGS.volume_group,
123 volume['name']))117 self._escape_snapshot(volume['name'])))
118
119 def _sizestr(self, size_in_g):
120 if int(size_in_g) == 0:
121 return '100M'
122 return '%sG' % size_in_g
123
124 # Linux LVM reserves name that starts with snapshot, so that
125 # such volume name can't be created. Mangle it.
126 def _escape_snapshot(self, snapshot_name):
127 if not snapshot_name.startswith('snapshot'):
128 return snapshot_name
129 return '_' + snapshot_name
130
131 def create_volume(self, volume):
132 """Creates a logical volume. Can optionally return a Dictionary of
133 changes to the volume object to be persisted."""
134 self._create_volume(volume['name'], self._sizestr(volume['size']))
135
136 def delete_volume(self, volume):
137 """Deletes a logical volume."""
138 if self._volume_not_present(volume['name']):
139 # If the volume isn't present, then don't attempt to delete
140 return True
141
142 # TODO(yamahata): lvm can't delete origin volume only without
143 # deleting derived snapshots. Can we do something fancy?
144 out, err = self._execute('sudo', 'lvdisplay', '--noheading',
145 '-C', '-o', 'Attr',
146 '%s/%s' % (FLAGS.volume_group,
147 volume['name']))
148 # fake_execute returns None resulting unit test error
149 if out:
150 out = out.strip()
151 if (out[0] == 'o') or (out[0] == 'O'):
152 raise exception.VolumeIsBusy(volume_name=volume['name'])
153
154 self._delete_volume(volume, volume['size'])
155
156 def create_snapshot(self, snapshot):
157 """Creates a snapshot."""
158 orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
159 self._try_execute('sudo', 'lvcreate', '-L',
160 self._sizestr(snapshot['volume_size']),
161 '--name', self._escape_snapshot(snapshot['name']),
162 '--snapshot', orig_lv_name)
163
164 def delete_snapshot(self, snapshot):
165 """Deletes a snapshot."""
166 if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
167 # If the snapshot isn't present, then don't attempt to delete
168 return True
169
170 # TODO(yamahata): zeroing out the whole snapshot triggers COW.
171 # it's quite slow.
172 self._delete_volume(snapshot, snapshot['volume_size'])
124173
125 def local_path(self, volume):174 def local_path(self, volume):
126 # NOTE(vish): stops deprecation warning175 # NOTE(vish): stops deprecation warning
127 escaped_group = FLAGS.volume_group.replace('-', '--')176 escaped_group = FLAGS.volume_group.replace('-', '--')
128 escaped_name = volume['name'].replace('-', '--')177 escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
129 return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)178 return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
130179
131 def ensure_export(self, context, volume):180 def ensure_export(self, context, volume):
@@ -559,6 +608,18 @@
559 self._try_execute('rbd', '--pool', FLAGS.rbd_pool,608 self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
560 'rm', volume['name'])609 'rm', volume['name'])
561610
611 def create_snapshot(self, snapshot):
612 """Creates an rbd snapshot"""
613 self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
614 'snap', 'create', '--snap', snapshot['name'],
615 snapshot['volume_name'])
616
617 def delete_snapshot(self, snapshot):
618 """Deletes an rbd snapshot"""
619 self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
620 'snap', 'rm', '--snap', snapshot['name'],
621 snapshot['volume_name'])
622
562 def local_path(self, volume):623 def local_path(self, volume):
563 """Returns the path of the rbd volume."""624 """Returns the path of the rbd volume."""
564 # This is the same as the remote path625 # This is the same as the remote path
@@ -600,18 +661,24 @@
600661
601 def create_volume(self, volume):662 def create_volume(self, volume):
602 """Creates a sheepdog volume"""663 """Creates a sheepdog volume"""
603 if int(volume['size']) == 0:
604 sizestr = '100M'
605 else:
606 sizestr = '%sG' % volume['size']
607 self._try_execute('qemu-img', 'create',664 self._try_execute('qemu-img', 'create',
608 "sheepdog:%s" % volume['name'],665 "sheepdog:%s" % volume['name'],
609 sizestr)666 self._sizestr(volume['size']))
610667
611 def delete_volume(self, volume):668 def delete_volume(self, volume):
612 """Deletes a logical volume"""669 """Deletes a logical volume"""
613 self._try_execute('collie', 'vdi', 'delete', volume['name'])670 self._try_execute('collie', 'vdi', 'delete', volume['name'])
614671
672 def create_snapshot(self, snapshot):
673 """Creates a sheepdog snapshot"""
674 self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
675 "sheepdog:%s" % snapshot['volume_name'])
676
677 def delete_snapshot(self, snapshot):
678 """Deletes a sheepdog snapshot"""
679 self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
680 '-s', snapshot['name'])
681
615 def local_path(self, volume):682 def local_path(self, volume):
616 return "sheepdog:%s" % volume['name']683 return "sheepdog:%s" % volume['name']
617684
618685
=== modified file 'nova/volume/manager.py'
--- nova/volume/manager.py 2011-03-17 13:35:00 +0000
+++ nova/volume/manager.py 2011-05-27 04:49:04 +0000
@@ -142,6 +142,12 @@
142 self.driver.remove_export(context, volume_ref)142 self.driver.remove_export(context, volume_ref)
143 LOG.debug(_("volume %s: deleting"), volume_ref['name'])143 LOG.debug(_("volume %s: deleting"), volume_ref['name'])
144 self.driver.delete_volume(volume_ref)144 self.driver.delete_volume(volume_ref)
145 except exception.VolumeIsBusy, e:
146 LOG.debug(_("volume %s: volume is busy"), volume_ref['name'])
147 self.driver.ensure_export(context, volume_ref)
148 self.db.volume_update(context, volume_ref['id'],
149 {'status': 'available'})
150 return True
145 except Exception:151 except Exception:
146 self.db.volume_update(context,152 self.db.volume_update(context,
147 volume_ref['id'],153 volume_ref['id'],
@@ -152,6 +158,49 @@
152 LOG.debug(_("volume %s: deleted successfully"), volume_ref['name'])158 LOG.debug(_("volume %s: deleted successfully"), volume_ref['name'])
153 return True159 return True
154160
161 def create_snapshot(self, context, volume_id, snapshot_id):
162 """Creates and exports the snapshot."""
163 context = context.elevated()
164 snapshot_ref = self.db.snapshot_get(context, snapshot_id)
165 LOG.info(_("snapshot %s: creating"), snapshot_ref['name'])
166
167 try:
168 snap_name = snapshot_ref['name']
169 LOG.debug(_("snapshot %(snap_name)s: creating") % locals())
170 model_update = self.driver.create_snapshot(snapshot_ref)
171 if model_update:
172 self.db.snapshot_update(context, snapshot_ref['id'],
173 model_update)
174
175 except Exception:
176 self.db.snapshot_update(context,
177 snapshot_ref['id'], {'status': 'error'})
178 raise
179
180 self.db.snapshot_update(context,
181 snapshot_ref['id'], {'status': 'available',
182 'progress': '100%'})
183 LOG.debug(_("snapshot %s: created successfully"), snapshot_ref['name'])
184 return snapshot_id
185
186 def delete_snapshot(self, context, snapshot_id):
187 """Deletes and unexports snapshot."""
188 context = context.elevated()
189 snapshot_ref = self.db.snapshot_get(context, snapshot_id)
190
191 try:
192 LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
193 self.driver.delete_snapshot(snapshot_ref)
194 except Exception:
195 self.db.snapshot_update(context,
196 snapshot_ref['id'],
197 {'status': 'error_deleting'})
198 raise
199
200 self.db.snapshot_destroy(context, snapshot_id)
201 LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name'])
202 return True
203
155 def setup_compute_volume(self, context, volume_id):204 def setup_compute_volume(self, context, volume_id):
156 """Setup remote volume on compute host.205 """Setup remote volume on compute host.
157206