Merge lp:~morita-kazutaka/nova/snapshot-volume into lp:~hudson-openstack/nova/trunk
- snapshot-volume
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Snapshot, Clone and Boot from volumes
(Undefined)
|
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 |
Commit message
Description of the change
This adds a volume snapshot support with the EC2 api.
Mark Washenberger (markwash) wrote : | # |
- 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.
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:/
Thanks,
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:/
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.
Vish Ishaya (vishvananda) wrote : | # |
Agreed. This all looks excellent to me. We can refactor for no-db-messaging later.
Mark Washenberger (markwash) wrote : | # |
I ran this through our smoke tests and it comes out clean. So I'm happy.
Jay Pipes (jaypipes) wrote : | # |
Agreed, this work looks excellent.
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_
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
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_
> 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_
I've also fixed pep8 errors.
Thanks,
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_
> > 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_
>
> I've also fixed pep8 errors.
Sounds good to me.
Preview Diff
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 |
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.