Merge lp:~johannes.erdfelt/glance/image-undelete into lp:~hudson-openstack/glance/trunk

Proposed by Johannes Erdfelt
Status: Rejected
Rejected by: Brian Waldon
Proposed branch: lp:~johannes.erdfelt/glance/image-undelete
Merge into: lp:~hudson-openstack/glance/trunk
Diff against target: 643 lines (+399/-11)
17 files modified
.mailmap (+1/-0)
bin/glance (+27/-1)
glance/api/v1/__init__.py (+2/-1)
glance/api/v1/images.py (+36/-2)
glance/client.py (+8/-0)
glance/registry/__init__.py (+6/-0)
glance/registry/client.py (+8/-0)
glance/registry/db/api.py (+25/-0)
glance/registry/db/migrate_repo/versions/008_add_saved_status.py (+41/-0)
glance/registry/db/models.py (+1/-0)
glance/registry/server.py (+31/-1)
glance/store/__init__.py (+7/-6)
tests/functional/test_bin_glance.py (+85/-0)
tests/functional/test_httplib2_api.py (+63/-0)
tests/functional/test_scrubber.py (+28/-0)
tests/stubs.py (+10/-0)
tests/unit/test_clients.py (+20/-0)
To merge this branch: bzr merge lp:~johannes.erdfelt/glance/image-undelete
Reviewer Review Type Date Requested Status
Brian Waldon (community) Approve
Josh Kearney (community) Approve
Review via email: mp+69161@code.launchpad.net

Commit message

Add image restore (undelete) functionality to Glance

Description of the change

Delayed delete is only as useful as being able to undelete images if you've determined that you didn't really want to delete it.

To post a comment you must log in.
Revision history for this message
Josh Kearney (jk0) wrote :

*golf clap*

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

This will definitely be useful once delayed_delete is in, however, I did notice a few things:

1) There isn't very much testing around this feature. Can you more thoroughly test this at each level it is exposed?

2) The undelete action in db/api.py sets the status to 'active'. I don't think this is the best practice, as it could be undeleting an image with status 'killed' or 'saving'.

3) Can we call it 'restore' instead of 'undelete'? I wouldn't want 'delete' to be 'unadd'...

4) I'm not crazy about member actions in the API (i.e. POST /images/<id>/undelete). Can you rework it to use a PUT? Not sure what the best option would be...

review: Needs Fixing
Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

1) Can you be more detailed what extra testing you think is necessary?

2) I'll work on saving the previous status and restoring that value.

3) I agree that 'restore' is a better term. I'll make those changes shortly as well.

4) I can't say I'm 100% pleased with the /undelete member (as well as /restore). However, I think PUT is worse. PUT stores the request body at the resource specified. Restoring a deleted image doesn't update the body of the resource, just makes metadata changes.

Maybe /images/<id>/actions (ala nova) or /images/restore (taking image id as a POST variable) would be better choices?

There's also the alternative of making this more a client side operation, by allowing clients to retrieve the original status and then making the client use existing API entry points to restore the status. It would be trickier from an implementation standpoint since the existing code returns 404 for deleted (but not purged) images.

It could be that images that are pending delete still let metadata updates, but they aren't returned in list operations.

I think this one of those REST impedance mismatch issues so we'll need to be creative.

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

> 1) Can you be more detailed what extra testing you think is necessary?

I think it is very necessary to test the glance cli tool, the api client, the api itself, the registry client, and the registy api. Each layer needs to be tested.

> 2) I'll work on saving the previous status and restoring that value.

Sounds good.

> 3) I agree that 'restore' is a better term. I'll make those changes shortly as
> well.

Excellent. If you have a better suggestion, I would love to hear it :)

> 4) I can't say I'm 100% pleased with the /undelete member (as well as
> /restore). However, I think PUT is worse. PUT stores the request body at the
> resource specified. Restoring a deleted image doesn't update the body of the
> resource, just makes metadata changes.
>
> Maybe /images/<id>/actions (ala nova) or /images/restore (taking image id as a
> POST variable) would be better choices?
>
> There's also the alternative of making this more a client side operation, by
> allowing clients to retrieve the original status and then making the client
> use existing API entry points to restore the status. It would be trickier from
> an implementation standpoint since the existing code returns 404 for deleted
> (but not purged) images.
>
> It could be that images that are pending delete still let metadata updates,
> but they aren't returned in list operations.
>
> I think this one of those REST impedance mismatch issues so we'll need to be
> creative.

