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
1=== modified file 'Authors'
2--- Authors 2011-05-24 16:36:04 +0000
3+++ Authors 2011-05-27 04:49:04 +0000
4@@ -30,6 +30,7 @@
5 Hisaharu Ishii <ishii.hisaharu@lab.ntt.co.jp>
6 Hisaki Ohara <hisaki.ohara@intel.com>
7 Ilya Alekseyev <ialekseev@griddynamics.com>
8+Isaku Yamahata <yamahata@valinux.co.jp>
9 Jason Koelker <jason@koelker.net>
10 Jay Pipes <jaypipes@gmail.com>
11 Jesse Andrews <anotherjesse@gmail.com>
12
13=== modified file 'nova/api/ec2/__init__.py'
14--- nova/api/ec2/__init__.py 2011-05-23 21:15:41 +0000
15+++ nova/api/ec2/__init__.py 2011-05-27 04:49:04 +0000
16@@ -327,6 +327,12 @@
17 ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
18 message = _('Volume %s not found') % ec2_id
19 return self._error(req, context, type(ex).__name__, message)
20+ except exception.SnapshotNotFound as ex:
21+ LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
22+ context=context)
23+ ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
24+ message = _('Snapshot %s not found') % ec2_id
25+ return self._error(req, context, type(ex).__name__, message)
26 except exception.NotFound as ex:
27 LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)
28 return self._error(req, context, type(ex).__name__, unicode(ex))
29
30=== modified file 'nova/api/ec2/cloud.py'
31--- nova/api/ec2/cloud.py 2011-05-20 06:51:29 +0000
32+++ nova/api/ec2/cloud.py 2011-05-27 04:49:04 +0000
33@@ -283,14 +283,50 @@
34 owner=None,
35 restorable_by=None,
36 **kwargs):
37- return {'snapshotSet': [{'snapshotId': 'fixme',
38- 'volumeId': 'fixme',
39- 'status': 'fixme',
40- 'startTime': 'fixme',
41- 'progress': 'fixme',
42- 'ownerId': 'fixme',
43- 'volumeSize': 0,
44- 'description': 'fixme'}]}
45+ if snapshot_id:
46+ snapshots = []
47+ for ec2_id in snapshot_id:
48+ internal_id = ec2utils.ec2_id_to_id(ec2_id)
49+ snapshot = self.volume_api.get_snapshot(
50+ context,
51+ snapshot_id=internal_id)
52+ snapshots.append(snapshot)
53+ else:
54+ snapshots = self.volume_api.get_all_snapshots(context)
55+ snapshots = [self._format_snapshot(context, s) for s in snapshots]
56+ return {'snapshotSet': snapshots}
57+
58+ def _format_snapshot(self, context, snapshot):
59+ s = {}
60+ s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
61+ s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
62+ 'vol-%08x')
63+ s['status'] = snapshot['status']
64+ s['startTime'] = snapshot['created_at']
65+ s['progress'] = snapshot['progress']
66+ s['ownerId'] = snapshot['project_id']
67+ s['volumeSize'] = snapshot['volume_size']
68+ s['description'] = snapshot['display_description']
69+
70+ s['display_name'] = snapshot['display_name']
71+ s['display_description'] = snapshot['display_description']
72+ return s
73+
74+ def create_snapshot(self, context, volume_id, **kwargs):
75+ LOG.audit(_("Create snapshot of volume %s"), volume_id,
76+ context=context)
77+ volume_id = ec2utils.ec2_id_to_id(volume_id)
78+ snapshot = self.volume_api.create_snapshot(
79+ context,
80+ volume_id=volume_id,
81+ name=kwargs.get('display_name'),
82+ description=kwargs.get('display_description'))
83+ return self._format_snapshot(context, snapshot)
84+
85+ def delete_snapshot(self, context, snapshot_id, **kwargs):
86+ snapshot_id = ec2utils.ec2_id_to_id(snapshot_id)
87+ self.volume_api.delete_snapshot(context, snapshot_id=snapshot_id)
88+ return True
89
90 def describe_key_pairs(self, context, key_name=None, **kwargs):
91 key_pairs = db.key_pair_get_all_by_user(context, context.user_id)
92@@ -619,6 +655,11 @@
93 'volumeId': v['volumeId']}]
94 else:
95 v['attachmentSet'] = [{}]
96+ if volume.get('snapshot_id') != None:
97+ v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
98+ 'snap-%08x')
99+ else:
100+ v['snapshotId'] = None
101
102 v['display_name'] = volume['display_name']
103 v['display_description'] = volume['display_description']
104
105=== modified file 'nova/db/api.py'
106--- nova/db/api.py 2011-05-19 18:08:15 +0000
107+++ nova/db/api.py 2011-05-27 04:49:04 +0000
108@@ -47,6 +47,8 @@
109 'Template string to be used to generate instance names')
110 flags.DEFINE_string('volume_name_template', 'volume-%08x',
111 'Template string to be used to generate instance names')
112+flags.DEFINE_string('snapshot_name_template', 'snapshot-%08x',
113+ 'Template string to be used to generate snapshot names')
114
115
116 IMPL = utils.LazyPluggable(FLAGS['db_backend'],
117@@ -881,6 +883,43 @@
118 ####################
119
120
121+def snapshot_create(context, values):
122+ """Create a snapshot from the values dictionary."""
123+ return IMPL.snapshot_create(context, values)
124+
125+
126+def snapshot_destroy(context, snapshot_id):
127+ """Destroy the snapshot or raise if it does not exist."""
128+ return IMPL.snapshot_destroy(context, snapshot_id)
129+
130+
131+def snapshot_get(context, snapshot_id):
132+ """Get a snapshot or raise if it does not exist."""
133+ return IMPL.snapshot_get(context, snapshot_id)
134+
135+
136+def snapshot_get_all(context):
137+ """Get all snapshots."""
138+ return IMPL.snapshot_get_all(context)
139+
140+
141+def snapshot_get_all_by_project(context, project_id):
142+ """Get all snapshots belonging to a project."""
143+ return IMPL.snapshot_get_all_by_project(context, project_id)
144+
145+
146+def snapshot_update(context, snapshot_id, values):
147+ """Set the given properties on an snapshot and update it.
148+
149+ Raises NotFound if snapshot does not exist.
150+
151+ """
152+ return IMPL.snapshot_update(context, snapshot_id, values)
153+
154+
155+####################
156+
157+
158 def security_group_get_all(context):
159 """Get all security groups."""
160 return IMPL.security_group_get_all(context)
161
162=== modified file 'nova/db/sqlalchemy/api.py'
163--- nova/db/sqlalchemy/api.py 2011-05-18 23:30:58 +0000
164+++ nova/db/sqlalchemy/api.py 2011-05-27 04:49:04 +0000
165@@ -1790,6 +1790,82 @@
166
167
168 @require_context
169+def snapshot_create(context, values):
170+ snapshot_ref = models.Snapshot()
171+ snapshot_ref.update(values)
172+
173+ session = get_session()
174+ with session.begin():
175+ snapshot_ref.save(session=session)
176+ return snapshot_ref
177+
178+
179+@require_admin_context
180+def snapshot_destroy(context, snapshot_id):
181+ session = get_session()
182+ with session.begin():
183+ session.query(models.Snapshot).\
184+ filter_by(id=snapshot_id).\
185+ update({'deleted': 1,
186+ 'deleted_at': datetime.datetime.utcnow(),
187+ 'updated_at': literal_column('updated_at')})
188+
189+
190+@require_context
191+def snapshot_get(context, snapshot_id, session=None):
192+ if not session:
193+ session = get_session()
194+ result = None
195+
196+ if is_admin_context(context):
197+ result = session.query(models.Snapshot).\
198+ filter_by(id=snapshot_id).\
199+ filter_by(deleted=can_read_deleted(context)).\
200+ first()
201+ elif is_user_context(context):
202+ result = session.query(models.Snapshot).\
203+ filter_by(project_id=context.project_id).\
204+ filter_by(id=snapshot_id).\
205+ filter_by(deleted=False).\
206+ first()
207+ if not result:
208+ raise exception.SnapshotNotFound(snapshot_id=snapshot_id)
209+
210+ return result
211+
212+
213+@require_admin_context
214+def snapshot_get_all(context):
215+ session = get_session()
216+ return session.query(models.Snapshot).\
217+ filter_by(deleted=can_read_deleted(context)).\
218+ all()
219+
220+
221+@require_context
222+def snapshot_get_all_by_project(context, project_id):
223+ authorize_project_context(context, project_id)
224+
225+ session = get_session()
226+ return session.query(models.Snapshot).\
227+ filter_by(project_id=project_id).\
228+ filter_by(deleted=can_read_deleted(context)).\
229+ all()
230+
231+
232+@require_context
233+def snapshot_update(context, snapshot_id, values):
234+ session = get_session()
235+ with session.begin():
236+ snapshot_ref = snapshot_get(context, snapshot_id, session=session)
237+ snapshot_ref.update(values)
238+ snapshot_ref.save(session=session)
239+
240+
241+###################
242+
243+
244+@require_context
245 def security_group_get_all(context):
246 session = get_session()
247 return session.query(models.SecurityGroup).\
248
249=== added file 'nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py'
250--- nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py 1970-01-01 00:00:00 +0000
251+++ nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py 2011-05-27 04:49:04 +0000
252@@ -0,0 +1,70 @@
253+# vim: tabstop=4 shiftwidth=4 softtabstop=4
254+
255+# Copyright 2011 MORITA Kazutaka.
256+# All Rights Reserved.
257+#
258+# Licensed under the Apache License, Version 2.0 (the "License"); you may
259+# not use this file except in compliance with the License. You may obtain
260+# a copy of the License at
261+#
262+# http://www.apache.org/licenses/LICENSE-2.0
263+#
264+# Unless required by applicable law or agreed to in writing, software
265+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
266+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
267+# License for the specific language governing permissions and limitations
268+# under the License.
269+
270+from sqlalchemy import Column, Table, MetaData
271+from sqlalchemy import Integer, DateTime, Boolean, String
272+
273+from nova import log as logging
274+
275+meta = MetaData()
276+
277+snapshots = Table('snapshots', meta,
278+ Column('created_at', DateTime(timezone=False)),
279+ Column('updated_at', DateTime(timezone=False)),
280+ Column('deleted_at', DateTime(timezone=False)),
281+ Column('deleted', Boolean(create_constraint=True, name=None)),
282+ Column('id', Integer(), primary_key=True, nullable=False),
283+ Column('volume_id', Integer(), nullable=False),
284+ Column('user_id',
285+ String(length=255, convert_unicode=False, assert_unicode=None,
286+ unicode_error=None, _warn_on_bytestring=False)),
287+ Column('project_id',
288+ String(length=255, convert_unicode=False, assert_unicode=None,
289+ unicode_error=None, _warn_on_bytestring=False)),
290+ Column('status',
291+ String(length=255, convert_unicode=False, assert_unicode=None,
292+ unicode_error=None, _warn_on_bytestring=False)),
293+ Column('progress',
294+ String(length=255, convert_unicode=False, assert_unicode=None,
295+ unicode_error=None, _warn_on_bytestring=False)),
296+ Column('volume_size', Integer()),
297+ Column('scheduled_at', DateTime(timezone=False)),
298+ Column('display_name',
299+ String(length=255, convert_unicode=False, assert_unicode=None,
300+ unicode_error=None, _warn_on_bytestring=False)),
301+ Column('display_description',
302+ String(length=255, convert_unicode=False, assert_unicode=None,
303+ unicode_error=None, _warn_on_bytestring=False)))
304+
305+
306+def upgrade(migrate_engine):
307+ # Upgrade operations go here. Don't create your own engine;
308+ # bind migrate_engine to your metadata
309+ meta.bind = migrate_engine
310+
311+ try:
312+ snapshots.create()
313+ except Exception:
314+ logging.info(repr(snapshots))
315+ logging.exception('Exception while creating table')
316+ meta.drop_all(tables=[snapshots])
317+ raise
318+
319+
320+def downgrade(migrate_engine):
321+ # Operations to reverse the above upgrade go here.
322+ snapshots.drop()
323
324=== modified file 'nova/db/sqlalchemy/models.py'
325--- nova/db/sqlalchemy/models.py 2011-05-17 20:50:12 +0000
326+++ nova/db/sqlalchemy/models.py 2011-05-27 04:49:04 +0000
327@@ -329,6 +329,31 @@
328 hard_limit = Column(Integer, nullable=True)
329
330
331+class Snapshot(BASE, NovaBase):
332+ """Represents a block storage device that can be attached to a vm."""
333+ __tablename__ = 'snapshots'
334+ id = Column(Integer, primary_key=True, autoincrement=True)
335+
336+ @property
337+ def name(self):
338+ return FLAGS.snapshot_name_template % self.id
339+
340+ @property
341+ def volume_name(self):
342+ return FLAGS.volume_name_template % self.volume_id
343+
344+ user_id = Column(String(255))
345+ project_id = Column(String(255))
346+
347+ volume_id = Column(Integer)
348+ status = Column(String(255))
349+ progress = Column(String(255))
350+ volume_size = Column(Integer)
351+
352+ display_name = Column(String(255))
353+ display_description = Column(String(255))
354+
355+
356 class ExportDevice(BASE, NovaBase):
357 """Represates a shelf and blade that a volume can be exported on."""
358 __tablename__ = 'export_devices'
359
360=== modified file 'nova/exception.py'
361--- nova/exception.py 2011-05-18 19:46:39 +0000
362+++ nova/exception.py 2011-05-27 04:49:04 +0000
363@@ -271,6 +271,14 @@
364 message = _("Volume not found for instance %(instance_id)s.")
365
366
367+class SnapshotNotFound(NotFound):
368+ message = _("Snapshot %(snapshot_id)s could not be found.")
369+
370+
371+class VolumeIsBusy(Error):
372+ message = _("deleting volume %(volume_name)s that has snapshot")
373+
374+
375 class ExportDeviceNotFoundForVolume(NotFound):
376 message = _("No export device found for volume %(volume_id)s.")
377
378
379=== modified file 'nova/tests/test_cloud.py'
380--- nova/tests/test_cloud.py 2011-05-25 22:43:04 +0000
381+++ nova/tests/test_cloud.py 2011-05-27 04:49:04 +0000
382@@ -186,6 +186,52 @@
383 db.service_destroy(self.context, service1['id'])
384 db.service_destroy(self.context, service2['id'])
385
386+ def test_describe_snapshots(self):
387+ """Makes sure describe_snapshots works and filters results."""
388+ vol = db.volume_create(self.context, {})
389+ snap1 = db.snapshot_create(self.context, {'volume_id': vol['id']})
390+ snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']})
391+ result = self.cloud.describe_snapshots(self.context)
392+ self.assertEqual(len(result['snapshotSet']), 2)
393+ snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x')
394+ result = self.cloud.describe_snapshots(self.context,
395+ snapshot_id=[snapshot_id])
396+ self.assertEqual(len(result['snapshotSet']), 1)
397+ self.assertEqual(
398+ ec2utils.ec2_id_to_id(result['snapshotSet'][0]['snapshotId']),
399+ snap2['id'])
400+ db.snapshot_destroy(self.context, snap1['id'])
401+ db.snapshot_destroy(self.context, snap2['id'])
402+ db.volume_destroy(self.context, vol['id'])
403+
404+ def test_create_snapshot(self):
405+ """Makes sure create_snapshot works."""
406+ vol = db.volume_create(self.context, {'status': "available"})
407+ volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
408+
409+ result = self.cloud.create_snapshot(self.context,
410+ volume_id=volume_id)
411+ snapshot_id = result['snapshotId']
412+ result = self.cloud.describe_snapshots(self.context)
413+ self.assertEqual(len(result['snapshotSet']), 1)
414+ self.assertEqual(result['snapshotSet'][0]['snapshotId'], snapshot_id)
415+
416+ db.snapshot_destroy(self.context, ec2utils.ec2_id_to_id(snapshot_id))
417+ db.volume_destroy(self.context, vol['id'])
418+
419+ def test_delete_snapshot(self):
420+ """Makes sure delete_snapshot works."""
421+ vol = db.volume_create(self.context, {'status': "available"})
422+ snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
423+ 'status': "available"})
424+ snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
425+
426+ result = self.cloud.delete_snapshot(self.context,
427+ snapshot_id=snapshot_id)
428+ self.assertTrue(result)
429+
430+ db.volume_destroy(self.context, vol['id'])
431+
432 def test_describe_instances(self):
433 """Makes sure describe_instances works and filters results."""
434 inst1 = db.instance_create(self.context, {'reservation_id': 'a',
435
436=== modified file 'nova/tests/test_volume.py'
437--- nova/tests/test_volume.py 2011-04-21 19:50:04 +0000
438+++ nova/tests/test_volume.py 2011-05-27 04:49:04 +0000
439@@ -176,6 +176,34 @@
440 # This will allow us to test cross-node interactions
441 pass
442
443+ @staticmethod
444+ def _create_snapshot(volume_id, size='0'):
445+ """Create a snapshot object."""
446+ snap = {}
447+ snap['volume_size'] = size
448+ snap['user_id'] = 'fake'
449+ snap['project_id'] = 'fake'
450+ snap['volume_id'] = volume_id
451+ snap['status'] = "creating"
452+ return db.snapshot_create(context.get_admin_context(), snap)['id']
453+
454+ def test_create_delete_snapshot(self):
455+ """Test snapshot can be created and deleted."""
456+ volume_id = self._create_volume()
457+ self.volume.create_volume(self.context, volume_id)
458+ snapshot_id = self._create_snapshot(volume_id)
459+ self.volume.create_snapshot(self.context, volume_id, snapshot_id)
460+ self.assertEqual(snapshot_id,
461+ db.snapshot_get(context.get_admin_context(),
462+ snapshot_id).id)
463+
464+ self.volume.delete_snapshot(self.context, snapshot_id)
465+ self.assertRaises(exception.NotFound,
466+ db.snapshot_get,
467+ self.context,
468+ snapshot_id)
469+ self.volume.delete_volume(self.context, volume_id)
470+
471
472 class DriverTestCase(test.TestCase):
473 """Base Test class for Drivers."""
474
475=== modified file 'nova/volume/api.py'
476--- nova/volume/api.py 2011-03-31 08:39:00 +0000
477+++ nova/volume/api.py 2011-05-27 04:49:04 +0000
478@@ -90,6 +90,15 @@
479 return self.db.volume_get_all(context)
480 return self.db.volume_get_all_by_project(context, context.project_id)
481
482+ def get_snapshot(self, context, snapshot_id):
483+ rv = self.db.snapshot_get(context, snapshot_id)
484+ return dict(rv.iteritems())
485+
486+ def get_all_snapshots(self, context):
487+ if context.is_admin:
488+ return self.db.snapshot_get_all(context)
489+ return self.db.snapshot_get_all_by_project(context, context.project_id)
490+
491 def check_attach(self, context, volume_id):
492 volume = self.get(context, volume_id)
493 # TODO(vish): abstract status checking?
494@@ -110,3 +119,38 @@
495 self.db.queue_get_for(context, FLAGS.compute_topic, host),
496 {"method": "remove_volume",
497 "args": {'volume_id': volume_id}})
498+
499+ def create_snapshot(self, context, volume_id, name, description):
500+ volume = self.get(context, volume_id)
501+ if volume['status'] != "available":
502+ raise exception.ApiError(_("Volume status must be available"))
503+
504+ options = {
505+ 'volume_id': volume_id,
506+ 'user_id': context.user_id,
507+ 'project_id': context.project_id,
508+ 'status': "creating",
509+ 'progress': '0%',
510+ 'volume_size': volume['size'],
511+ 'display_name': name,
512+ 'display_description': description}
513+
514+ snapshot = self.db.snapshot_create(context, options)
515+ rpc.cast(context,
516+ FLAGS.scheduler_topic,
517+ {"method": "create_snapshot",
518+ "args": {"topic": FLAGS.volume_topic,
519+ "volume_id": volume_id,
520+ "snapshot_id": snapshot['id']}})
521+ return snapshot
522+
523+ def delete_snapshot(self, context, snapshot_id):
524+ snapshot = self.get_snapshot(context, snapshot_id)
525+ if snapshot['status'] != "available":
526+ raise exception.ApiError(_("Snapshot status must be available"))
527+ self.db.snapshot_update(context, snapshot_id, {'status': 'deleting'})
528+ rpc.cast(context,
529+ FLAGS.scheduler_topic,
530+ {"method": "delete_snapshot",
531+ "args": {"topic": FLAGS.volume_topic,
532+ "snapshot_id": snapshot_id}})
533
534=== modified file 'nova/volume/driver.py'
535--- nova/volume/driver.py 2011-04-13 17:19:25 +0000
536+++ nova/volume/driver.py 2011-05-27 04:49:04 +0000
537@@ -90,42 +90,91 @@
538 raise exception.Error(_("volume group %s doesn't exist")
539 % FLAGS.volume_group)
540
541- def create_volume(self, volume):
542- """Creates a logical volume. Can optionally return a Dictionary of
543- changes to the volume object to be persisted."""
544- if int(volume['size']) == 0:
545- sizestr = '100M'
546- else:
547- sizestr = '%sG' % volume['size']
548+ def _create_volume(self, volume_name, sizestr):
549 self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',
550- volume['name'],
551- FLAGS.volume_group)
552-
553- def delete_volume(self, volume):
554- """Deletes a logical volume."""
555+ volume_name, FLAGS.volume_group)
556+
557+ def _copy_volume(self, srcstr, deststr, size_in_g):
558+ self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,
559+ 'count=%d' % (size_in_g * 1024), 'bs=1M')
560+
561+ def _volume_not_present(self, volume_name):
562+ path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
563 try:
564- self._try_execute('sudo', 'lvdisplay',
565- '%s/%s' %
566- (FLAGS.volume_group,
567- volume['name']))
568+ self._try_execute('sudo', 'lvdisplay', path_name)
569 except Exception as e:
570- # If the volume isn't present, then don't attempt to delete
571+ # If the volume isn't present
572 return True
573+ return False
574
575+ def _delete_volume(self, volume, size_in_g):
576+ """Deletes a logical volume."""
577 # zero out old volumes to prevent data leaking between users
578 # TODO(ja): reclaiming space should be done lazy and low priority
579- self._execute('sudo', 'dd', 'if=/dev/zero',
580- 'of=%s' % self.local_path(volume),
581- 'count=%d' % (volume['size'] * 1024),
582- 'bs=1M')
583+ self._copy_volume('/dev/zero', self.local_path(volume), size_in_g)
584 self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %
585 (FLAGS.volume_group,
586- volume['name']))
587+ self._escape_snapshot(volume['name'])))
588+
589+ def _sizestr(self, size_in_g):
590+ if int(size_in_g) == 0:
591+ return '100M'
592+ return '%sG' % size_in_g
593+
594+ # Linux LVM reserves name that starts with snapshot, so that
595+ # such volume name can't be created. Mangle it.
596+ def _escape_snapshot(self, snapshot_name):
597+ if not snapshot_name.startswith('snapshot'):
598+ return snapshot_name
599+ return '_' + snapshot_name
600+
601+ def create_volume(self, volume):
602+ """Creates a logical volume. Can optionally return a Dictionary of
603+ changes to the volume object to be persisted."""
604+ self._create_volume(volume['name'], self._sizestr(volume['size']))
605+
606+ def delete_volume(self, volume):
607+ """Deletes a logical volume."""
608+ if self._volume_not_present(volume['name']):
609+ # If the volume isn't present, then don't attempt to delete
610+ return True
611+
612+ # TODO(yamahata): lvm can't delete origin volume only without
613+ # deleting derived snapshots. Can we do something fancy?
614+ out, err = self._execute('sudo', 'lvdisplay', '--noheading',
615+ '-C', '-o', 'Attr',
616+ '%s/%s' % (FLAGS.volume_group,
617+ volume['name']))
618+ # fake_execute returns None resulting unit test error
619+ if out:
620+ out = out.strip()
621+ if (out[0] == 'o') or (out[0] == 'O'):
622+ raise exception.VolumeIsBusy(volume_name=volume['name'])
623+
624+ self._delete_volume(volume, volume['size'])
625+
626+ def create_snapshot(self, snapshot):
627+ """Creates a snapshot."""
628+ orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
629+ self._try_execute('sudo', 'lvcreate', '-L',
630+ self._sizestr(snapshot['volume_size']),
631+ '--name', self._escape_snapshot(snapshot['name']),
632+ '--snapshot', orig_lv_name)
633+
634+ def delete_snapshot(self, snapshot):
635+ """Deletes a snapshot."""
636+ if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
637+ # If the snapshot isn't present, then don't attempt to delete
638+ return True
639+
640+ # TODO(yamahata): zeroing out the whole snapshot triggers COW.
641+ # it's quite slow.
642+ self._delete_volume(snapshot, snapshot['volume_size'])
643
644 def local_path(self, volume):
645 # NOTE(vish): stops deprecation warning
646 escaped_group = FLAGS.volume_group.replace('-', '--')
647- escaped_name = volume['name'].replace('-', '--')
648+ escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
649 return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
650
651 def ensure_export(self, context, volume):
652@@ -559,6 +608,18 @@
653 self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
654 'rm', volume['name'])
655
656+ def create_snapshot(self, snapshot):
657+ """Creates an rbd snapshot"""
658+ self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
659+ 'snap', 'create', '--snap', snapshot['name'],
660+ snapshot['volume_name'])
661+
662+ def delete_snapshot(self, snapshot):
663+ """Deletes an rbd snapshot"""
664+ self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
665+ 'snap', 'rm', '--snap', snapshot['name'],
666+ snapshot['volume_name'])
667+
668 def local_path(self, volume):
669 """Returns the path of the rbd volume."""
670 # This is the same as the remote path
671@@ -600,18 +661,24 @@
672
673 def create_volume(self, volume):
674 """Creates a sheepdog volume"""
675- if int(volume['size']) == 0:
676- sizestr = '100M'
677- else:
678- sizestr = '%sG' % volume['size']
679 self._try_execute('qemu-img', 'create',
680 "sheepdog:%s" % volume['name'],
681- sizestr)
682+ self._sizestr(volume['size']))
683
684 def delete_volume(self, volume):
685 """Deletes a logical volume"""
686 self._try_execute('collie', 'vdi', 'delete', volume['name'])
687
688+ def create_snapshot(self, snapshot):
689+ """Creates a sheepdog snapshot"""
690+ self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
691+ "sheepdog:%s" % snapshot['volume_name'])
692+
693+ def delete_snapshot(self, snapshot):
694+ """Deletes a sheepdog snapshot"""
695+ self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
696+ '-s', snapshot['name'])
697+
698 def local_path(self, volume):
699 return "sheepdog:%s" % volume['name']
700
701
702=== modified file 'nova/volume/manager.py'
703--- nova/volume/manager.py 2011-03-17 13:35:00 +0000
704+++ nova/volume/manager.py 2011-05-27 04:49:04 +0000
705@@ -142,6 +142,12 @@
706 self.driver.remove_export(context, volume_ref)
707 LOG.debug(_("volume %s: deleting"), volume_ref['name'])
708 self.driver.delete_volume(volume_ref)
709+ except exception.VolumeIsBusy, e:
710+ LOG.debug(_("volume %s: volume is busy"), volume_ref['name'])
711+ self.driver.ensure_export(context, volume_ref)
712+ self.db.volume_update(context, volume_ref['id'],
713+ {'status': 'available'})
714+ return True
715 except Exception:
716 self.db.volume_update(context,
717 volume_ref['id'],
718@@ -152,6 +158,49 @@
719 LOG.debug(_("volume %s: deleted successfully"), volume_ref['name'])
720 return True
721
722+ def create_snapshot(self, context, volume_id, snapshot_id):
723+ """Creates and exports the snapshot."""
724+ context = context.elevated()
725+ snapshot_ref = self.db.snapshot_get(context, snapshot_id)
726+ LOG.info(_("snapshot %s: creating"), snapshot_ref['name'])
727+
728+ try:
729+ snap_name = snapshot_ref['name']
730+ LOG.debug(_("snapshot %(snap_name)s: creating") % locals())
731+ model_update = self.driver.create_snapshot(snapshot_ref)
732+ if model_update:
733+ self.db.snapshot_update(context, snapshot_ref['id'],
734+ model_update)
735+
736+ except Exception:
737+ self.db.snapshot_update(context,
738+ snapshot_ref['id'], {'status': 'error'})
739+ raise
740+
741+ self.db.snapshot_update(context,
742+ snapshot_ref['id'], {'status': 'available',
743+ 'progress': '100%'})
744+ LOG.debug(_("snapshot %s: created successfully"), snapshot_ref['name'])
745+ return snapshot_id
746+
747+ def delete_snapshot(self, context, snapshot_id):
748+ """Deletes and unexports snapshot."""
749+ context = context.elevated()
750+ snapshot_ref = self.db.snapshot_get(context, snapshot_id)
751+
752+ try:
753+ LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
754+ self.driver.delete_snapshot(snapshot_ref)
755+ except Exception:
756+ self.db.snapshot_update(context,
757+ snapshot_ref['id'],
758+ {'status': 'error_deleting'})
759+ raise
760+
761+ self.db.snapshot_destroy(context, snapshot_id)
762+ LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name'])
763+ return True
764+
765 def setup_compute_volume(self, context, volume_id):
766 """Setup remote volume on compute host.
767