Merge lp:~johannes.erdfelt/glance/image-undelete into lp:~hudson-openstack/glance/trunk
- image-undelete
- Merge into trunk
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 |
Related bugs: |
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.
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/
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/
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.
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/
> 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.
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
Brian Waldon (bcwaldon) wrote : | # |
Testing looks great. Two minor nits:
1) Can you align restore_image and image_restore?
2) can you move /images/
- 196. By Johannes Erdfelt
-
Merge with trunk
- 197. By Johannes Erdfelt
-
actions -> action
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.
Brian Waldon (bcwaldon) wrote : | # |
I'm cool with the naming, and thanks for updating the action collection. Looks good.
OpenStack Infra (hudson-openstack) wrote : | # |
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.
writing top-level names to glance.
writing dependency_links to glance.
writing manifest file 'glance.
reading manifest file 'glance.
reading manifest template 'MANIFEST.in'
warning: no files found matching 'ChangeLog'
writing manifest file 'glance.
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_
test_bad_
test_delete_image (tests.
test_delete_
Here, we try to delete an image that is in the queued state. ... ok
Test that the ETag header matches the x-image-
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
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
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 |
*golf clap*