I agree there isn't an obvious solution, so we will have to get creative. /images/<id>/action sounds best to me. Also, I'm thinking we should 404 on 'pending_delete', disallowing metadata updates. It should essentially be deleted.

Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

Ok, this should now have all of the changes bcwaldon requested.

194. By Johannes Erdfelt

Merge with trunk

195. By Johannes Erdfelt

Merge with trunk

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

Testing looks great. Two minor nits:
1) Can you align restore_image and image_restore?
2) can you move /images/<id>/actions to /images/<id>/action? Nova uses 'action' for this kind of thing, while it uses 'actions' for admin-only reporting functionality

196. By Johannes Erdfelt

Merge with trunk

197. By Johannes Erdfelt

actions -> action

Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

I've pushed up a fix that changes 'actions' to 'action'.

As far as restore_image vs image_restore, glance as a whole is very inconsistent. For instance, the client APIs generally follow <action>_<object>, whereas the DB API and some other code follow <object>_<action>.

I followed the surrounding code to determine which naming scheme to follow. My branch should be consistent within each module.

I'm indifferent which one is better, but I don't want this branch to explode in scope to fixing naming to be consistent within all of Glance. That's probably better done in another branch.

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

I'm cool with the naming, and thanks for updating the action collection. Looks good.

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (38.2 KiB)

The attempt to merge lp:~johannes.erdfelt/glance/image-undelete into lp:glance failed. Below is the output from the failed tests.

running test
running egg_info
creating glance.egg-info
writing glance.egg-info/PKG-INFO
writing top-level names to glance.egg-info/top_level.txt
writing dependency_links to glance.egg-info/dependency_links.txt
writing manifest file 'glance.egg-info/SOURCES.txt'
reading manifest file 'glance.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching 'ChangeLog'
writing manifest file 'glance.egg-info/SOURCES.txt'
running build_ext

We test the following: ... ok
We test the following: ... ok
Test for LP Bugs #736295, #767203 ... ok
We test the following: ... ok
We test conditions that produced LP Bug #768969, where an image ... ok
Set up three test images and ensure each query param filter works ... ok
We test the following sequential series of actions: ... ok
Ensure marker and limit query params work ... ok
Set up three test images and ensure each query param filter works ... ok
We test the process flow where a user registers an image ... ok
A test against the actual datastore backend for the registry ... ok
A test that errors coming from the POST API do not ... ok
We test that various calls to the images and root endpoints are ... ok
Upload image, delete it, then restore it ... ok
Upload initial image, then attempt to upload duplicate image ... ok
Set up four test images and ensure each query param filter works ... ok
We test the following sequential series of actions: ... ok
Ensure marker and limit query params work ... ok
Set up three test images and ensure each query param filter works ... ok
We test the process flow where a user registers an image ... ok
A test against the actual datastore backend for the registry ... ok
A test that errors coming from the POST API do not ... ok
We test that various calls to the images and root endpoints are ... ok
Test logging output proper when verbose and debug ... ok
Test logging output proper when verbose and debug ... ok
A test for LP bug #704854 -- Exception thrown by registry ... ok
We test the following: ... ok
We test the following: ... ok
test that images don't get deleted immediatly and that the scrubber ... ok
test that images get deleted immediately by default ... ok
test that images can be restored after delayed delete ... ok
Tests raises BadRequest for invalid store header ... ok
Tests to add a basic image in the file store ... ok
Tests creates a queued image for no body and no loc header ... ok
Tests creates a queued image for no body and no loc header ... ok
Test that the image contents are checksummed properly ... ok
test_bad_container_format (tests.unit.test_api.TestGlanceAPI) ... ok
test_bad_disk_format (tests.unit.test_api.TestGlanceAPI) ... ok
test_delete_image (tests.unit.test_api.TestGlanceAPI) ... ok
test_delete_non_exists_image (tests.unit.test_api.TestGlanceAPI) ... ok
Here, we try to delete an image that is in the queued state. ... ok
Test that the ETag header matches the x-image-meta-checksum ... ok
Tests that the /images/detail registry API returns a 400 ... ok
Tests that the /images registry API returns list of ...

