Merge lp:~jk0/nova/backup_with_rotation into lp:~hudson-openstack/nova/trunk
- backup_with_rotation
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Brian Lamar |
Approved revision: | 1228 |
Merged at revision: | 1226 |
Proposed branch: | lp:~jk0/nova/backup_with_rotation |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
573 lines (+298/-44) 7 files modified
nova/api/openstack/images.py (+41/-5) nova/compute/api.py (+44/-3) nova/compute/manager.py (+73/-2) nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py (+1/-1) nova/exception.py (+8/-0) nova/tests/api/openstack/fakes.py (+10/-0) nova/tests/api/openstack/test_images.py (+121/-33) |
To merge this branch: | bzr merge lp:~jk0/nova/backup_with_rotation |
Related bugs: | |
Related blueprints: |
Backup instance and rotate
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Lamar (community) | Approve | ||
Brian Waldon (community) | Approve | ||
Review via email: mp+65835@code.launchpad.net |
Commit message
Description of the change
Implement backup with rotation and expose this functionality in the OS API.
Vish Ishaya (vishvananda) wrote : | # |
Brian Lamar (blamar) wrote : | # |
Vish/Josh,
I feel like I say this a lot, but this logic doesn't belong in nova/compute/*. If we want the OpenStack API to support backing up images with sweet rotation logic, I would argue it should be implemented as a completely different service.
OpenStack API provides the ability to:
-Backup images
-Query images by creation time/modification time
-Delete a single image or multiple images
Other Service uses these API calls to implement:
-Scheduled deletion of images
-Migration of old images to tape-backup/long term storage/order them on CD-ROMs/whatever
However!
I could be convinced that this logic should go in to glance...because at the very least this isn't a compute task, but an image-store task.
Brian Waldon (bcwaldon) wrote : | # |
I think this is a great idea, and a feature that we could really benefit from. However, I'm hesitant to say this belongs in Nova. This feels like a feature in a future "scheduling service" that can execute things at certain times.
If the decision is made to keep this in Nova, I would like to see the OS API functionality implemented as an extension. This isn't part of the v1.1 API specification.
Brian Waldon (bcwaldon) wrote : | # |
After giving it some thought, I'm much more okay with this implementation as a temporary solution. I also don't have a problem with the API change if it is considered part of the admin api. However, being part of the admin api, does that mean that a deployer will have to set up each api node with the admin api on?
Fix up these few things, and it's good to go:
20-25: Can you find a more common place to put this? I feel like we could use this logic in a lot of places.
186, 193: instantiate these classes
214: you might want to specify a limit kwarg here. It defaults to 25
220: There is a branch in MP right now in glance that allows db-level sorting: lp:~rackspace-titan/glance/api-results-ordering
243-245: I don't think this change is necessary
Josh Kearney (jk0) wrote : | # |
Thanks for the feedback (changes coming shortly).
> 243-245: I don't think this change is necessary
The pep8 version in the current pip-requires picked this up.
- 1220. By Josh Kearney
-
Review feedback.
- 1221. By Josh Kearney
-
Review feedback.
- 1222. By Josh Kearney
-
Whoops.
- 1223. By Josh Kearney
-
OOPS
Brian Lamar (blamar) wrote : | # |
Lines 24, 53, 63: Can you add a explanation=
- 1224. By Josh Kearney
-
Review feedback.
Brian Waldon (bcwaldon) wrote : | # |
Looks like these couple of things still need to be addressed:
20-25: Can you find a more common place to put this? I feel like we could use this logic in a lot of places.
236: The sorting functionality went in today. You can add sort_key and sort_dir keyword params to handle your sorting
Rick Harris (rconradharris) wrote : | # |
> 20-25: Can you find a more common place to put this? I feel like we could use this logic in a lot of places.
As it's written, this was custom-tailored for this method. We /could/ re-work it to be more generic and use it in other places; the drawback there is that it ends up somewhat defeating the original purpose, DRYing up the code we're modifying.
If we do want to put a version of this code in a separate place, I'd vote that we do that as we're refactoring some of the code that would use it.
Brian Waldon (bcwaldon) wrote : | # |
> > 20-25: Can you find a more common place to put this? I feel like we could
> use this logic in a lot of places.
>
> As it's written, this was custom-tailored for this method. We /could/ re-work
> it to be more generic and use it in other places; the drawback there is that
> it ends up somewhat defeating the original purpose, DRYing up the code we're
> modifying.
>
> If we do want to put a version of this code in a separate place, I'd vote that
> we do that as we're refactoring some of the code that would use it.
Sounds reasonable.
- 1225. By Josh Kearney
-
Let glance handle sorting.
- 1226. By Josh Kearney
-
Merged trunk
- 1227. By Josh Kearney
-
Merged trunk
Brian Waldon (bcwaldon) wrote : | # |
Josh, I think it might be a good idea to explicitly add sort_key/sort_dir params to protect from future changes. I would like the peace of mind to know we won't get screwed here if the default sort key/direction change. Is that reasonable?
- 1228. By Josh Kearney
-
Review feedback
Josh Kearney (jk0) wrote : | # |
> Josh, I think it might be a good idea to explicitly add sort_key/sort_dir
> params to protect from future changes. I would like the peace of mind to know
> we won't get screwed here if the default sort key/direction change. Is that
> reasonable?
Yep. All set.
Brian Waldon (bcwaldon) wrote : | # |
> > Josh, I think it might be a good idea to explicitly add sort_key/sort_dir
> > params to protect from future changes. I would like the peace of mind to
> know
> > we won't get screwed here if the default sort key/direction change. Is that
> > reasonable?
>
> Yep. All set.
Awesome. Great work, guys.
Brian Lamar (blamar) wrote : | # |
I really want to help you guys get this in, but what are your thoughts on making this inline with the other admin API functions? Currently nova/api/
server_
and then have the backup logic go in the servers controller? My main point here is that we're backing up a server...not an image right? It seems odd to say "backup this image" because an image is something you start a server from. Also, it seems more 'secure' to not even load the routes when allow_admin_api flag isn't set, like the existing admin API methods.
Josh Kearney (jk0) wrote : | # |
> I really want to help you guys get this in, but what are your thoughts on
> making this inline with the other admin API functions? Currently
> nova/api/
> could we maybe add:
>
> server_
>
> and then have the backup logic go in the servers controller? My main point
> here is that we're backing up a server...not an image right? It seems odd to
> say "backup this image" because an image is something you start a server from.
> Also, it seems more 'secure' to not even load the routes when allow_admin_api
> flag isn't set, like the existing admin API methods.
From a Glance perspective everything is considered an image (base images, snapshots, backups, etc), so we felt most comfortable including this portion of the code in images. Same idea in python-novaclient. For example:
Snapshot: nova image-create <instance ID> <snapshot name>
Backup: nova image-create <instance ID> <backup name> --image-type backup --backup-type <daily|weeky> --rotation <arbitrary number>
So for the most part, the only differences between these two are the image_type, backup_type and rotation properties in glance.
I do like what you're suggesting, but would this refactor be better suited in its own branch / BP?
Brian Lamar (blamar) wrote : | # |
Yeah, can you maybe make a blueprint to look at this as a whole in Nova? Nova should be instance-focused and Glance should be image-focused. It's been stated multiple times that /images in Nova is going away so we need to have a blueprint to show us the way.
This looks good and passes all of the normal test suites.
Preview Diff
1 | === modified file 'nova/api/openstack/images.py' |
2 | --- nova/api/openstack/images.py 2011-06-24 12:01:51 +0000 |
3 | +++ nova/api/openstack/images.py 2011-06-28 23:51:36 +0000 |
4 | @@ -90,31 +90,67 @@ |
5 | return webob.exc.HTTPNoContent() |
6 | |
7 | def create(self, req, body): |
8 | - """Snapshot a server instance and save the image. |
9 | + """Snapshot or backup a server instance and save the image. |
10 | + |
11 | + Images now have an `image_type` associated with them, which can be |
12 | + 'snapshot' or the backup type, like 'daily' or 'weekly'. |
13 | + |
14 | + If the image_type is backup-like, then the rotation factor can be |
15 | + included and that will cause the oldest backups that exceed the |
16 | + rotation factor to be deleted. |
17 | |
18 | :param req: `wsgi.Request` object |
19 | """ |
20 | + def get_param(param): |
21 | + try: |
22 | + return body["image"][param] |
23 | + except KeyError: |
24 | + raise webob.exc.HTTPBadRequest(explanation="Missing required " |
25 | + "param: %s" % param) |
26 | + |
27 | context = req.environ['nova.context'] |
28 | content_type = req.get_content_type() |
29 | |
30 | if not body: |
31 | raise webob.exc.HTTPBadRequest() |
32 | |
33 | + image_type = body["image"].get("image_type", "snapshot") |
34 | + |
35 | try: |
36 | server_id = self._server_id_from_req(req, body) |
37 | - image_name = body["image"]["name"] |
38 | except KeyError: |
39 | raise webob.exc.HTTPBadRequest() |
40 | |
41 | + image_name = get_param("name") |
42 | props = self._get_extra_properties(req, body) |
43 | |
44 | - image = self._compute_service.snapshot(context, server_id, |
45 | - image_name, props) |
46 | + if image_type == "snapshot": |
47 | + image = self._compute_service.snapshot( |
48 | + context, server_id, image_name, |
49 | + extra_properties=props) |
50 | + elif image_type == "backup": |
51 | + # NOTE(sirp): Unlike snapshot, backup is not a customer facing |
52 | + # API call; rather, it's used by the internal backup scheduler |
53 | + if not FLAGS.allow_admin_api: |
54 | + raise webob.exc.HTTPBadRequest( |
55 | + explanation="Admin API Required") |
56 | + |
57 | + backup_type = get_param("backup_type") |
58 | + rotation = int(get_param("rotation")) |
59 | + |
60 | + image = self._compute_service.backup( |
61 | + context, server_id, image_name, |
62 | + backup_type, rotation, extra_properties=props) |
63 | + else: |
64 | + LOG.error(_("Invalid image_type '%s' passed") % image_type) |
65 | + raise webob.exc.HTTPBadRequest(explanation="Invalue image_type: " |
66 | + "%s" % image_type) |
67 | + |
68 | return dict(image=self.get_builder(req).build(image, detail=True)) |
69 | |
70 | def get_builder(self, request): |
71 | """Indicates that you must use a Controller subclass.""" |
72 | - raise NotImplementedError |
73 | + raise NotImplementedError() |
74 | |
75 | def _server_id_from_req(self, req, data): |
76 | raise NotImplementedError() |
77 | |
78 | === modified file 'nova/compute/api.py' |
79 | --- nova/compute/api.py 2011-06-24 12:07:33 +0000 |
80 | +++ nova/compute/api.py 2011-06-28 23:51:36 +0000 |
81 | @@ -711,19 +711,60 @@ |
82 | raise exception.Error(_("Unable to find host for Instance %s") |
83 | % instance_id) |
84 | |
85 | + def backup(self, context, instance_id, name, backup_type, rotation, |
86 | + extra_properties=None): |
87 | + """Backup the given instance |
88 | + |
89 | + :param instance_id: nova.db.sqlalchemy.models.Instance.Id |
90 | + :param name: name of the backup or snapshot |
91 | + name = backup_type # daily backups are called 'daily' |
92 | + :param rotation: int representing how many backups to keep around; |
93 | + None if rotation shouldn't be used (as in the case of snapshots) |
94 | + :param extra_properties: dict of extra image properties to include |
95 | + """ |
96 | + recv_meta = self._create_image(context, instance_id, name, 'backup', |
97 | + backup_type=backup_type, rotation=rotation, |
98 | + extra_properties=extra_properties) |
99 | + return recv_meta |
100 | + |
101 | def snapshot(self, context, instance_id, name, extra_properties=None): |
102 | """Snapshot the given instance. |
103 | |
104 | + :param instance_id: nova.db.sqlalchemy.models.Instance.Id |
105 | + :param name: name of the backup or snapshot |
106 | + :param extra_properties: dict of extra image properties to include |
107 | + |
108 | :returns: A dict containing image metadata |
109 | """ |
110 | - properties = {'instance_id': str(instance_id), |
111 | + return self._create_image(context, instance_id, name, 'snapshot', |
112 | + extra_properties=extra_properties) |
113 | + |
114 | + def _create_image(self, context, instance_id, name, image_type, |
115 | + backup_type=None, rotation=None, extra_properties=None): |
116 | + """Create snapshot or backup for an instance on this host. |
117 | + |
118 | + :param context: security context |
119 | + :param instance_id: nova.db.sqlalchemy.models.Instance.Id |
120 | + :param name: string for name of the snapshot |
121 | + :param image_type: snapshot | backup |
122 | + :param backup_type: daily | weekly |
123 | + :param rotation: int representing how many backups to keep around; |
124 | + None if rotation shouldn't be used (as in the case of snapshots) |
125 | + :param extra_properties: dict of extra image properties to include |
126 | + |
127 | + """ |
128 | + instance = db.api.instance_get(context, instance_id) |
129 | + properties = {'instance_uuid': instance['uuid'], |
130 | 'user_id': str(context.user_id), |
131 | - 'image_state': 'creating'} |
132 | + 'image_state': 'creating', |
133 | + 'image_type': image_type, |
134 | + 'backup_type': backup_type} |
135 | properties.update(extra_properties or {}) |
136 | sent_meta = {'name': name, 'is_public': False, |
137 | 'status': 'creating', 'properties': properties} |
138 | recv_meta = self.image_service.create(context, sent_meta) |
139 | - params = {'image_id': recv_meta['id']} |
140 | + params = {'image_id': recv_meta['id'], 'image_type': image_type, |
141 | + 'backup_type': backup_type, 'rotation': rotation} |
142 | self._cast_compute_message('snapshot_instance', context, instance_id, |
143 | params=params) |
144 | return recv_meta |
145 | |
146 | === modified file 'nova/compute/manager.py' |
147 | --- nova/compute/manager.py 2011-06-28 20:37:05 +0000 |
148 | +++ nova/compute/manager.py 2011-06-28 23:51:36 +0000 |
149 | @@ -46,6 +46,7 @@ |
150 | |
151 | from nova import exception |
152 | from nova import flags |
153 | +import nova.image |
154 | from nova import log as logging |
155 | from nova import manager |
156 | from nova import network |
157 | @@ -506,8 +507,19 @@ |
158 | self._update_state(context, instance_id) |
159 | |
160 | @exception.wrap_exception |
161 | - def snapshot_instance(self, context, instance_id, image_id): |
162 | - """Snapshot an instance on this host.""" |
163 | + def snapshot_instance(self, context, instance_id, image_id, |
164 | + image_type='snapshot', backup_type=None, |
165 | + rotation=None): |
166 | + """Snapshot an instance on this host. |
167 | + |
168 | + :param context: security context |
169 | + :param instance_id: nova.db.sqlalchemy.models.Instance.Id |
170 | + :param image_id: glance.db.sqlalchemy.models.Image.Id |
171 | + :param image_type: snapshot | backup |
172 | + :param backup_type: daily | weekly |
173 | + :param rotation: int representing how many backups to keep around; |
174 | + None if rotation shouldn't be used (as in the case of snapshots) |
175 | + """ |
176 | context = context.elevated() |
177 | instance_ref = self.db.instance_get(context, instance_id) |
178 | |
179 | @@ -527,6 +539,65 @@ |
180 | |
181 | self.driver.snapshot(instance_ref, image_id) |
182 | |
183 | + if image_type == 'snapshot': |
184 | + if rotation: |
185 | + raise exception.ImageRotationNotAllowed() |
186 | + elif image_type == 'backup': |
187 | + if rotation: |
188 | + instance_uuid = instance_ref['uuid'] |
189 | + self.rotate_backups(context, instance_uuid, backup_type, |
190 | + rotation) |
191 | + else: |
192 | + raise exception.RotationRequiredForBackup() |
193 | + else: |
194 | + raise Exception(_('Image type not recognized %s') % image_type) |
195 | + |
196 | + def rotate_backups(self, context, instance_uuid, backup_type, rotation): |
197 | + """Delete excess backups associated to an instance. |
198 | + |
199 | + Instances are allowed a fixed number of backups (the rotation number); |
200 | + this method deletes the oldest backups that exceed the rotation |
201 | + threshold. |
202 | + |
203 | + :param context: security context |
204 | + :param instance_uuid: string representing uuid of instance |
205 | + :param backup_type: daily | weekly |
206 | + :param rotation: int representing how many backups to keep around; |
207 | + None if rotation shouldn't be used (as in the case of snapshots) |
208 | + """ |
209 | + # NOTE(jk0): Eventually extract this out to the ImageService? |
210 | + def fetch_images(): |
211 | + images = [] |
212 | + marker = None |
213 | + while True: |
214 | + batch = image_service.detail(context, filters=filters, |
215 | + marker=marker, sort_key='created_at', sort_dir='desc') |
216 | + if not batch: |
217 | + break |
218 | + images += batch |
219 | + marker = batch[-1]['id'] |
220 | + return images |
221 | + |
222 | + image_service = nova.image.get_default_image_service() |
223 | + filters = {'property-image_type': 'backup', |
224 | + 'property-backup_type': backup_type, |
225 | + 'property-instance_uuid': instance_uuid} |
226 | + |
227 | + images = fetch_images() |
228 | + num_images = len(images) |
229 | + LOG.debug(_("Found %(num_images)d images (rotation: %(rotation)d)" |
230 | + % locals())) |
231 | + if num_images > rotation: |
232 | + # NOTE(sirp): this deletes all backups that exceed the rotation |
233 | + # limit |
234 | + excess = len(images) - rotation |
235 | + LOG.debug(_("Rotating out %d backups" % excess)) |
236 | + for i in xrange(excess): |
237 | + image = images.pop() |
238 | + image_id = image['id'] |
239 | + LOG.debug(_("Deleting image %d" % image_id)) |
240 | + image_service.delete(context, image_id) |
241 | + |
242 | @exception.wrap_exception |
243 | @checks_instance_lock |
244 | def set_admin_password(self, context, instance_id, new_pass=None): |
245 | |
246 | === modified file 'nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py' |
247 | --- nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py 2011-06-28 22:26:08 +0000 |
248 | +++ nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py 2011-06-28 23:51:36 +0000 |
249 | @@ -58,7 +58,7 @@ |
250 | Column('to_port', Integer()), |
251 | Column('cidr', |
252 | String(length=255, convert_unicode=False, assert_unicode=None, |
253 | - unicode_error=None, _warn_on_bytestring=False))) |
254 | + unicode_error=None, _warn_on_bytestring=False))) |
255 | |
256 | |
257 | def upgrade(migrate_engine): |
258 | |
259 | === modified file 'nova/exception.py' |
260 | --- nova/exception.py 2011-06-28 15:13:52 +0000 |
261 | +++ nova/exception.py 2011-06-28 23:51:36 +0000 |
262 | @@ -558,6 +558,14 @@ |
263 | message = _("Unable to use global role %(role_id)s") |
264 | |
265 | |
266 | +class ImageRotationNotAllowed(NovaException): |
267 | + message = _("Rotation is not allowed for snapshots") |
268 | + |
269 | + |
270 | +class RotationRequiredForBackup(NovaException): |
271 | + message = _("Rotation param is required for backup image_type") |
272 | + |
273 | + |
274 | #TODO(bcwaldon): EOL this exception! |
275 | class Duplicate(NovaException): |
276 | pass |
277 | |
278 | === modified file 'nova/tests/api/openstack/fakes.py' |
279 | --- nova/tests/api/openstack/fakes.py 2011-06-28 00:23:19 +0000 |
280 | +++ nova/tests/api/openstack/fakes.py 2011-06-28 23:51:36 +0000 |
281 | @@ -147,6 +147,16 @@ |
282 | stubs.Set(nova.compute.API, 'snapshot', snapshot) |
283 | |
284 | |
285 | +def stub_out_compute_api_backup(stubs): |
286 | + def backup(self, context, instance_id, name, backup_type, rotation, |
287 | + extra_properties=None): |
288 | + props = dict(instance_id=instance_id, instance_ref=instance_id, |
289 | + backup_type=backup_type, rotation=rotation) |
290 | + props.update(extra_properties or {}) |
291 | + return dict(id='123', status='ACTIVE', name=name, properties=props) |
292 | + stubs.Set(nova.compute.API, 'backup', backup) |
293 | + |
294 | + |
295 | def stub_out_glance_add_image(stubs, sent_to_glance): |
296 | """ |
297 | We return the metadata sent to glance by modifying the sent_to_glance dict |
298 | |
299 | === modified file 'nova/tests/api/openstack/test_images.py' |
300 | --- nova/tests/api/openstack/test_images.py 2011-06-24 12:01:51 +0000 |
301 | +++ nova/tests/api/openstack/test_images.py 2011-06-28 23:51:36 +0000 |
302 | @@ -340,6 +340,7 @@ |
303 | self.fixtures = self._make_image_fixtures() |
304 | fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) |
305 | fakes.stub_out_compute_api_snapshot(self.stubs) |
306 | + fakes.stub_out_compute_api_backup(self.stubs) |
307 | |
308 | def tearDown(self): |
309 | """Run after each test.""" |
310 | @@ -364,10 +365,10 @@ |
311 | response_list = response_dict["images"] |
312 | |
313 | expected = [{'id': 123, 'name': 'public image'}, |
314 | - {'id': 124, 'name': 'queued backup'}, |
315 | - {'id': 125, 'name': 'saving backup'}, |
316 | - {'id': 126, 'name': 'active backup'}, |
317 | - {'id': 127, 'name': 'killed backup'}, |
318 | + {'id': 124, 'name': 'queued snapshot'}, |
319 | + {'id': 125, 'name': 'saving snapshot'}, |
320 | + {'id': 126, 'name': 'active snapshot'}, |
321 | + {'id': 127, 'name': 'killed snapshot'}, |
322 | {'id': 129, 'name': None}] |
323 | |
324 | self.assertDictListMatch(response_list, expected) |
325 | @@ -617,14 +618,14 @@ |
326 | }, |
327 | { |
328 | 'id': 124, |
329 | - 'name': 'queued backup', |
330 | + 'name': 'queued snapshot', |
331 | 'updated': self.NOW_API_FORMAT, |
332 | 'created': self.NOW_API_FORMAT, |
333 | 'status': 'QUEUED', |
334 | }, |
335 | { |
336 | 'id': 125, |
337 | - 'name': 'saving backup', |
338 | + 'name': 'saving snapshot', |
339 | 'updated': self.NOW_API_FORMAT, |
340 | 'created': self.NOW_API_FORMAT, |
341 | 'status': 'SAVING', |
342 | @@ -632,14 +633,14 @@ |
343 | }, |
344 | { |
345 | 'id': 126, |
346 | - 'name': 'active backup', |
347 | + 'name': 'active snapshot', |
348 | 'updated': self.NOW_API_FORMAT, |
349 | 'created': self.NOW_API_FORMAT, |
350 | 'status': 'ACTIVE' |
351 | }, |
352 | { |
353 | 'id': 127, |
354 | - 'name': 'killed backup', |
355 | + 'name': 'killed snapshot', |
356 | 'updated': self.NOW_API_FORMAT, |
357 | 'created': self.NOW_API_FORMAT, |
358 | 'status': 'FAILED', |
359 | @@ -684,7 +685,7 @@ |
360 | }, |
361 | { |
362 | 'id': 124, |
363 | - 'name': 'queued backup', |
364 | + 'name': 'queued snapshot', |
365 | 'serverRef': "http://localhost:8774/v1.1/servers/42", |
366 | 'updated': self.NOW_API_FORMAT, |
367 | 'created': self.NOW_API_FORMAT, |
368 | @@ -706,7 +707,7 @@ |
369 | }, |
370 | { |
371 | 'id': 125, |
372 | - 'name': 'saving backup', |
373 | + 'name': 'saving snapshot', |
374 | 'serverRef': "http://localhost:8774/v1.1/servers/42", |
375 | 'updated': self.NOW_API_FORMAT, |
376 | 'created': self.NOW_API_FORMAT, |
377 | @@ -729,7 +730,7 @@ |
378 | }, |
379 | { |
380 | 'id': 126, |
381 | - 'name': 'active backup', |
382 | + 'name': 'active snapshot', |
383 | 'serverRef': "http://localhost:8774/v1.1/servers/42", |
384 | 'updated': self.NOW_API_FORMAT, |
385 | 'created': self.NOW_API_FORMAT, |
386 | @@ -751,7 +752,7 @@ |
387 | }, |
388 | { |
389 | 'id': 127, |
390 | - 'name': 'killed backup', |
391 | + 'name': 'killed snapshot', |
392 | 'serverRef': "http://localhost:8774/v1.1/servers/42", |
393 | 'updated': self.NOW_API_FORMAT, |
394 | 'created': self.NOW_API_FORMAT, |
395 | @@ -969,18 +970,103 @@ |
396 | self.assertEqual(res.status_int, 404) |
397 | |
398 | def test_create_image(self): |
399 | - |
400 | - body = dict(image=dict(serverId='123', name='Backup 1')) |
401 | - req = webob.Request.blank('/v1.0/images') |
402 | - req.method = 'POST' |
403 | - req.body = json.dumps(body) |
404 | - req.headers["content-type"] = "application/json" |
405 | - response = req.get_response(fakes.wsgi_app()) |
406 | - self.assertEqual(200, response.status_int) |
407 | + body = dict(image=dict(serverId='123', name='Snapshot 1')) |
408 | + req = webob.Request.blank('/v1.0/images') |
409 | + req.method = 'POST' |
410 | + req.body = json.dumps(body) |
411 | + req.headers["content-type"] = "application/json" |
412 | + response = req.get_response(fakes.wsgi_app()) |
413 | + self.assertEqual(200, response.status_int) |
414 | + |
415 | + def test_create_snapshot_no_name(self): |
416 | + """Name is required for snapshots""" |
417 | + body = dict(image=dict(serverId='123')) |
418 | + req = webob.Request.blank('/v1.0/images') |
419 | + req.method = 'POST' |
420 | + req.body = json.dumps(body) |
421 | + req.headers["content-type"] = "application/json" |
422 | + response = req.get_response(fakes.wsgi_app()) |
423 | + self.assertEqual(400, response.status_int) |
424 | + |
425 | + def test_create_backup_no_name(self): |
426 | + """Name is also required for backups""" |
427 | + body = dict(image=dict(serverId='123', image_type='backup', |
428 | + backup_type='daily', rotation=1)) |
429 | + req = webob.Request.blank('/v1.0/images') |
430 | + req.method = 'POST' |
431 | + req.body = json.dumps(body) |
432 | + req.headers["content-type"] = "application/json" |
433 | + response = req.get_response(fakes.wsgi_app()) |
434 | + self.assertEqual(400, response.status_int) |
435 | + |
436 | + def test_create_backup_with_rotation_and_backup_type(self): |
437 | + """The happy path for creating backups |
438 | + |
439 | + Creating a backup is an admin-only operation, as opposed to snapshots |
440 | + which are available to anybody. |
441 | + """ |
442 | + # FIXME(sirp): teardown needed? |
443 | + FLAGS.allow_admin_api = True |
444 | + |
445 | + # FIXME(sirp): should the fact that backups are admin_only be a FLAG |
446 | + body = dict(image=dict(serverId='123', image_type='backup', |
447 | + name='Backup 1', |
448 | + backup_type='daily', rotation=1)) |
449 | + req = webob.Request.blank('/v1.0/images') |
450 | + req.method = 'POST' |
451 | + req.body = json.dumps(body) |
452 | + req.headers["content-type"] = "application/json" |
453 | + response = req.get_response(fakes.wsgi_app()) |
454 | + self.assertEqual(200, response.status_int) |
455 | + |
456 | + def test_create_backup_no_rotation(self): |
457 | + """Rotation is required for backup requests""" |
458 | + # FIXME(sirp): teardown needed? |
459 | + FLAGS.allow_admin_api = True |
460 | + |
461 | + # FIXME(sirp): should the fact that backups are admin_only be a FLAG |
462 | + body = dict(image=dict(serverId='123', name='daily', |
463 | + image_type='backup', backup_type='daily')) |
464 | + req = webob.Request.blank('/v1.0/images') |
465 | + req.method = 'POST' |
466 | + req.body = json.dumps(body) |
467 | + req.headers["content-type"] = "application/json" |
468 | + response = req.get_response(fakes.wsgi_app()) |
469 | + self.assertEqual(400, response.status_int) |
470 | + |
471 | + def test_create_backup_no_backup_type(self): |
472 | + """Backup Type (daily or weekly) is required for backup requests""" |
473 | + # FIXME(sirp): teardown needed? |
474 | + FLAGS.allow_admin_api = True |
475 | + |
476 | + # FIXME(sirp): should the fact that backups are admin_only be a FLAG |
477 | + body = dict(image=dict(serverId='123', name='daily', |
478 | + image_type='backup', rotation=1)) |
479 | + req = webob.Request.blank('/v1.0/images') |
480 | + req.method = 'POST' |
481 | + req.body = json.dumps(body) |
482 | + req.headers["content-type"] = "application/json" |
483 | + response = req.get_response(fakes.wsgi_app()) |
484 | + self.assertEqual(400, response.status_int) |
485 | + |
486 | + def test_create_image_with_invalid_image_type(self): |
487 | + """Valid image_types are snapshot | daily | weekly""" |
488 | + # FIXME(sirp): teardown needed? |
489 | + FLAGS.allow_admin_api = True |
490 | + |
491 | + # FIXME(sirp): should the fact that backups are admin_only be a FLAG |
492 | + body = dict(image=dict(serverId='123', image_type='monthly', |
493 | + rotation=1)) |
494 | + req = webob.Request.blank('/v1.0/images') |
495 | + req.method = 'POST' |
496 | + req.body = json.dumps(body) |
497 | + req.headers["content-type"] = "application/json" |
498 | + response = req.get_response(fakes.wsgi_app()) |
499 | + self.assertEqual(400, response.status_int) |
500 | |
501 | def test_create_image_no_server_id(self): |
502 | |
503 | - body = dict(image=dict(name='Backup 1')) |
504 | + body = dict(image=dict(name='Snapshot 1')) |
505 | req = webob.Request.blank('/v1.0/images') |
506 | req.method = 'POST' |
507 | req.body = json.dumps(body) |
508 | @@ -990,7 +1076,7 @@ |
509 | |
510 | def test_create_image_v1_1(self): |
511 | |
512 | - body = dict(image=dict(serverRef='123', name='Backup 1')) |
513 | + body = dict(image=dict(serverRef='123', name='Snapshot 1')) |
514 | req = webob.Request.blank('/v1.1/images') |
515 | req.method = 'POST' |
516 | req.body = json.dumps(body) |
517 | @@ -1024,7 +1110,7 @@ |
518 | |
519 | def test_create_image_v1_1_xml_serialization(self): |
520 | |
521 | - body = dict(image=dict(serverRef='123', name='Backup 1')) |
522 | + body = dict(image=dict(serverRef='123', name='Snapshot 1')) |
523 | req = webob.Request.blank('/v1.1/images') |
524 | req.method = 'POST' |
525 | req.body = json.dumps(body) |
526 | @@ -1038,7 +1124,7 @@ |
527 | <image |
528 | created="None" |
529 | id="123" |
530 | - name="Backup 1" |
531 | + name="Snapshot 1" |
532 | serverRef="http://localhost/v1.1/servers/123" |
533 | status="ACTIVE" |
534 | updated="None" |
535 | @@ -1057,7 +1143,7 @@ |
536 | |
537 | def test_create_image_v1_1_no_server_ref(self): |
538 | |
539 | - body = dict(image=dict(name='Backup 1')) |
540 | + body = dict(image=dict(name='Snapshot 1')) |
541 | req = webob.Request.blank('/v1.1/images') |
542 | req.method = 'POST' |
543 | req.body = json.dumps(body) |
544 | @@ -1084,19 +1170,21 @@ |
545 | status='active', properties={}) |
546 | image_id += 1 |
547 | |
548 | - # Backup for User 1 |
549 | + # Snapshot for User 1 |
550 | server_ref = 'http://localhost:8774/v1.1/servers/42' |
551 | - backup_properties = {'instance_ref': server_ref, 'user_id': '1'} |
552 | + snapshot_properties = {'instance_ref': server_ref, 'user_id': '1'} |
553 | for status in ('queued', 'saving', 'active', 'killed'): |
554 | - add_fixture(id=image_id, name='%s backup' % status, |
555 | + add_fixture(id=image_id, name='%s snapshot' % status, |
556 | is_public=False, status=status, |
557 | - properties=backup_properties) |
558 | + properties=snapshot_properties) |
559 | image_id += 1 |
560 | |
561 | - # Backup for User 2 |
562 | - other_backup_properties = {'instance_id': '43', 'user_id': '2'} |
563 | - add_fixture(id=image_id, name='someone elses backup', is_public=False, |
564 | - status='active', properties=other_backup_properties) |
565 | + # Snapshot for User 2 |
566 | + other_snapshot_properties = {'instance_id': '43', 'user_id': '2'} |
567 | + add_fixture(id=image_id, name='someone elses snapshot', |
568 | + is_public=False, status='active', |
569 | + properties=other_snapshot_properties) |
570 | + |
571 | image_id += 1 |
572 | |
573 | # Image without a name |
All this looks good. Just unsure if this is in the api spec or should be an extension?