198. By Johannes Erdfelt

Merge with trunk

Revision history for this message
Johannes Erdfelt (johannes.erdfelt) wrote :

Yeah, so the unit tests work fine on my development system. I even spent a while trying to figure out a way to cause the tests to fail like they did on the jenkins server.

Should we just assume it was a transient failure and try to merge again?

Unmerged revisions

198. By Johannes Erdfelt

Merge with trunk

197. By Johannes Erdfelt

actions -> action

196. By Johannes Erdfelt

Merge with trunk

195. By Johannes Erdfelt

Merge with trunk

194. By Johannes Erdfelt

Merge with trunk

193. By Johannes Erdfelt

Add test for bin/glance, the HTTP API and the registry client

192. By Johannes Erdfelt

Fix debug message

191. By Johannes Erdfelt

Save status of image since restoring an image may not always mean setting it
to active

190. By Johannes Erdfelt

Clarify help for restore command

189. By Johannes Erdfelt

Fix PEP8 warning

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.mailmap'
2--- .mailmap 2011-04-27 18:00:49 +0000
3+++ .mailmap 2011-08-01 18:29:24 +0000
4@@ -3,6 +3,7 @@
5 # <preferred e-mail> <other e-mail 2>
6 <corywright@gmail.com> <cory.wright@rackspace.com>
7 <jsuh@isi.edu> <jsuh@bespin>
8+<johannes.erdfelt@rackspace.com> <johannes@squeeze>
9 <josh@jk0.org> <josh.kearney@rackspace.com>
10 <rconradharris@gmail.com> <rick.harris@rackspace.com>
11 <rconradharris@gmail.com> <rick@quasar.racklabs.com>
12
13=== modified file 'bin/glance'
14--- bin/glance 2011-07-25 22:01:26 +0000
15+++ bin/glance 2011-08-01 18:29:24 +0000
16@@ -366,6 +366,29 @@
17 return FAILURE
18
19
20+def image_restore(options, args):
21+ """
22+%(prog)s restore [options] <ID>
23+
24+Restores a previously deleted image from Glance"""
25+ try:
26+ image_id = args.pop()
27+ except IndexError:
28+ print "Please specify the ID of the image you wish to restore "
29+ print "as the first argument"
30+ return FAILURE
31+
32+ c = get_client(options)
33+
34+ try:
35+ c.restore_image(image_id)
36+ print "Restored image %s" % image_id
37+ return SUCCESS
38+ except exception.NotFound:
39+ print "No image with ID %s was found" % image_id
40+ return FAILURE
41+
42+
43 def image_show(options, args):
44 """
45 %(prog)s show [options] <ID>
46@@ -816,7 +839,8 @@
47 'index': images_index,
48 'details': images_detailed,
49 'show': image_show,
50- 'clear': images_clear}
51+ 'clear': images_clear,
52+ 'restore': image_restore}
53
54 CACHE_COMMANDS = {
55 'cache-index': cache_index,
56@@ -878,6 +902,8 @@
57
58 delete Deletes an image from Glance
59
60+ restore Restores a previously deleted image from Glance
61+
62 index Return brief information about images in Glance
63
64 details Return detailed information about images in
65
66=== modified file 'glance/api/v1/__init__.py'
67--- glance/api/v1/__init__.py 2011-07-21 23:22:18 +0000
68+++ glance/api/v1/__init__.py 2011-08-01 18:29:24 +0000
69@@ -34,7 +34,8 @@
70 mapper = routes.Mapper()
71 resource = images.create_resource(options)
72 mapper.resource("image", "images", controller=resource,
73- collection={'detail': 'GET'})
74+ collection={'detail': 'GET'},
75+ member={'action': 'POST'})
76 mapper.connect("/", controller=resource, action="index")
77 mapper.connect("/images/{id}", controller=resource, action="meta",
78 conditions=dict(method=["HEAD"]))
79
80=== modified file 'glance/api/v1/images.py'
81--- glance/api/v1/images.py 2011-07-28 23:01:40 +0000
82+++ glance/api/v1/images.py 2011-08-01 18:29:24 +0000
83@@ -68,6 +68,8 @@
84 PUT /images/<ID> -- Update image metadata and/or upload image
85 data for a previously-reserved image
86 DELETE /images/<ID> -- Delete the image with id <ID>
87+ POST /images/<ID>/action -- Perform action on image with id <ID>
88+ (only 'restore' right now)
89 """
90
91 def __init__(self, options):
92@@ -557,10 +559,42 @@
93 # to delete the image if the backend doesn't yet store it.
94 # See https://bugs.launchpad.net/glance/+bug/747799
95 if image['location']:
96- schedule_delete_from_backend(image['location'], self.options,
97- req.context, id)
98+ schedule_delete_from_backend(self.options, req.context, image)
99 registry.delete_image_metadata(self.options, req.context, id)
100
101+ def action(self, req, id, body):
102+ """
103+ Performs action on image
104+
105+ :param request: The WSGI/Webob Request object
106+ :retval There is no response body
107+
108+ :raises HttpBadRequest if image registry is invalid
109+ :raises HttpNotFound if image or any chunk is not available
110+ :raises HttpNotAuthorized if image or any chunk is not
111+ deleteable by the requesting user
112+ """
113+ if req.context.read_only:
114+ msg = "Read-only access"
115+ logger.debug(msg)
116+ raise HTTPForbidden(msg, request=req,
117+ content_type="text/plain")
118+
119+ action = body['action']
120+ if action == 'restore':
121+ try:
122+ registry.restore_image(self.options, req.context, id)
123+ except exception.NotFound:
124+ msg = "Image with identifier %s not found" % id
125+ logger.debug(msg)
126+ raise HTTPNotFound(msg, request=req,
127+ content_type='text/plain')
128+ else:
129+ msg = "Unknown action %s" % action
130+ raise HTTPBadRequest(msg, request=req, content_type='text/plain')
131+
132+ return ''
133+
134 def get_store_or_400(self, request, store_name):
135 """
136 Grabs the storage backend for the supplied store name
137
138=== modified file 'glance/client.py'
139--- glance/client.py 2011-07-25 21:59:56 +0000
140+++ glance/client.py 2011-08-01 18:29:24 +0000
141@@ -164,6 +164,14 @@
142 self.do_request("DELETE", "/images/%s" % image_id)
143 return True
144
145+ def restore_image(self, image_id):
146+ """
147+ Restores an image that is in pending_delete
148+ """
149+ self.do_request("POST", "/images/%s/action" % image_id,
150+ json.dumps({"action": "restore"}))
151+ return True
152+
153 def get_cached_images(self, **kwargs):
154 """
155 Returns a list of images stored in the image cache.
156
157=== modified file 'glance/registry/__init__.py'
158--- glance/registry/__init__.py 2011-07-20 22:53:44 +0000
159+++ glance/registry/__init__.py 2011-08-01 18:29:24 +0000
160@@ -86,6 +86,12 @@
161 return c.delete_image(image_id)
162
163
164+def restore_image(options, context, image_id):
165+ logger.debug("Restoring image %s...", image_id)
166+ c = get_registry_client(options, context)
167+ return c.restore_image(image_id)
168+
169+
170 def _debug_print_metadata(image_meta):
171 data = image_meta.copy()
172 properties = data.pop('properties', None)
173
174=== modified file 'glance/registry/client.py'
175--- glance/registry/client.py 2011-07-20 22:53:44 +0000
176+++ glance/registry/client.py 2011-08-01 18:29:24 +0000
177@@ -126,3 +126,11 @@
178 """
179 self.do_request("DELETE", "/images/%s" % image_id)
180 return True
181+
182+ def restore_image(self, image_id):
183+ """
184+ Restores an image that is pending deletion
185+ """
186+ self.do_request("POST", "/images/%s/action" % image_id,
187+ json.dumps({"action": "restore"}))
188+ return True
189
190=== modified file 'glance/registry/db/api.py'
191--- glance/registry/db/api.py 2011-07-23 03:38:20 +0000
192+++ glance/registry/db/api.py 2011-08-01 18:29:24 +0000
193@@ -118,6 +118,31 @@
194 image_property_delete(context, prop_ref, session=session)
195
196
197+def image_restore(context, image_id):
198+ """Restore image that is pending deletion.
199+
200+ Raises NotFound if image does not exist.
201+
202+ """
203+ session = get_session()
204+ with session.begin():
205+ try:
206+ image_ref = session.query(models.Image).\
207+ options(joinedload(models.Image.properties)).\
208+ filter_by(id=image_id).\
209+ one()
210+ except exc.NoResultFound:
211+ raise exception.NotFound("No image found with ID %s" % image_id)
212+
213+ if image_ref['status'] != 'pending_delete':
214+ raise exception.ApiError('Image not in pending_delete state', 400)
215+
216+ image_ref.update({'status': image_ref['saved_status'],
217+ 'saved_status': None,
218+ 'deleted': False})
219+ image_ref.save(session=session)
220+
221+
222 def image_get(context, image_id, session=None):
223 """Get an image or raise if it does not exist."""
224 session = session or get_session()
225
226=== added file 'glance/registry/db/migrate_repo/versions/008_add_saved_status.py'
227--- glance/registry/db/migrate_repo/versions/008_add_saved_status.py 1970-01-01 00:00:00 +0000
228+++ glance/registry/db/migrate_repo/versions/008_add_saved_status.py 2011-08-01 18:29:24 +0000
229@@ -0,0 +1,41 @@
230+# vim: tabstop=4 shiftwidth=4 softtabstop=4
231+
232+# Copyright 2011 OpenStack LLC.
233+# All Rights Reserved.
234+#
235+# Licensed under the Apache License, Version 2.0 (the "License"); you may
236+# not use this file except in compliance with the License. You may obtain
237+# a copy of the License at
238+#
239+# http://www.apache.org/licenses/LICENSE-2.0
240+#
241+# Unless required by applicable law or agreed to in writing, software
242+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
243+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
244+# License for the specific language governing permissions and limitations
245+# under the License.
246+
247+from sqlalchemy import MetaData, Table, Column, String
248+
249+
250+meta = MetaData()
251+
252+saved_status = Column('saved_status', String(30))
253+
254+
255+def upgrade(migrate_engine):
256+ meta.bind = migrate_engine
257+
258+ images = Table('images', meta, autoload=True,
259+ autoload_with=migrate_engine)
260+
261+ images.create_column(saved_status)
262+
263+
264+def downgrade(migrate_engine):
265+ meta.bind = migrate_engine
266+
267+ images = Table('images', meta, autoload=True,
268+ autoload_with=migrate_engine)
269+
270+ images.drop_column(saved_status)
271
272=== modified file 'glance/registry/db/models.py'
273--- glance/registry/db/models.py 2011-07-20 20:27:33 +0000
274+++ glance/registry/db/models.py 2011-08-01 18:29:24 +0000
275@@ -102,6 +102,7 @@
276 container_format = Column(String(20))
277 size = Column(BigInteger)
278 status = Column(String(30), nullable=False)
279+ saved_status = Column(String(30))
280 is_public = Column(Boolean, nullable=False, default=False)
281 location = Column(Text)
282 checksum = Column(String(32))
283
284=== modified file 'glance/registry/server.py'
285--- glance/registry/server.py 2011-07-22 23:35:24 +0000
286+++ glance/registry/server.py 2011-08-01 18:29:24 +0000
287@@ -345,6 +345,35 @@
288 request=req,
289 content_type='text/plain')
290
291+ def action(self, req, id, body):
292+ """Restores image pending deletion.
293+
294+ :param req: Request body. Ignored.
295+ :param id: The opaque internal identifier for the image
296+
297+ :retval Returns the updated image information as a mapping,
298+
299+ """
300+ context = None
301+ action = body['action']
302+ if action == 'restore':
303+ try:
304+ logger.debug("Restoring image %(id)s" % locals())
305+ db_api.image_restore(context, id)
306+ return ''
307+ except exception.Invalid, e:
308+ msg = "Failed to restore image."
309+ logger.error(msg)
310+ return exc.HTTPBadRequest(msg)
311+ except exception.NotFound:
312+ raise exc.HTTPNotFound(body='Image not found',
313+ request=req,
314+ content_type='text/plain')
315+ else:
316+ msg = "Unknown action %s." % action
317+ logger.error(msg)
318+ return exc.HTTPBadRequest(msg)
319+
320
321 def create_resource(options):
322 """Images resource factory method."""
323@@ -360,7 +389,8 @@
324 mapper = routes.Mapper()
325 resource = create_resource(options)
326 mapper.resource("image", "images", controller=resource,
327- collection={'detail': 'GET'})
328+ collection={'detail': 'GET'},
329+ member={'action': 'POST'})
330 mapper.connect("/", controller=resource, action="index")
331 super(API, self).__init__(mapper)
332
333
334=== modified file 'glance/store/__init__.py'
335--- glance/store/__init__.py 2011-07-28 23:01:40 +0000
336+++ glance/store/__init__.py 2011-08-01 18:29:24 +0000
337@@ -111,20 +111,21 @@
338 return loc.store_name
339
340
341-def schedule_delete_from_backend(uri, options, context, id, **kwargs):
342+def schedule_delete_from_backend(options, context, image, **kwargs):
343 """
344 Given a uri and a time, schedule the deletion of an image.
345 """
346 use_delay = config.get_option(options, 'delayed_delete', type='bool',
347 default=False)
348 if not use_delay:
349- registry.update_image_metadata(options, context, id,
350+ registry.update_image_metadata(options, context, image['id'],
351 {'status': 'deleted'})
352 try:
353- return delete_from_backend(uri, **kwargs)
354+ return delete_from_backend(image['location'], **kwargs)
355 except (UnsupportedBackend, exception.NotFound):
356 msg = "Failed to delete image from store (%s). "
357- logger.error(msg % uri)
358+ logger.error(msg % image['location'])
359
360- registry.update_image_metadata(options, context, id,
361- {'status': 'pending_delete'})
362+ registry.update_image_metadata(options, context, image['id'],
363+ {'status': 'pending_delete',
364+ 'saved_status': image['status']})
365
366=== modified file 'tests/functional/test_bin_glance.py'
367--- tests/functional/test_bin_glance.py 2011-06-28 14:37:31 +0000
368+++ tests/functional/test_bin_glance.py 2011-08-01 18:29:24 +0000
369@@ -288,3 +288,88 @@
370 self.assertEqual(0, rec[0])
371
372 self.stop_servers()
373+
374+ def test_delete_restore(self):
375+ """
376+ We test the following:
377+
378+ 0. Verify no public images in index
379+ 1. Add a public image with a location attr
380+ and no image data
381+ 2. Check that image exists in index
382+ 3. Delete the image
383+ 4. Verify no longer in index
384+ 5. Restore the image
385+ 6. Check that image exists in index
386+ """
387+ self.cleanup()
388+ self.start_servers(delayed_delete=True)
389+
390+ api_port = self.api_port
391+ registry_port = self.registry_port
392+
393+ # 0. Verify no public images
394+ cmd = "bin/glance --port=%d index" % api_port
395+
396+ exitcode, out, err = execute(cmd)
397+
398+ self.assertEqual(0, exitcode)
399+ self.assertEqual('No public images found.', out.strip())
400+
401+ # 1. Add public image
402+ cmd = "bin/glance --port=%d add is_public=True name=MyImage" % api_port
403+
404+ exitcode, out, err = execute(cmd)
405+
406+ self.assertEqual(0, exitcode)
407+ self.assertEqual('Added new image with ID: 1', out.strip())
408+
409+ # 2. Verify image added as public image
410+ cmd = "bin/glance --port=%d index" % api_port
411+
412+ exitcode, out, err = execute(cmd)
413+
414+ self.assertEqual(0, exitcode)
415+ lines = out.split("\n")
416+ first_line = lines[0]
417+ image_data_line = lines[3]
418+ self.assertEqual('Found 1 public images...', first_line)
419+ self.assertTrue('MyImage' in image_data_line)
420+
421+ # 3. Delete the image
422+ cmd = "bin/glance --port=%d --force delete 1" % api_port
423+
424+ exitcode, out, err = execute(cmd)
425+
426+ self.assertEqual(0, exitcode)
427+ self.assertEqual('Deleted image 1', out.strip())
428+
429+ # 4. Verify no public images
430+ cmd = "bin/glance --port=%d index" % api_port
431+
432+ exitcode, out, err = execute(cmd)
433+
434+ self.assertEqual(0, exitcode)
435+ self.assertEqual('No public images found.', out.strip())
436+
437+ # 5. Restore the image
438+ cmd = "bin/glance --port=%d restore 1" % api_port
439+
440+ exitcode, out, err = execute(cmd)
441+
442+ self.assertEqual(0, exitcode)
443+ self.assertEqual('Restored image 1', out.strip())
444+
445+ # 6. Verify image listed as public image
446+ cmd = "bin/glance --port=%d index" % api_port
447+
448+ exitcode, out, err = execute(cmd)
449+
450+ self.assertEqual(0, exitcode)
451+ lines = out.split("\n")
452+ first_line = lines[0]
453+ image_data_line = lines[3]
454+ self.assertEqual('Found 1 public images...', first_line)
455+ self.assertTrue('MyImage' in image_data_line)
456+
457+ self.stop_servers()
458
459=== modified file 'tests/functional/test_httplib2_api.py'
460--- tests/functional/test_httplib2_api.py 2011-07-21 12:42:38 +0000
461+++ tests/functional/test_httplib2_api.py 2011-08-01 18:29:24 +0000
462@@ -1076,3 +1076,66 @@
463 "Could not find '%s' in '%s'" % (expected, content))
464
465 self.stop_servers()
466+
467+ def test_delete_restore(self):
468+ """
469+ Upload image, delete it, then restore it
470+ """
471+ self.cleanup()
472+ self.start_servers(delayed_delete=True)
473+
474+ # 0. GET /images
475+ # Verify no public images
476+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
477+ http = httplib2.Http()
478+ response, content = http.request(path, 'GET')
479+ self.assertEqual(response.status, 200)
480+ self.assertEqual(content, '{"images": []}')
481+
482+ # 1. POST /images with public image named Image1
483+ headers = {'Content-Type': 'application/octet-stream',
484+ 'X-Image-Meta-Name': 'Image1',
485+ 'X-Image-Meta-Status': 'active',
486+ 'X-Image-Meta-Container-Format': 'ovf',
487+ 'X-Image-Meta-Disk-Format': 'vdi',
488+ 'X-Image-Meta-Size': '19',
489+ 'X-Image-Meta-Is-Public': 'True'}
490+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
491+ http = httplib2.Http()
492+ response, content = http.request(path, 'POST', headers=headers,
493+ body='x' * 19)
494+ self.assertEqual(response.status, 201)
495+
496+ # 2. GET /images
497+ # Verify 1 public image
498+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
499+ http = httplib2.Http()
500+ response, content = http.request(path, 'GET')
501+ self.assertEqual(response.status, 200)
502+ body = json.loads(content)
503+ self.assertEqual(len(body['images']), 1)
504+
505+ # 3. DELETE /images/1
506+ path = "http://%s:%d/v1/images/1" % ("0.0.0.0", self.api_port)
507+ http = httplib2.Http()
508+ response, content = http.request(path, 'DELETE')
509+ self.assertEqual(response.status, 200)
510+
511+ # 4. GET /images
512+ # Verify no public images
513+ path = "http://%s:%d/v1/images" % ("0.0.0.0", self.api_port)
514+ http = httplib2.Http()
515+ response, content = http.request(path, 'GET')
516+ self.assertEqual(response.status, 200)
517+ self.assertEqual(content, '{"images": []}')
518+
519+ # 5. POST /images/1/action to restore image
520+ headers = {'Content-Type': 'application/json'}
521+ body = {'action': 'restore'}
522+ path = "http://%s:%d/v1/images/1/action" % ("0.0.0.0", self.api_port)
523+ http = httplib2.Http()
524+ response, content = http.request(path, 'POST', headers=headers,
525+ body=json.dumps(body))
526+ self.assertEqual(response.status, 200)
527+
528+ self.stop_servers()
529
530=== modified file 'tests/functional/test_scrubber.py'
531--- tests/functional/test_scrubber.py 2011-07-26 14:59:22 +0000
532+++ tests/functional/test_scrubber.py 2011-08-01 18:29:24 +0000
533@@ -125,3 +125,31 @@
534 self.assertEqual(rec['status'], 'deleted')
535
536 self.stop_servers()
537+
538+ @functional.runs_sql
539+ def test_restore(self):
540+ """
541+ test that images can be restored after delayed delete
542+ """
543+
544+ def _get_all_pending_delete():
545+ sql = "SELECT * FROM images WHERE status = 'pending_delete'"
546+ return list(self.run_sql_cmd(sql))
547+
548+ self.cleanup()
549+ registry_db = self.registry_server.sql_connection
550+ self.start_servers(delayed_delete=True, sql_connection=registry_db,
551+ daemon=True)
552+
553+ client = self._get_client()
554+ meta = client.add_image(TEST_IMAGE_META, TEST_IMAGE_DATA)
555+ id = meta['id']
556+ self.assertFalse(_get_all_pending_delete())
557+
558+ client.delete_image(id)
559+ self.assertTrue(_get_all_pending_delete())
560+
561+ client.restore_image(id)
562+ self.assertFalse(_get_all_pending_delete())
563+
564+ self.stop_servers()
565
566=== modified file 'tests/stubs.py'
567--- tests/stubs.py 2011-07-23 00:07:38 +0000
568+++ tests/stubs.py 2011-08-01 18:29:24 +0000
569@@ -276,6 +276,7 @@
570 {'id': 1,
571 'name': 'fake image #1',
572 'status': 'active',
573+ 'saved_status': None,
574 'disk_format': 'ami',
575 'container_format': 'ami',
576 'is_public': False,
577@@ -292,6 +293,7 @@
578 {'id': 2,
579 'name': 'fake image #2',
580 'status': 'active',
581+ 'saved_status': None,
582 'disk_format': 'vhd',
583 'container_format': 'ovf',
584 'is_public': True,
585@@ -324,6 +326,7 @@
586 values['deleted'] = False
587 values['properties'] = values.get('properties', {})
588 values['location'] = values.get('location')
589+ values['saved_status'] = values.get('saved_status')
590
591 now = datetime.datetime.utcnow()
592 values['created_at'] = values.get('created_at', now)
593@@ -382,6 +385,11 @@
594 image['deleted_at'] = datetime.datetime.utcnow()
595 self.deleted_images.append(image)
596
597+ def image_restore(self, _context, image_id):
598+ image = self.image_get(_context, image_id)
599+ image['status'] = image['saved_status']
600+ image['saved_status'] = None
601+
602 def image_get(self, _context, image_id):
603
604 images = [i for i in self.images if str(i['id']) == str(image_id)]
605@@ -469,6 +477,8 @@
606 fake_datastore.image_update)
607 stubs.Set(glance.registry.db.api, 'image_destroy',
608 fake_datastore.image_destroy)
609+ stubs.Set(glance.registry.db.api, 'image_restore',
610+ fake_datastore.image_restore)
611 stubs.Set(glance.registry.db.api, 'image_get',
612 fake_datastore.image_get)
613 stubs.Set(glance.registry.db.api, 'image_get_all_pending_delete',
614
615=== modified file 'tests/unit/test_clients.py'
616--- tests/unit/test_clients.py 2011-07-26 10:11:38 +0000
617+++ tests/unit/test_clients.py 2011-08-01 18:29:24 +0000
618@@ -883,6 +883,26 @@
619 self.client.delete_image,
620 3)
621
622+ def test_restore(self):
623+ """Tests restoring deleted image"""
624+ extra_fixture = {'id': 3,
625+ 'status': 'pending_delete',
626+ 'saved_status': 'active',
627+ 'is_public': True,
628+ 'disk_format': 'vhd',
629+ 'container_format': 'ovf',
630+ 'name': 'asdf',
631+ 'size': 19,
632+ 'checksum': None}
633+
634+ glance.registry.db.api.image_create(None, extra_fixture)
635+
636+ self.assertTrue(self.client.restore_image(3))
637+
638+ data = self.client.get_image(3)
639+
640+ self.assertEquals(data['status'], 'active')
641+
642
643 class TestClient(unittest.TestCase):
644

Subscribers

People subscribed via source and target branches