Merge lp:~vladimir.p/nova/volume_type_extradata into lp:~hudson-openstack/nova/trunk
- volume_type_extradata
- Merge into trunk
Proposed by
Vladimir Popovski
Status: | Merged |
---|---|
Approved by: | Dan Prince |
Approved revision: | 1469 |
Merged at revision: | 1494 |
Proposed branch: | lp:~vladimir.p/nova/volume_type_extradata |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
2042 lines (+1729/-14) 16 files modified
nova/api/openstack/contrib/volumes.py (+34/-2) nova/api/openstack/contrib/volumetypes.py (+197/-0) nova/db/api.py (+76/-0) nova/db/sqlalchemy/api.py (+317/-5) nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py (+115/-0) nova/db/sqlalchemy/migration.py (+2/-1) nova/db/sqlalchemy/models.py (+45/-0) nova/exception.py (+27/-0) nova/tests/api/openstack/test_extensions.py (+1/-0) nova/tests/api/openstack/test_volume_types.py (+171/-0) nova/tests/api/openstack/test_volume_types_extra_specs.py (+181/-0) nova/tests/integrated/test_volumes.py (+17/-0) nova/tests/test_volume_types.py (+207/-0) nova/tests/test_volume_types_extra_specs.py (+132/-0) nova/volume/api.py (+78/-6) nova/volume/volume_types.py (+129/-0) |
To merge this branch: | bzr merge lp:~vladimir.p/nova/volume_type_extradata |
Related bugs: | |
Related blueprints: |
Adding Volume Types to Nova Volumes
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ben McGraw (community) | Approve | ||
Dan Prince (community) | Approve | ||
Vish Ishaya (community) | Approve | ||
Review via email: mp+72762@code.launchpad.net |
Commit message
Description of the change
Added volume metadata / volume types / volume types extra_specs
To post a comment you must log in.
- 1466. By Vladimir Popovski
-
added Openstack APIs for volume types & extradata
- 1467. By Vladimir Popovski
-
forgot to add new extension to test_extensions
Revision history for this message
Vish Ishaya (vishvananda) wrote : | # |
Just realized that this has an FFE, so it doesn't need to be rebased to get into D4, it can go in after. You can ignore my instructions above.
review:
Approve
- 1468. By Vladimir Popovski
-
added new tables to list of DBs in migration.py
- 1469. By Vladimir Popovski
-
merged with nova 1490
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'nova/api/openstack/contrib/volumes.py' |
2 | --- nova/api/openstack/contrib/volumes.py 2011-07-26 17:10:31 +0000 |
3 | +++ nova/api/openstack/contrib/volumes.py 2011-08-24 23:44:25 +0000 |
4 | @@ -24,6 +24,7 @@ |
5 | from nova import log as logging |
6 | from nova import quota |
7 | from nova import volume |
8 | +from nova.volume import volume_types |
9 | from nova.api.openstack import common |
10 | from nova.api.openstack import extensions |
11 | from nova.api.openstack import faults |
12 | @@ -63,6 +64,22 @@ |
13 | |
14 | d['displayName'] = vol['display_name'] |
15 | d['displayDescription'] = vol['display_description'] |
16 | + |
17 | + if vol['volume_type_id'] and vol.get('volume_type'): |
18 | + d['volumeType'] = vol['volume_type']['name'] |
19 | + else: |
20 | + d['volumeType'] = vol['volume_type_id'] |
21 | + |
22 | + LOG.audit(_("vol=%s"), vol, context=context) |
23 | + |
24 | + if vol.get('volume_metadata'): |
25 | + meta_dict = {} |
26 | + for i in vol['volume_metadata']: |
27 | + meta_dict[i['key']] = i['value'] |
28 | + d['metadata'] = meta_dict |
29 | + else: |
30 | + d['metadata'] = {} |
31 | + |
32 | return d |
33 | |
34 | |
35 | @@ -80,6 +97,8 @@ |
36 | "createdAt", |
37 | "displayName", |
38 | "displayDescription", |
39 | + "volumeType", |
40 | + "metadata", |
41 | ]}}} |
42 | |
43 | def __init__(self): |
44 | @@ -136,12 +155,25 @@ |
45 | vol = body['volume'] |
46 | size = vol['size'] |
47 | LOG.audit(_("Create volume of %s GB"), size, context=context) |
48 | + |
49 | + vol_type = vol.get('volume_type', None) |
50 | + if vol_type: |
51 | + try: |
52 | + vol_type = volume_types.get_volume_type_by_name(context, |
53 | + vol_type) |
54 | + except exception.NotFound: |
55 | + return faults.Fault(exc.HTTPNotFound()) |
56 | + |
57 | + metadata = vol.get('metadata', None) |
58 | + |
59 | new_volume = self.volume_api.create(context, size, None, |
60 | vol.get('display_name'), |
61 | - vol.get('display_description')) |
62 | + vol.get('display_description'), |
63 | + volume_type=vol_type, |
64 | + metadata=metadata) |
65 | |
66 | # Work around problem that instance is lazy-loaded... |
67 | - new_volume['instance'] = None |
68 | + new_volume = self.volume_api.get(context, new_volume['id']) |
69 | |
70 | retval = _translate_volume_detail_view(context, new_volume) |
71 | |
72 | |
73 | === added file 'nova/api/openstack/contrib/volumetypes.py' |
74 | --- nova/api/openstack/contrib/volumetypes.py 1970-01-01 00:00:00 +0000 |
75 | +++ nova/api/openstack/contrib/volumetypes.py 2011-08-24 23:44:25 +0000 |
76 | @@ -0,0 +1,197 @@ |
77 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
78 | + |
79 | +# Copyright (c) 2011 Zadara Storage Inc. |
80 | +# Copyright (c) 2011 OpenStack LLC. |
81 | +# |
82 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
83 | +# not use this file except in compliance with the License. You may obtain |
84 | +# a copy of the License at |
85 | +# |
86 | +# http://www.apache.org/licenses/LICENSE-2.0 |
87 | +# |
88 | +# Unless required by applicable law or agreed to in writing, software |
89 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
90 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
91 | +# License for the specific language governing permissions and limitations |
92 | +# under the License. |
93 | + |
94 | +""" The volume type & volume types extra specs extension""" |
95 | + |
96 | +from webob import exc |
97 | + |
98 | +from nova import db |
99 | +from nova import exception |
100 | +from nova import quota |
101 | +from nova.volume import volume_types |
102 | +from nova.api.openstack import extensions |
103 | +from nova.api.openstack import faults |
104 | +from nova.api.openstack import wsgi |
105 | + |
106 | + |
107 | +class VolumeTypesController(object): |
108 | + """ The volume types API controller for the Openstack API """ |
109 | + |
110 | + def index(self, req): |
111 | + """ Returns the list of volume types """ |
112 | + context = req.environ['nova.context'] |
113 | + return volume_types.get_all_types(context) |
114 | + |
115 | + def create(self, req, body): |
116 | + """Creates a new volume type.""" |
117 | + context = req.environ['nova.context'] |
118 | + |
119 | + if not body or body == "": |
120 | + return faults.Fault(exc.HTTPUnprocessableEntity()) |
121 | + |
122 | + vol_type = body.get('volume_type', None) |
123 | + if vol_type is None or vol_type == "": |
124 | + return faults.Fault(exc.HTTPUnprocessableEntity()) |
125 | + |
126 | + name = vol_type.get('name', None) |
127 | + specs = vol_type.get('extra_specs', {}) |
128 | + |
129 | + if name is None or name == "": |
130 | + return faults.Fault(exc.HTTPUnprocessableEntity()) |
131 | + |
132 | + try: |
133 | + volume_types.create(context, name, specs) |
134 | + vol_type = volume_types.get_volume_type_by_name(context, name) |
135 | + except quota.QuotaError as error: |
136 | + self._handle_quota_error(error) |
137 | + except exception.NotFound: |
138 | + return faults.Fault(exc.HTTPNotFound()) |
139 | + |
140 | + return {'volume_type': vol_type} |
141 | + |
142 | + def show(self, req, id): |
143 | + """ Return a single volume type item """ |
144 | + context = req.environ['nova.context'] |
145 | + |
146 | + try: |
147 | + vol_type = volume_types.get_volume_type(context, id) |
148 | + except exception.NotFound or exception.ApiError: |
149 | + return faults.Fault(exc.HTTPNotFound()) |
150 | + |
151 | + return {'volume_type': vol_type} |
152 | + |
153 | + def delete(self, req, id): |
154 | + """ Deletes an existing volume type """ |
155 | + context = req.environ['nova.context'] |
156 | + |
157 | + try: |
158 | + vol_type = volume_types.get_volume_type(context, id) |
159 | + volume_types.destroy(context, vol_type['name']) |
160 | + except exception.NotFound: |
161 | + return faults.Fault(exc.HTTPNotFound()) |
162 | + |
163 | + def _handle_quota_error(self, error): |
164 | + """Reraise quota errors as api-specific http exceptions.""" |
165 | + if error.code == "MetadataLimitExceeded": |
166 | + raise exc.HTTPBadRequest(explanation=error.message) |
167 | + raise error |
168 | + |
169 | + |
170 | +class VolumeTypeExtraSpecsController(object): |
171 | + """ The volume type extra specs API controller for the Openstack API """ |
172 | + |
173 | + def _get_extra_specs(self, context, vol_type_id): |
174 | + extra_specs = db.api.volume_type_extra_specs_get(context, vol_type_id) |
175 | + specs_dict = {} |
176 | + for key, value in extra_specs.iteritems(): |
177 | + specs_dict[key] = value |
178 | + return dict(extra_specs=specs_dict) |
179 | + |
180 | + def _check_body(self, body): |
181 | + if body == None or body == "": |
182 | + expl = _('No Request Body') |
183 | + raise exc.HTTPBadRequest(explanation=expl) |
184 | + |
185 | + def index(self, req, vol_type_id): |
186 | + """ Returns the list of extra specs for a given volume type """ |
187 | + context = req.environ['nova.context'] |
188 | + return self._get_extra_specs(context, vol_type_id) |
189 | + |
190 | + def create(self, req, vol_type_id, body): |
191 | + self._check_body(body) |
192 | + context = req.environ['nova.context'] |
193 | + specs = body.get('extra_specs') |
194 | + try: |
195 | + db.api.volume_type_extra_specs_update_or_create(context, |
196 | + vol_type_id, |
197 | + specs) |
198 | + except quota.QuotaError as error: |
199 | + self._handle_quota_error(error) |
200 | + return body |
201 | + |
202 | + def update(self, req, vol_type_id, id, body): |
203 | + self._check_body(body) |
204 | + context = req.environ['nova.context'] |
205 | + if not id in body: |
206 | + expl = _('Request body and URI mismatch') |
207 | + raise exc.HTTPBadRequest(explanation=expl) |
208 | + if len(body) > 1: |
209 | + expl = _('Request body contains too many items') |
210 | + raise exc.HTTPBadRequest(explanation=expl) |
211 | + try: |
212 | + db.api.volume_type_extra_specs_update_or_create(context, |
213 | + vol_type_id, |
214 | + body) |
215 | + except quota.QuotaError as error: |
216 | + self._handle_quota_error(error) |
217 | + |
218 | + return body |
219 | + |
220 | + def show(self, req, vol_type_id, id): |
221 | + """ Return a single extra spec item """ |
222 | + context = req.environ['nova.context'] |
223 | + specs = self._get_extra_specs(context, vol_type_id) |
224 | + if id in specs['extra_specs']: |
225 | + return {id: specs['extra_specs'][id]} |
226 | + else: |
227 | + return faults.Fault(exc.HTTPNotFound()) |
228 | + |
229 | + def delete(self, req, vol_type_id, id): |
230 | + """ Deletes an existing extra spec """ |
231 | + context = req.environ['nova.context'] |
232 | + db.api.volume_type_extra_specs_delete(context, vol_type_id, id) |
233 | + |
234 | + def _handle_quota_error(self, error): |
235 | + """Reraise quota errors as api-specific http exceptions.""" |
236 | + if error.code == "MetadataLimitExceeded": |
237 | + raise exc.HTTPBadRequest(explanation=error.message) |
238 | + raise error |
239 | + |
240 | + |
241 | +class Volumetypes(extensions.ExtensionDescriptor): |
242 | + |
243 | + def get_name(self): |
244 | + return "VolumeTypes" |
245 | + |
246 | + def get_alias(self): |
247 | + return "os-volume-types" |
248 | + |
249 | + def get_description(self): |
250 | + return "Volume types support" |
251 | + |
252 | + def get_namespace(self): |
253 | + return \ |
254 | + "http://docs.openstack.org/ext/volume_types/api/v1.1" |
255 | + |
256 | + def get_updated(self): |
257 | + return "2011-08-24T00:00:00+00:00" |
258 | + |
259 | + def get_resources(self): |
260 | + resources = [] |
261 | + res = extensions.ResourceExtension( |
262 | + 'os-volume-types', |
263 | + VolumeTypesController()) |
264 | + resources.append(res) |
265 | + |
266 | + res = extensions.ResourceExtension('extra_specs', |
267 | + VolumeTypeExtraSpecsController(), |
268 | + parent=dict( |
269 | + member_name='vol_type', |
270 | + collection_name='os-volume-types')) |
271 | + resources.append(res) |
272 | + |
273 | + return resources |
274 | |
275 | === modified file 'nova/db/api.py' |
276 | --- nova/db/api.py 2011-08-22 23:35:09 +0000 |
277 | +++ nova/db/api.py 2011-08-24 23:44:25 +0000 |
278 | @@ -1436,3 +1436,79 @@ |
279 | key/value pairs specified in the extra specs dict argument""" |
280 | IMPL.instance_type_extra_specs_update_or_create(context, instance_type_id, |
281 | extra_specs) |
282 | + |
283 | + |
284 | +################## |
285 | + |
286 | + |
287 | +def volume_metadata_get(context, volume_id): |
288 | + """Get all metadata for a volume.""" |
289 | + return IMPL.volume_metadata_get(context, volume_id) |
290 | + |
291 | + |
292 | +def volume_metadata_delete(context, volume_id, key): |
293 | + """Delete the given metadata item.""" |
294 | + IMPL.volume_metadata_delete(context, volume_id, key) |
295 | + |
296 | + |
297 | +def volume_metadata_update(context, volume_id, metadata, delete): |
298 | + """Update metadata if it exists, otherwise create it.""" |
299 | + IMPL.volume_metadata_update(context, volume_id, metadata, delete) |
300 | + |
301 | + |
302 | +################## |
303 | + |
304 | + |
305 | +def volume_type_create(context, values): |
306 | + """Create a new volume type.""" |
307 | + return IMPL.volume_type_create(context, values) |
308 | + |
309 | + |
310 | +def volume_type_get_all(context, inactive=False): |
311 | + """Get all volume types.""" |
312 | + return IMPL.volume_type_get_all(context, inactive) |
313 | + |
314 | + |
315 | +def volume_type_get(context, id): |
316 | + """Get volume type by id.""" |
317 | + return IMPL.volume_type_get(context, id) |
318 | + |
319 | + |
320 | +def volume_type_get_by_name(context, name): |
321 | + """Get volume type by name.""" |
322 | + return IMPL.volume_type_get_by_name(context, name) |
323 | + |
324 | + |
325 | +def volume_type_destroy(context, name): |
326 | + """Delete a volume type.""" |
327 | + return IMPL.volume_type_destroy(context, name) |
328 | + |
329 | + |
330 | +def volume_type_purge(context, name): |
331 | + """Purges (removes) a volume type from DB. |
332 | + |
333 | + Use volume_type_destroy for most cases |
334 | + |
335 | + """ |
336 | + return IMPL.volume_type_purge(context, name) |
337 | + |
338 | + |
339 | +#################### |
340 | + |
341 | + |
342 | +def volume_type_extra_specs_get(context, volume_type_id): |
343 | + """Get all extra specs for a volume type.""" |
344 | + return IMPL.volume_type_extra_specs_get(context, volume_type_id) |
345 | + |
346 | + |
347 | +def volume_type_extra_specs_delete(context, volume_type_id, key): |
348 | + """Delete the given extra specs item.""" |
349 | + IMPL.volume_type_extra_specs_delete(context, volume_type_id, key) |
350 | + |
351 | + |
352 | +def volume_type_extra_specs_update_or_create(context, volume_type_id, |
353 | + extra_specs): |
354 | + """Create or update volume type extra specs. This adds or modifies the |
355 | + key/value pairs specified in the extra specs dict argument""" |
356 | + IMPL.volume_type_extra_specs_update_or_create(context, volume_type_id, |
357 | + extra_specs) |
358 | |
359 | === modified file 'nova/db/sqlalchemy/api.py' |
360 | --- nova/db/sqlalchemy/api.py 2011-08-22 23:35:09 +0000 |
361 | +++ nova/db/sqlalchemy/api.py 2011-08-24 23:44:25 +0000 |
362 | @@ -132,6 +132,20 @@ |
363 | return wrapper |
364 | |
365 | |
366 | +def require_volume_exists(f): |
367 | + """Decorator to require the specified volume to exist. |
368 | + |
369 | + Requres the wrapped function to use context and volume_id as |
370 | + their first two arguments. |
371 | + """ |
372 | + |
373 | + def wrapper(context, volume_id, *args, **kwargs): |
374 | + db.api.volume_get(context, volume_id) |
375 | + return f(context, volume_id, *args, **kwargs) |
376 | + wrapper.__name__ = f.__name__ |
377 | + return wrapper |
378 | + |
379 | + |
380 | ################### |
381 | |
382 | |
383 | @@ -1019,11 +1033,11 @@ |
384 | ################### |
385 | |
386 | |
387 | -def _metadata_refs(metadata_dict): |
388 | +def _metadata_refs(metadata_dict, meta_class): |
389 | metadata_refs = [] |
390 | if metadata_dict: |
391 | for k, v in metadata_dict.iteritems(): |
392 | - metadata_ref = models.InstanceMetadata() |
393 | + metadata_ref = meta_class() |
394 | metadata_ref['key'] = k |
395 | metadata_ref['value'] = v |
396 | metadata_refs.append(metadata_ref) |
397 | @@ -1037,8 +1051,8 @@ |
398 | context - request context object |
399 | values - dict containing column values. |
400 | """ |
401 | - values['metadata'] = _metadata_refs(values.get('metadata')) |
402 | - |
403 | + values['metadata'] = _metadata_refs(values.get('metadata'), |
404 | + models.InstanceMetadata) |
405 | instance_ref = models.Instance() |
406 | instance_ref['uuid'] = str(utils.gen_uuid()) |
407 | |
408 | @@ -2144,6 +2158,8 @@ |
409 | |
410 | @require_context |
411 | def volume_create(context, values): |
412 | + values['volume_metadata'] = _metadata_refs(values.get('metadata'), |
413 | + models.VolumeMetadata) |
414 | volume_ref = models.Volume() |
415 | volume_ref.update(values) |
416 | |
417 | @@ -2180,6 +2196,11 @@ |
418 | session.query(models.IscsiTarget).\ |
419 | filter_by(volume_id=volume_id).\ |
420 | update({'volume_id': None}) |
421 | + session.query(models.VolumeMetadata).\ |
422 | + filter_by(volume_id=volume_id).\ |
423 | + update({'deleted': True, |
424 | + 'deleted_at': utils.utcnow(), |
425 | + 'updated_at': literal_column('updated_at')}) |
426 | |
427 | |
428 | @require_admin_context |
429 | @@ -2203,12 +2224,16 @@ |
430 | if is_admin_context(context): |
431 | result = session.query(models.Volume).\ |
432 | options(joinedload('instance')).\ |
433 | + options(joinedload('volume_metadata')).\ |
434 | + options(joinedload('volume_type')).\ |
435 | filter_by(id=volume_id).\ |
436 | filter_by(deleted=can_read_deleted(context)).\ |
437 | first() |
438 | elif is_user_context(context): |
439 | result = session.query(models.Volume).\ |
440 | options(joinedload('instance')).\ |
441 | + options(joinedload('volume_metadata')).\ |
442 | + options(joinedload('volume_type')).\ |
443 | filter_by(project_id=context.project_id).\ |
444 | filter_by(id=volume_id).\ |
445 | filter_by(deleted=False).\ |
446 | @@ -2224,6 +2249,8 @@ |
447 | session = get_session() |
448 | return session.query(models.Volume).\ |
449 | options(joinedload('instance')).\ |
450 | + options(joinedload('volume_metadata')).\ |
451 | + options(joinedload('volume_type')).\ |
452 | filter_by(deleted=can_read_deleted(context)).\ |
453 | all() |
454 | |
455 | @@ -2233,6 +2260,8 @@ |
456 | session = get_session() |
457 | return session.query(models.Volume).\ |
458 | options(joinedload('instance')).\ |
459 | + options(joinedload('volume_metadata')).\ |
460 | + options(joinedload('volume_type')).\ |
461 | filter_by(host=host).\ |
462 | filter_by(deleted=can_read_deleted(context)).\ |
463 | all() |
464 | @@ -2242,6 +2271,8 @@ |
465 | def volume_get_all_by_instance(context, instance_id): |
466 | session = get_session() |
467 | result = session.query(models.Volume).\ |
468 | + options(joinedload('volume_metadata')).\ |
469 | + options(joinedload('volume_type')).\ |
470 | filter_by(instance_id=instance_id).\ |
471 | filter_by(deleted=False).\ |
472 | all() |
473 | @@ -2257,6 +2288,8 @@ |
474 | session = get_session() |
475 | return session.query(models.Volume).\ |
476 | options(joinedload('instance')).\ |
477 | + options(joinedload('volume_metadata')).\ |
478 | + options(joinedload('volume_type')).\ |
479 | filter_by(project_id=project_id).\ |
480 | filter_by(deleted=can_read_deleted(context)).\ |
481 | all() |
482 | @@ -2269,6 +2302,8 @@ |
483 | filter_by(id=volume_id).\ |
484 | filter_by(deleted=can_read_deleted(context)).\ |
485 | options(joinedload('instance')).\ |
486 | + options(joinedload('volume_metadata')).\ |
487 | + options(joinedload('volume_type')).\ |
488 | first() |
489 | if not result: |
490 | raise exception.VolumeNotFound(volume_id=volume_id) |
491 | @@ -2303,12 +2338,116 @@ |
492 | @require_context |
493 | def volume_update(context, volume_id, values): |
494 | session = get_session() |
495 | + metadata = values.get('metadata') |
496 | + if metadata is not None: |
497 | + volume_metadata_update(context, |
498 | + volume_id, |
499 | + values.pop('metadata'), |
500 | + delete=True) |
501 | with session.begin(): |
502 | volume_ref = volume_get(context, volume_id, session=session) |
503 | volume_ref.update(values) |
504 | volume_ref.save(session=session) |
505 | |
506 | |
507 | +#################### |
508 | + |
509 | + |
510 | +@require_context |
511 | +@require_volume_exists |
512 | +def volume_metadata_get(context, volume_id): |
513 | + session = get_session() |
514 | + |
515 | + meta_results = session.query(models.VolumeMetadata).\ |
516 | + filter_by(volume_id=volume_id).\ |
517 | + filter_by(deleted=False).\ |
518 | + all() |
519 | + |
520 | + meta_dict = {} |
521 | + for i in meta_results: |
522 | + meta_dict[i['key']] = i['value'] |
523 | + return meta_dict |
524 | + |
525 | + |
526 | +@require_context |
527 | +@require_volume_exists |
528 | +def volume_metadata_delete(context, volume_id, key): |
529 | + session = get_session() |
530 | + session.query(models.VolumeMetadata).\ |
531 | + filter_by(volume_id=volume_id).\ |
532 | + filter_by(key=key).\ |
533 | + filter_by(deleted=False).\ |
534 | + update({'deleted': True, |
535 | + 'deleted_at': utils.utcnow(), |
536 | + 'updated_at': literal_column('updated_at')}) |
537 | + |
538 | + |
539 | +@require_context |
540 | +@require_volume_exists |
541 | +def volume_metadata_delete_all(context, volume_id): |
542 | + session = get_session() |
543 | + session.query(models.VolumeMetadata).\ |
544 | + filter_by(volume_id=volume_id).\ |
545 | + filter_by(deleted=False).\ |
546 | + update({'deleted': True, |
547 | + 'deleted_at': utils.utcnow(), |
548 | + 'updated_at': literal_column('updated_at')}) |
549 | + |
550 | + |
551 | +@require_context |
552 | +@require_volume_exists |
553 | +def volume_metadata_get_item(context, volume_id, key, session=None): |
554 | + if not session: |
555 | + session = get_session() |
556 | + |
557 | + meta_result = session.query(models.VolumeMetadata).\ |
558 | + filter_by(volume_id=volume_id).\ |
559 | + filter_by(key=key).\ |
560 | + filter_by(deleted=False).\ |
561 | + first() |
562 | + |
563 | + if not meta_result: |
564 | + raise exception.VolumeMetadataNotFound(metadata_key=key, |
565 | + volume_id=volume_id) |
566 | + return meta_result |
567 | + |
568 | + |
569 | +@require_context |
570 | +@require_volume_exists |
571 | +def volume_metadata_update(context, volume_id, metadata, delete): |
572 | + session = get_session() |
573 | + |
574 | + # Set existing metadata to deleted if delete argument is True |
575 | + if delete: |
576 | + original_metadata = volume_metadata_get(context, volume_id) |
577 | + for meta_key, meta_value in original_metadata.iteritems(): |
578 | + if meta_key not in metadata: |
579 | + meta_ref = volume_metadata_get_item(context, volume_id, |
580 | + meta_key, session) |
581 | + meta_ref.update({'deleted': True}) |
582 | + meta_ref.save(session=session) |
583 | + |
584 | + meta_ref = None |
585 | + |
586 | + # Now update all existing items with new values, or create new meta objects |
587 | + for meta_key, meta_value in metadata.iteritems(): |
588 | + |
589 | + # update the value whether it exists or not |
590 | + item = {"value": meta_value} |
591 | + |
592 | + try: |
593 | + meta_ref = volume_metadata_get_item(context, volume_id, |
594 | + meta_key, session) |
595 | + except exception.VolumeMetadataNotFound, e: |
596 | + meta_ref = models.VolumeMetadata() |
597 | + item.update({"key": meta_key, "volume_id": volume_id}) |
598 | + |
599 | + meta_ref.update(item) |
600 | + meta_ref.save(session=session) |
601 | + |
602 | + return metadata |
603 | + |
604 | + |
605 | ################### |
606 | |
607 | |
608 | @@ -3143,7 +3282,7 @@ |
609 | |
610 | |
611 | def _dict_with_extra_specs(inst_type_query): |
612 | - """Takes an instance type query returned by sqlalchemy |
613 | + """Takes an instance OR volume type query returned by sqlalchemy |
614 | and returns it as a dictionary, converting the extra_specs |
615 | entry from a list of dicts: |
616 | |
617 | @@ -3525,3 +3664,176 @@ |
618 | "deleted": 0}) |
619 | spec_ref.save(session=session) |
620 | return specs |
621 | + |
622 | + |
623 | +################## |
624 | + |
625 | + |
626 | +@require_admin_context |
627 | +def volume_type_create(_context, values): |
628 | + """Create a new instance type. In order to pass in extra specs, |
629 | + the values dict should contain a 'extra_specs' key/value pair: |
630 | + |
631 | + {'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}} |
632 | + |
633 | + """ |
634 | + try: |
635 | + specs = values.get('extra_specs') |
636 | + |
637 | + values['extra_specs'] = _metadata_refs(values.get('extra_specs'), |
638 | + models.VolumeTypeExtraSpecs) |
639 | + volume_type_ref = models.VolumeTypes() |
640 | + volume_type_ref.update(values) |
641 | + volume_type_ref.save() |
642 | + except Exception, e: |
643 | + raise exception.DBError(e) |
644 | + return volume_type_ref |
645 | + |
646 | + |
647 | +@require_context |
648 | +def volume_type_get_all(context, inactive=False, filters={}): |
649 | + """ |
650 | + Returns a dict describing all volume_types with name as key. |
651 | + """ |
652 | + session = get_session() |
653 | + if inactive: |
654 | + vol_types = session.query(models.VolumeTypes).\ |
655 | + options(joinedload('extra_specs')).\ |
656 | + order_by("name").\ |
657 | + all() |
658 | + else: |
659 | + vol_types = session.query(models.VolumeTypes).\ |
660 | + options(joinedload('extra_specs')).\ |
661 | + filter_by(deleted=False).\ |
662 | + order_by("name").\ |
663 | + all() |
664 | + vol_dict = {} |
665 | + if vol_types: |
666 | + for i in vol_types: |
667 | + vol_dict[i['name']] = _dict_with_extra_specs(i) |
668 | + return vol_dict |
669 | + |
670 | + |
671 | +@require_context |
672 | +def volume_type_get(context, id): |
673 | + """Returns a dict describing specific volume_type""" |
674 | + session = get_session() |
675 | + vol_type = session.query(models.VolumeTypes).\ |
676 | + options(joinedload('extra_specs')).\ |
677 | + filter_by(id=id).\ |
678 | + first() |
679 | + |
680 | + if not vol_type: |
681 | + raise exception.VolumeTypeNotFound(volume_type=id) |
682 | + else: |
683 | + return _dict_with_extra_specs(vol_type) |
684 | + |
685 | + |
686 | +@require_context |
687 | +def volume_type_get_by_name(context, name): |
688 | + """Returns a dict describing specific volume_type""" |
689 | + session = get_session() |
690 | + vol_type = session.query(models.VolumeTypes).\ |
691 | + options(joinedload('extra_specs')).\ |
692 | + filter_by(name=name).\ |
693 | + first() |
694 | + if not vol_type: |
695 | + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) |
696 | + else: |
697 | + return _dict_with_extra_specs(vol_type) |
698 | + |
699 | + |
700 | +@require_admin_context |
701 | +def volume_type_destroy(context, name): |
702 | + """ Marks specific volume_type as deleted""" |
703 | + session = get_session() |
704 | + volume_type_ref = session.query(models.VolumeTypes).\ |
705 | + filter_by(name=name) |
706 | + records = volume_type_ref.update(dict(deleted=True)) |
707 | + if records == 0: |
708 | + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) |
709 | + else: |
710 | + return volume_type_ref |
711 | + |
712 | + |
713 | +@require_admin_context |
714 | +def volume_type_purge(context, name): |
715 | + """ Removes specific volume_type from DB |
716 | + Usually volume_type_destroy should be used |
717 | + """ |
718 | + session = get_session() |
719 | + volume_type_ref = session.query(models.VolumeTypes).\ |
720 | + filter_by(name=name) |
721 | + records = volume_type_ref.delete() |
722 | + if records == 0: |
723 | + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) |
724 | + else: |
725 | + return volume_type_ref |
726 | + |
727 | + |
728 | +#################### |
729 | + |
730 | + |
731 | +@require_context |
732 | +def volume_type_extra_specs_get(context, volume_type_id): |
733 | + session = get_session() |
734 | + |
735 | + spec_results = session.query(models.VolumeTypeExtraSpecs).\ |
736 | + filter_by(volume_type_id=volume_type_id).\ |
737 | + filter_by(deleted=False).\ |
738 | + all() |
739 | + |
740 | + spec_dict = {} |
741 | + for i in spec_results: |
742 | + spec_dict[i['key']] = i['value'] |
743 | + return spec_dict |
744 | + |
745 | + |
746 | +@require_context |
747 | +def volume_type_extra_specs_delete(context, volume_type_id, key): |
748 | + session = get_session() |
749 | + session.query(models.VolumeTypeExtraSpecs).\ |
750 | + filter_by(volume_type_id=volume_type_id).\ |
751 | + filter_by(key=key).\ |
752 | + filter_by(deleted=False).\ |
753 | + update({'deleted': True, |
754 | + 'deleted_at': utils.utcnow(), |
755 | + 'updated_at': literal_column('updated_at')}) |
756 | + |
757 | + |
758 | +@require_context |
759 | +def volume_type_extra_specs_get_item(context, volume_type_id, key, |
760 | + session=None): |
761 | + |
762 | + if not session: |
763 | + session = get_session() |
764 | + |
765 | + spec_result = session.query(models.VolumeTypeExtraSpecs).\ |
766 | + filter_by(volume_type_id=volume_type_id).\ |
767 | + filter_by(key=key).\ |
768 | + filter_by(deleted=False).\ |
769 | + first() |
770 | + |
771 | + if not spec_result: |
772 | + raise exception.\ |
773 | + VolumeTypeExtraSpecsNotFound(extra_specs_key=key, |
774 | + volume_type_id=volume_type_id) |
775 | + return spec_result |
776 | + |
777 | + |
778 | +@require_context |
779 | +def volume_type_extra_specs_update_or_create(context, volume_type_id, |
780 | + specs): |
781 | + session = get_session() |
782 | + spec_ref = None |
783 | + for key, value in specs.iteritems(): |
784 | + try: |
785 | + spec_ref = volume_type_extra_specs_get_item( |
786 | + context, volume_type_id, key, session) |
787 | + except exception.VolumeTypeExtraSpecsNotFound, e: |
788 | + spec_ref = models.VolumeTypeExtraSpecs() |
789 | + spec_ref.update({"key": key, "value": value, |
790 | + "volume_type_id": volume_type_id, |
791 | + "deleted": 0}) |
792 | + spec_ref.save(session=session) |
793 | + return specs |
794 | |
795 | === added file 'nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py' |
796 | --- nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py 1970-01-01 00:00:00 +0000 |
797 | +++ nova/db/sqlalchemy/migrate_repo/versions/042_add_volume_types_and_extradata.py 2011-08-24 23:44:25 +0000 |
798 | @@ -0,0 +1,115 @@ |
799 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
800 | + |
801 | +# Copyright (c) 2011 Zadara Storage Inc. |
802 | +# Copyright (c) 2011 OpenStack LLC. |
803 | +# |
804 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
805 | +# not use this file except in compliance with the License. You may obtain |
806 | +# a copy of the License at |
807 | +# |
808 | +# http://www.apache.org/licenses/LICENSE-2.0 |
809 | +# |
810 | +# Unless required by applicable law or agreed to in writing, software |
811 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
812 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
813 | +# License for the specific language governing permissions and limitations |
814 | +# under the License. |
815 | + |
816 | +from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table |
817 | +from sqlalchemy import Text, Boolean, ForeignKey |
818 | + |
819 | +from nova import log as logging |
820 | + |
821 | +meta = MetaData() |
822 | + |
823 | +# Just for the ForeignKey and column creation to succeed, these are not the |
824 | +# actual definitions of tables . |
825 | +# |
826 | + |
827 | +volumes = Table('volumes', meta, |
828 | + Column('id', Integer(), primary_key=True, nullable=False), |
829 | + ) |
830 | + |
831 | +volume_type_id = Column('volume_type_id', Integer(), nullable=True) |
832 | + |
833 | + |
834 | +# New Tables |
835 | +# |
836 | + |
837 | +volume_types = Table('volume_types', meta, |
838 | + Column('created_at', DateTime(timezone=False)), |
839 | + Column('updated_at', DateTime(timezone=False)), |
840 | + Column('deleted_at', DateTime(timezone=False)), |
841 | + Column('deleted', Boolean(create_constraint=True, name=None)), |
842 | + Column('id', Integer(), primary_key=True, nullable=False), |
843 | + Column('name', |
844 | + String(length=255, convert_unicode=False, assert_unicode=None, |
845 | + unicode_error=None, _warn_on_bytestring=False), |
846 | + unique=True)) |
847 | + |
848 | +volume_type_extra_specs_table = Table('volume_type_extra_specs', meta, |
849 | + Column('created_at', DateTime(timezone=False)), |
850 | + Column('updated_at', DateTime(timezone=False)), |
851 | + Column('deleted_at', DateTime(timezone=False)), |
852 | + Column('deleted', Boolean(create_constraint=True, name=None)), |
853 | + Column('id', Integer(), primary_key=True, nullable=False), |
854 | + Column('volume_type_id', |
855 | + Integer(), |
856 | + ForeignKey('volume_types.id'), |
857 | + nullable=False), |
858 | + Column('key', |
859 | + String(length=255, convert_unicode=False, assert_unicode=None, |
860 | + unicode_error=None, _warn_on_bytestring=False)), |
861 | + Column('value', |
862 | + String(length=255, convert_unicode=False, assert_unicode=None, |
863 | + unicode_error=None, _warn_on_bytestring=False))) |
864 | + |
865 | + |
866 | +volume_metadata_table = Table('volume_metadata', meta, |
867 | + Column('created_at', DateTime(timezone=False)), |
868 | + Column('updated_at', DateTime(timezone=False)), |
869 | + Column('deleted_at', DateTime(timezone=False)), |
870 | + Column('deleted', Boolean(create_constraint=True, name=None)), |
871 | + Column('id', Integer(), primary_key=True, nullable=False), |
872 | + Column('volume_id', |
873 | + Integer(), |
874 | + ForeignKey('volumes.id'), |
875 | + nullable=False), |
876 | + Column('key', |
877 | + String(length=255, convert_unicode=False, assert_unicode=None, |
878 | + unicode_error=None, _warn_on_bytestring=False)), |
879 | + Column('value', |
880 | + String(length=255, convert_unicode=False, assert_unicode=None, |
881 | + unicode_error=None, _warn_on_bytestring=False))) |
882 | + |
883 | + |
884 | +new_tables = (volume_types, |
885 | + volume_type_extra_specs_table, |
886 | + volume_metadata_table) |
887 | + |
888 | +# |
889 | +# Tables to alter |
890 | +# |
891 | + |
892 | + |
893 | +def upgrade(migrate_engine): |
894 | + meta.bind = migrate_engine |
895 | + |
896 | + for table in new_tables: |
897 | + try: |
898 | + table.create() |
899 | + except Exception: |
900 | + logging.info(repr(table)) |
901 | + logging.exception('Exception while creating table') |
902 | + raise |
903 | + |
904 | + volumes.create_column(volume_type_id) |
905 | + |
906 | + |
907 | +def downgrade(migrate_engine): |
908 | + meta.bind = migrate_engine |
909 | + |
910 | + volumes.drop_column(volume_type_id) |
911 | + |
912 | + for table in new_tables: |
913 | + table.drop() |
914 | |
915 | === modified file 'nova/db/sqlalchemy/migration.py' |
916 | --- nova/db/sqlalchemy/migration.py 2011-06-24 12:01:51 +0000 |
917 | +++ nova/db/sqlalchemy/migration.py 2011-08-24 23:44:25 +0000 |
918 | @@ -64,7 +64,8 @@ |
919 | 'users', 'user_project_association', |
920 | 'user_project_role_association', |
921 | 'user_role_association', |
922 | - 'volumes'): |
923 | + 'volumes', 'volume_metadata', |
924 | + 'volume_types', 'volume_type_extra_specs'): |
925 | assert table in meta.tables |
926 | return db_version_control(1) |
927 | except AssertionError: |
928 | |
929 | === modified file 'nova/db/sqlalchemy/models.py' |
930 | --- nova/db/sqlalchemy/models.py 2011-08-23 04:17:57 +0000 |
931 | +++ nova/db/sqlalchemy/models.py 2011-08-24 23:44:25 +0000 |
932 | @@ -318,6 +318,50 @@ |
933 | provider_location = Column(String(255)) |
934 | provider_auth = Column(String(255)) |
935 | |
936 | + volume_type_id = Column(Integer) |
937 | + |
938 | + |
939 | +class VolumeMetadata(BASE, NovaBase): |
940 | + """Represents a metadata key/value pair for a volume""" |
941 | + __tablename__ = 'volume_metadata' |
942 | + id = Column(Integer, primary_key=True) |
943 | + key = Column(String(255)) |
944 | + value = Column(String(255)) |
945 | + volume_id = Column(Integer, ForeignKey('volumes.id'), nullable=False) |
946 | + volume = relationship(Volume, backref="volume_metadata", |
947 | + foreign_keys=volume_id, |
948 | + primaryjoin='and_(' |
949 | + 'VolumeMetadata.volume_id == Volume.id,' |
950 | + 'VolumeMetadata.deleted == False)') |
951 | + |
952 | + |
953 | +class VolumeTypes(BASE, NovaBase): |
954 | + """Represent possible volume_types of volumes offered""" |
955 | + __tablename__ = "volume_types" |
956 | + id = Column(Integer, primary_key=True) |
957 | + name = Column(String(255), unique=True) |
958 | + |
959 | + volumes = relationship(Volume, |
960 | + backref=backref('volume_type', uselist=False), |
961 | + foreign_keys=id, |
962 | + primaryjoin='and_(Volume.volume_type_id == ' |
963 | + 'VolumeTypes.id)') |
964 | + |
965 | + |
966 | +class VolumeTypeExtraSpecs(BASE, NovaBase): |
967 | + """Represents additional specs as key/value pairs for a volume_type""" |
968 | + __tablename__ = 'volume_type_extra_specs' |
969 | + id = Column(Integer, primary_key=True) |
970 | + key = Column(String(255)) |
971 | + value = Column(String(255)) |
972 | + volume_type_id = Column(Integer, ForeignKey('volume_types.id'), |
973 | + nullable=False) |
974 | + volume_type = relationship(VolumeTypes, backref="extra_specs", |
975 | + foreign_keys=volume_type_id, |
976 | + primaryjoin='and_(' |
977 | + 'VolumeTypeExtraSpecs.volume_type_id == VolumeTypes.id,' |
978 | + 'VolumeTypeExtraSpecs.deleted == False)') |
979 | + |
980 | |
981 | class Quota(BASE, NovaBase): |
982 | """Represents a single quota override for a project. |
983 | @@ -803,6 +847,7 @@ |
984 | Network, SecurityGroup, SecurityGroupIngressRule, |
985 | SecurityGroupInstanceAssociation, AuthToken, User, |
986 | Project, Certificate, ConsolePool, Console, Zone, |
987 | + VolumeMetadata, VolumeTypes, VolumeTypeExtraSpecs, |
988 | AgentBuild, InstanceMetadata, InstanceTypeExtraSpecs, Migration) |
989 | engine = create_engine(FLAGS.sql_connection, echo=False) |
990 | for model in models: |
991 | |
992 | === modified file 'nova/exception.py' |
993 | --- nova/exception.py 2011-08-22 23:35:09 +0000 |
994 | +++ nova/exception.py 2011-08-24 23:44:25 +0000 |
995 | @@ -197,6 +197,10 @@ |
996 | message = _("Invalid instance type %(instance_type)s.") |
997 | |
998 | |
999 | +class InvalidVolumeType(Invalid): |
1000 | + message = _("Invalid volume type %(volume_type)s.") |
1001 | + |
1002 | + |
1003 | class InvalidPortRange(Invalid): |
1004 | message = _("Invalid port range %(from_port)s:%(to_port)s.") |
1005 | |
1006 | @@ -338,6 +342,29 @@ |
1007 | message = _("Volume not found for instance %(instance_id)s.") |
1008 | |
1009 | |
1010 | +class VolumeMetadataNotFound(NotFound): |
1011 | + message = _("Volume %(volume_id)s has no metadata with " |
1012 | + "key %(metadata_key)s.") |
1013 | + |
1014 | + |
1015 | +class NoVolumeTypesFound(NotFound): |
1016 | + message = _("Zero volume types found.") |
1017 | + |
1018 | + |
1019 | +class VolumeTypeNotFound(NotFound): |
1020 | + message = _("Volume type %(volume_type_id)s could not be found.") |
1021 | + |
1022 | + |
1023 | +class VolumeTypeNotFoundByName(VolumeTypeNotFound): |
1024 | + message = _("Volume type with name %(volume_type_name)s " |
1025 | + "could not be found.") |
1026 | + |
1027 | + |
1028 | +class VolumeTypeExtraSpecsNotFound(NotFound): |
1029 | + message = _("Volume Type %(volume_type_id)s has no extra specs with " |
1030 | + "key %(extra_specs_key)s.") |
1031 | + |
1032 | + |
1033 | class SnapshotNotFound(NotFound): |
1034 | message = _("Snapshot %(snapshot_id)s could not be found.") |
1035 | |
1036 | |
1037 | === modified file 'nova/tests/api/openstack/test_extensions.py' |
1038 | --- nova/tests/api/openstack/test_extensions.py 2011-08-22 23:35:09 +0000 |
1039 | +++ nova/tests/api/openstack/test_extensions.py 2011-08-24 23:44:25 +0000 |
1040 | @@ -97,6 +97,7 @@ |
1041 | "SecurityGroups", |
1042 | "VirtualInterfaces", |
1043 | "Volumes", |
1044 | + "VolumeTypes", |
1045 | ] |
1046 | self.ext_list.sort() |
1047 | |
1048 | |
1049 | === added file 'nova/tests/api/openstack/test_volume_types.py' |
1050 | --- nova/tests/api/openstack/test_volume_types.py 1970-01-01 00:00:00 +0000 |
1051 | +++ nova/tests/api/openstack/test_volume_types.py 2011-08-24 23:44:25 +0000 |
1052 | @@ -0,0 +1,171 @@ |
1053 | +# Copyright 2011 OpenStack LLC. |
1054 | +# All Rights Reserved. |
1055 | +# |
1056 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1057 | +# not use this file except in compliance with the License. You may obtain |
1058 | +# a copy of the License at |
1059 | +# |
1060 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1061 | +# |
1062 | +# Unless required by applicable law or agreed to in writing, software |
1063 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1064 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1065 | +# License for the specific language governing permissions and limitations |
1066 | +# under the License. |
1067 | + |
1068 | +import json |
1069 | +import stubout |
1070 | +import webob |
1071 | + |
1072 | +from nova import exception |
1073 | +from nova import context |
1074 | +from nova import test |
1075 | +from nova import log as logging |
1076 | +from nova.volume import volume_types |
1077 | +from nova.tests.api.openstack import fakes |
1078 | + |
1079 | +LOG = logging.getLogger('nova.tests.api.openstack.test_volume_types') |
1080 | + |
1081 | +last_param = {} |
1082 | + |
1083 | + |
1084 | +def stub_volume_type(id): |
1085 | + specs = { |
1086 | + "key1": "value1", |
1087 | + "key2": "value2", |
1088 | + "key3": "value3", |
1089 | + "key4": "value4", |
1090 | + "key5": "value5"} |
1091 | + return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) |
1092 | + |
1093 | + |
1094 | +def return_volume_types_get_all_types(context): |
1095 | + return dict(vol_type_1=stub_volume_type(1), |
1096 | + vol_type_2=stub_volume_type(2), |
1097 | + vol_type_3=stub_volume_type(3)) |
1098 | + |
1099 | + |
1100 | +def return_empty_volume_types_get_all_types(context): |
1101 | + return {} |
1102 | + |
1103 | + |
1104 | +def return_volume_types_get_volume_type(context, id): |
1105 | + if id == "777": |
1106 | + raise exception.VolumeTypeNotFound(volume_type_id=id) |
1107 | + return stub_volume_type(int(id)) |
1108 | + |
1109 | + |
1110 | +def return_volume_types_destroy(context, name): |
1111 | + if name == "777": |
1112 | + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) |
1113 | + pass |
1114 | + |
1115 | + |
1116 | +def return_volume_types_create(context, name, specs): |
1117 | + pass |
1118 | + |
1119 | + |
1120 | +def return_volume_types_get_by_name(context, name): |
1121 | + if name == "777": |
1122 | + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) |
1123 | + return stub_volume_type(int(name.split("_")[2])) |
1124 | + |
1125 | + |
1126 | +class VolumeTypesApiTest(test.TestCase): |
1127 | + def setUp(self): |
1128 | + super(VolumeTypesApiTest, self).setUp() |
1129 | + fakes.stub_out_key_pair_funcs(self.stubs) |
1130 | + |
1131 | + def tearDown(self): |
1132 | + self.stubs.UnsetAll() |
1133 | + super(VolumeTypesApiTest, self).tearDown() |
1134 | + |
1135 | + def test_volume_types_index(self): |
1136 | + self.stubs.Set(volume_types, 'get_all_types', |
1137 | + return_volume_types_get_all_types) |
1138 | + req = webob.Request.blank('/v1.1/123/os-volume-types') |
1139 | + res = req.get_response(fakes.wsgi_app()) |
1140 | + self.assertEqual(200, res.status_int) |
1141 | + res_dict = json.loads(res.body) |
1142 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1143 | + |
1144 | + self.assertEqual(3, len(res_dict)) |
1145 | + for name in ['vol_type_1', 'vol_type_2', 'vol_type_3']: |
1146 | + self.assertEqual(name, res_dict[name]['name']) |
1147 | + self.assertEqual('value1', res_dict[name]['extra_specs']['key1']) |
1148 | + |
1149 | + def test_volume_types_index_no_data(self): |
1150 | + self.stubs.Set(volume_types, 'get_all_types', |
1151 | + return_empty_volume_types_get_all_types) |
1152 | + req = webob.Request.blank('/v1.1/123/os-volume-types') |
1153 | + res = req.get_response(fakes.wsgi_app()) |
1154 | + res_dict = json.loads(res.body) |
1155 | + self.assertEqual(200, res.status_int) |
1156 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1157 | + self.assertEqual(0, len(res_dict)) |
1158 | + |
1159 | + def test_volume_types_show(self): |
1160 | + self.stubs.Set(volume_types, 'get_volume_type', |
1161 | + return_volume_types_get_volume_type) |
1162 | + req = webob.Request.blank('/v1.1/123/os-volume-types/1') |
1163 | + res = req.get_response(fakes.wsgi_app()) |
1164 | + self.assertEqual(200, res.status_int) |
1165 | + res_dict = json.loads(res.body) |
1166 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1167 | + self.assertEqual(1, len(res_dict)) |
1168 | + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) |
1169 | + |
1170 | + def test_volume_types_show_not_found(self): |
1171 | + self.stubs.Set(volume_types, 'get_volume_type', |
1172 | + return_volume_types_get_volume_type) |
1173 | + req = webob.Request.blank('/v1.1/123/os-volume-types/777') |
1174 | + res = req.get_response(fakes.wsgi_app()) |
1175 | + self.assertEqual(404, res.status_int) |
1176 | + |
1177 | + def test_volume_types_delete(self): |
1178 | + self.stubs.Set(volume_types, 'get_volume_type', |
1179 | + return_volume_types_get_volume_type) |
1180 | + self.stubs.Set(volume_types, 'destroy', |
1181 | + return_volume_types_destroy) |
1182 | + req = webob.Request.blank('/v1.1/123/os-volume-types/1') |
1183 | + req.method = 'DELETE' |
1184 | + res = req.get_response(fakes.wsgi_app()) |
1185 | + self.assertEqual(200, res.status_int) |
1186 | + |
1187 | + def test_volume_types_delete_not_found(self): |
1188 | + self.stubs.Set(volume_types, 'get_volume_type', |
1189 | + return_volume_types_get_volume_type) |
1190 | + self.stubs.Set(volume_types, 'destroy', |
1191 | + return_volume_types_destroy) |
1192 | + req = webob.Request.blank('/v1.1/123/os-volume-types/777') |
1193 | + req.method = 'DELETE' |
1194 | + res = req.get_response(fakes.wsgi_app()) |
1195 | + self.assertEqual(404, res.status_int) |
1196 | + |
1197 | + def test_create(self): |
1198 | + self.stubs.Set(volume_types, 'create', |
1199 | + return_volume_types_create) |
1200 | + self.stubs.Set(volume_types, 'get_volume_type_by_name', |
1201 | + return_volume_types_get_by_name) |
1202 | + req = webob.Request.blank('/v1.1/123/os-volume-types') |
1203 | + req.method = 'POST' |
1204 | + req.body = '{"volume_type": {"name": "vol_type_1", '\ |
1205 | + '"extra_specs": {"key1": "value1"}}}' |
1206 | + req.headers["content-type"] = "application/json" |
1207 | + res = req.get_response(fakes.wsgi_app()) |
1208 | + self.assertEqual(200, res.status_int) |
1209 | + res_dict = json.loads(res.body) |
1210 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1211 | + self.assertEqual(1, len(res_dict)) |
1212 | + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) |
1213 | + |
1214 | + def test_create_empty_body(self): |
1215 | + self.stubs.Set(volume_types, 'create', |
1216 | + return_volume_types_create) |
1217 | + self.stubs.Set(volume_types, 'get_volume_type_by_name', |
1218 | + return_volume_types_get_by_name) |
1219 | + req = webob.Request.blank('/v1.1/123/os-volume-types') |
1220 | + req.method = 'POST' |
1221 | + req.headers["content-type"] = "application/json" |
1222 | + res = req.get_response(fakes.wsgi_app()) |
1223 | + self.assertEqual(400, res.status_int) |
1224 | |
1225 | === added file 'nova/tests/api/openstack/test_volume_types_extra_specs.py' |
1226 | --- nova/tests/api/openstack/test_volume_types_extra_specs.py 1970-01-01 00:00:00 +0000 |
1227 | +++ nova/tests/api/openstack/test_volume_types_extra_specs.py 2011-08-24 23:44:25 +0000 |
1228 | @@ -0,0 +1,181 @@ |
1229 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1230 | + |
1231 | +# Copyright (c) 2011 Zadara Storage Inc. |
1232 | +# Copyright (c) 2011 OpenStack LLC. |
1233 | +# Copyright 2011 University of Southern California |
1234 | +# All Rights Reserved. |
1235 | +# |
1236 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1237 | +# not use this file except in compliance with the License. You may obtain |
1238 | +# a copy of the License at |
1239 | +# |
1240 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1241 | +# |
1242 | +# Unless required by applicable law or agreed to in writing, software |
1243 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1244 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1245 | +# License for the specific language governing permissions and limitations |
1246 | +# under the License. |
1247 | + |
1248 | +import json |
1249 | +import stubout |
1250 | +import webob |
1251 | +import os.path |
1252 | + |
1253 | + |
1254 | +from nova import test |
1255 | +from nova.api import openstack |
1256 | +from nova.api.openstack import extensions |
1257 | +from nova.tests.api.openstack import fakes |
1258 | +import nova.wsgi |
1259 | + |
1260 | + |
1261 | +def return_create_volume_type_extra_specs(context, volume_type_id, |
1262 | + extra_specs): |
1263 | + return stub_volume_type_extra_specs() |
1264 | + |
1265 | + |
1266 | +def return_volume_type_extra_specs(context, volume_type_id): |
1267 | + return stub_volume_type_extra_specs() |
1268 | + |
1269 | + |
1270 | +def return_empty_volume_type_extra_specs(context, volume_type_id): |
1271 | + return {} |
1272 | + |
1273 | + |
1274 | +def delete_volume_type_extra_specs(context, volume_type_id, key): |
1275 | + pass |
1276 | + |
1277 | + |
1278 | +def stub_volume_type_extra_specs(): |
1279 | + specs = { |
1280 | + "key1": "value1", |
1281 | + "key2": "value2", |
1282 | + "key3": "value3", |
1283 | + "key4": "value4", |
1284 | + "key5": "value5"} |
1285 | + return specs |
1286 | + |
1287 | + |
1288 | +class VolumeTypesExtraSpecsTest(test.TestCase): |
1289 | + |
1290 | + def setUp(self): |
1291 | + super(VolumeTypesExtraSpecsTest, self).setUp() |
1292 | + fakes.stub_out_key_pair_funcs(self.stubs) |
1293 | + self.api_path = '/v1.1/123/os-volume-types/1/extra_specs' |
1294 | + |
1295 | + def test_index(self): |
1296 | + self.stubs.Set(nova.db.api, 'volume_type_extra_specs_get', |
1297 | + return_volume_type_extra_specs) |
1298 | + request = webob.Request.blank(self.api_path) |
1299 | + res = request.get_response(fakes.wsgi_app()) |
1300 | + self.assertEqual(200, res.status_int) |
1301 | + res_dict = json.loads(res.body) |
1302 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1303 | + self.assertEqual('value1', res_dict['extra_specs']['key1']) |
1304 | + |
1305 | + def test_index_no_data(self): |
1306 | + self.stubs.Set(nova.db.api, 'volume_type_extra_specs_get', |
1307 | + return_empty_volume_type_extra_specs) |
1308 | + req = webob.Request.blank(self.api_path) |
1309 | + res = req.get_response(fakes.wsgi_app()) |
1310 | + res_dict = json.loads(res.body) |
1311 | + self.assertEqual(200, res.status_int) |
1312 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1313 | + self.assertEqual(0, len(res_dict['extra_specs'])) |
1314 | + |
1315 | + def test_show(self): |
1316 | + self.stubs.Set(nova.db.api, 'volume_type_extra_specs_get', |
1317 | + return_volume_type_extra_specs) |
1318 | + req = webob.Request.blank(self.api_path + '/key5') |
1319 | + res = req.get_response(fakes.wsgi_app()) |
1320 | + self.assertEqual(200, res.status_int) |
1321 | + res_dict = json.loads(res.body) |
1322 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1323 | + self.assertEqual('value5', res_dict['key5']) |
1324 | + |
1325 | + def test_show_spec_not_found(self): |
1326 | + self.stubs.Set(nova.db.api, 'volume_type_extra_specs_get', |
1327 | + return_empty_volume_type_extra_specs) |
1328 | + req = webob.Request.blank(self.api_path + '/key6') |
1329 | + res = req.get_response(fakes.wsgi_app()) |
1330 | + res_dict = json.loads(res.body) |
1331 | + self.assertEqual(404, res.status_int) |
1332 | + |
1333 | + def test_delete(self): |
1334 | + self.stubs.Set(nova.db.api, 'volume_type_extra_specs_delete', |
1335 | + delete_volume_type_extra_specs) |
1336 | + req = webob.Request.blank(self.api_path + '/key5') |
1337 | + req.method = 'DELETE' |
1338 | + res = req.get_response(fakes.wsgi_app()) |
1339 | + self.assertEqual(200, res.status_int) |
1340 | + |
1341 | + def test_create(self): |
1342 | + self.stubs.Set(nova.db.api, |
1343 | + 'volume_type_extra_specs_update_or_create', |
1344 | + return_create_volume_type_extra_specs) |
1345 | + req = webob.Request.blank(self.api_path) |
1346 | + req.method = 'POST' |
1347 | + req.body = '{"extra_specs": {"key1": "value1"}}' |
1348 | + req.headers["content-type"] = "application/json" |
1349 | + res = req.get_response(fakes.wsgi_app()) |
1350 | + res_dict = json.loads(res.body) |
1351 | + self.assertEqual(200, res.status_int) |
1352 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1353 | + self.assertEqual('value1', res_dict['extra_specs']['key1']) |
1354 | + |
1355 | + def test_create_empty_body(self): |
1356 | + self.stubs.Set(nova.db.api, |
1357 | + 'volume_type_extra_specs_update_or_create', |
1358 | + return_create_volume_type_extra_specs) |
1359 | + req = webob.Request.blank(self.api_path) |
1360 | + req.method = 'POST' |
1361 | + req.headers["content-type"] = "application/json" |
1362 | + res = req.get_response(fakes.wsgi_app()) |
1363 | + self.assertEqual(400, res.status_int) |
1364 | + |
1365 | + def test_update_item(self): |
1366 | + self.stubs.Set(nova.db.api, |
1367 | + 'volume_type_extra_specs_update_or_create', |
1368 | + return_create_volume_type_extra_specs) |
1369 | + req = webob.Request.blank(self.api_path + '/key1') |
1370 | + req.method = 'PUT' |
1371 | + req.body = '{"key1": "value1"}' |
1372 | + req.headers["content-type"] = "application/json" |
1373 | + res = req.get_response(fakes.wsgi_app()) |
1374 | + self.assertEqual(200, res.status_int) |
1375 | + self.assertEqual('application/json', res.headers['Content-Type']) |
1376 | + res_dict = json.loads(res.body) |
1377 | + self.assertEqual('value1', res_dict['key1']) |
1378 | + |
1379 | + def test_update_item_empty_body(self): |
1380 | + self.stubs.Set(nova.db.api, |
1381 | + 'volume_type_extra_specs_update_or_create', |
1382 | + return_create_volume_type_extra_specs) |
1383 | + req = webob.Request.blank(self.api_path + '/key1') |
1384 | + req.method = 'PUT' |
1385 | + req.headers["content-type"] = "application/json" |
1386 | + res = req.get_response(fakes.wsgi_app()) |
1387 | + self.assertEqual(400, res.status_int) |
1388 | + |
1389 | + def test_update_item_too_many_keys(self): |
1390 | + self.stubs.Set(nova.db.api, |
1391 | + 'volume_type_extra_specs_update_or_create', |
1392 | + return_create_volume_type_extra_specs) |
1393 | + req = webob.Request.blank(self.api_path + '/key1') |
1394 | + req.method = 'PUT' |
1395 | + req.body = '{"key1": "value1", "key2": "value2"}' |
1396 | + req.headers["content-type"] = "application/json" |
1397 | + res = req.get_response(fakes.wsgi_app()) |
1398 | + self.assertEqual(400, res.status_int) |
1399 | + |
1400 | + def test_update_item_body_uri_mismatch(self): |
1401 | + self.stubs.Set(nova.db.api, |
1402 | + 'volume_type_extra_specs_update_or_create', |
1403 | + return_create_volume_type_extra_specs) |
1404 | + req = webob.Request.blank(self.api_path + '/bad') |
1405 | + req.method = 'PUT' |
1406 | + req.body = '{"key1": "value1"}' |
1407 | + req.headers["content-type"] = "application/json" |
1408 | + res = req.get_response(fakes.wsgi_app()) |
1409 | + self.assertEqual(400, res.status_int) |
1410 | |
1411 | === modified file 'nova/tests/integrated/test_volumes.py' |
1412 | --- nova/tests/integrated/test_volumes.py 2011-08-03 21:06:56 +0000 |
1413 | +++ nova/tests/integrated/test_volumes.py 2011-08-24 23:44:25 +0000 |
1414 | @@ -285,6 +285,23 @@ |
1415 | self.assertEquals(undisco_move['mountpoint'], device) |
1416 | self.assertEquals(undisco_move['instance_id'], server_id) |
1417 | |
1418 | + def test_create_volume_with_metadata(self): |
1419 | + """Creates and deletes a volume.""" |
1420 | + |
1421 | + # Create volume |
1422 | + metadata = {'key1': 'value1', |
1423 | + 'key2': 'value2'} |
1424 | + created_volume = self.api.post_volume( |
1425 | + {'volume': {'size': 1, |
1426 | + 'metadata': metadata}}) |
1427 | + LOG.debug("created_volume: %s" % created_volume) |
1428 | + self.assertTrue(created_volume['id']) |
1429 | + created_volume_id = created_volume['id'] |
1430 | + |
1431 | + # Check it's there and metadata present |
1432 | + found_volume = self.api.get_volume(created_volume_id) |
1433 | + self.assertEqual(created_volume_id, found_volume['id']) |
1434 | + self.assertEqual(metadata, found_volume['metadata']) |
1435 | |
1436 | if __name__ == "__main__": |
1437 | unittest.main() |
1438 | |
1439 | === added file 'nova/tests/test_volume_types.py' |
1440 | --- nova/tests/test_volume_types.py 1970-01-01 00:00:00 +0000 |
1441 | +++ nova/tests/test_volume_types.py 2011-08-24 23:44:25 +0000 |
1442 | @@ -0,0 +1,207 @@ |
1443 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1444 | + |
1445 | +# Copyright (c) 2011 Zadara Storage Inc. |
1446 | +# Copyright (c) 2011 OpenStack LLC. |
1447 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1448 | +# not use this file except in compliance with the License. You may obtain |
1449 | +# a copy of the License at |
1450 | +# |
1451 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1452 | +# |
1453 | +# Unless required by applicable law or agreed to in writing, software |
1454 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1455 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1456 | +# License for the specific language governing permissions and limitations |
1457 | +# under the License. |
1458 | +""" |
1459 | +Unit Tests for volume types code |
1460 | +""" |
1461 | +import time |
1462 | + |
1463 | +from nova import context |
1464 | +from nova import db |
1465 | +from nova import exception |
1466 | +from nova import flags |
1467 | +from nova import log as logging |
1468 | +from nova import test |
1469 | +from nova import utils |
1470 | +from nova.volume import volume_types |
1471 | +from nova.db.sqlalchemy.session import get_session |
1472 | +from nova.db.sqlalchemy import models |
1473 | + |
1474 | +FLAGS = flags.FLAGS |
1475 | +LOG = logging.getLogger('nova.tests.test_volume_types') |
1476 | + |
1477 | + |
1478 | +class VolumeTypeTestCase(test.TestCase): |
1479 | + """Test cases for volume type code""" |
1480 | + def setUp(self): |
1481 | + super(VolumeTypeTestCase, self).setUp() |
1482 | + |
1483 | + self.ctxt = context.get_admin_context() |
1484 | + self.vol_type1_name = str(int(time.time())) |
1485 | + self.vol_type1_specs = dict( |
1486 | + type="physical drive", |
1487 | + drive_type="SAS", |
1488 | + size="300", |
1489 | + rpm="7200", |
1490 | + visible="True") |
1491 | + self.vol_type1 = dict(name=self.vol_type1_name, |
1492 | + extra_specs=self.vol_type1_specs) |
1493 | + |
1494 | + def test_volume_type_create_then_destroy(self): |
1495 | + """Ensure volume types can be created and deleted""" |
1496 | + prev_all_vtypes = volume_types.get_all_types(self.ctxt) |
1497 | + |
1498 | + volume_types.create(self.ctxt, |
1499 | + self.vol_type1_name, |
1500 | + self.vol_type1_specs) |
1501 | + new = volume_types.get_volume_type_by_name(self.ctxt, |
1502 | + self.vol_type1_name) |
1503 | + |
1504 | + LOG.info(_("Given data: %s"), self.vol_type1_specs) |
1505 | + LOG.info(_("Result data: %s"), new) |
1506 | + |
1507 | + for k, v in self.vol_type1_specs.iteritems(): |
1508 | + self.assertEqual(v, new['extra_specs'][k], |
1509 | + 'one of fields doesnt match') |
1510 | + |
1511 | + new_all_vtypes = volume_types.get_all_types(self.ctxt) |
1512 | + self.assertEqual(len(prev_all_vtypes) + 1, |
1513 | + len(new_all_vtypes), |
1514 | + 'drive type was not created') |
1515 | + |
1516 | + volume_types.destroy(self.ctxt, self.vol_type1_name) |
1517 | + new_all_vtypes = volume_types.get_all_types(self.ctxt) |
1518 | + self.assertEqual(prev_all_vtypes, |
1519 | + new_all_vtypes, |
1520 | + 'drive type was not deleted') |
1521 | + |
1522 | + def test_volume_type_create_then_purge(self): |
1523 | + """Ensure volume types can be created and deleted""" |
1524 | + prev_all_vtypes = volume_types.get_all_types(self.ctxt, inactive=1) |
1525 | + |
1526 | + volume_types.create(self.ctxt, |
1527 | + self.vol_type1_name, |
1528 | + self.vol_type1_specs) |
1529 | + new = volume_types.get_volume_type_by_name(self.ctxt, |
1530 | + self.vol_type1_name) |
1531 | + |
1532 | + for k, v in self.vol_type1_specs.iteritems(): |
1533 | + self.assertEqual(v, new['extra_specs'][k], |
1534 | + 'one of fields doesnt match') |
1535 | + |
1536 | + new_all_vtypes = volume_types.get_all_types(self.ctxt, inactive=1) |
1537 | + self.assertEqual(len(prev_all_vtypes) + 1, |
1538 | + len(new_all_vtypes), |
1539 | + 'drive type was not created') |
1540 | + |
1541 | + volume_types.destroy(self.ctxt, self.vol_type1_name) |
1542 | + new_all_vtypes2 = volume_types.get_all_types(self.ctxt, inactive=1) |
1543 | + self.assertEqual(len(new_all_vtypes), |
1544 | + len(new_all_vtypes2), |
1545 | + 'drive type was incorrectly deleted') |
1546 | + |
1547 | + volume_types.purge(self.ctxt, self.vol_type1_name) |
1548 | + new_all_vtypes2 = volume_types.get_all_types(self.ctxt, inactive=1) |
1549 | + self.assertEqual(len(new_all_vtypes) - 1, |
1550 | + len(new_all_vtypes2), |
1551 | + 'drive type was not purged') |
1552 | + |
1553 | + def test_get_all_volume_types(self): |
1554 | + """Ensures that all volume types can be retrieved""" |
1555 | + session = get_session() |
1556 | + total_volume_types = session.query(models.VolumeTypes).\ |
1557 | + count() |
1558 | + vol_types = volume_types.get_all_types(self.ctxt) |
1559 | + self.assertEqual(total_volume_types, len(vol_types)) |
1560 | + |
1561 | + def test_non_existant_inst_type_shouldnt_delete(self): |
1562 | + """Ensures that volume type creation fails with invalid args""" |
1563 | + self.assertRaises(exception.ApiError, |
1564 | + volume_types.destroy, self.ctxt, "sfsfsdfdfs") |
1565 | + |
1566 | + def test_repeated_vol_types_should_raise_api_error(self): |
1567 | + """Ensures that volume duplicates raises ApiError""" |
1568 | + new_name = self.vol_type1_name + "dup" |
1569 | + volume_types.create(self.ctxt, new_name) |
1570 | + volume_types.destroy(self.ctxt, new_name) |
1571 | + self.assertRaises( |
1572 | + exception.ApiError, |
1573 | + volume_types.create, self.ctxt, new_name) |
1574 | + |
1575 | + def test_invalid_volume_types_params(self): |
1576 | + """Ensures that volume type creation fails with invalid args""" |
1577 | + self.assertRaises(exception.InvalidVolumeType, |
1578 | + volume_types.destroy, self.ctxt, None) |
1579 | + self.assertRaises(exception.InvalidVolumeType, |
1580 | + volume_types.purge, self.ctxt, None) |
1581 | + self.assertRaises(exception.InvalidVolumeType, |
1582 | + volume_types.get_volume_type, self.ctxt, None) |
1583 | + self.assertRaises(exception.InvalidVolumeType, |
1584 | + volume_types.get_volume_type_by_name, |
1585 | + self.ctxt, None) |
1586 | + |
1587 | + def test_volume_type_get_by_id_and_name(self): |
1588 | + """Ensure volume types get returns same entry""" |
1589 | + volume_types.create(self.ctxt, |
1590 | + self.vol_type1_name, |
1591 | + self.vol_type1_specs) |
1592 | + new = volume_types.get_volume_type_by_name(self.ctxt, |
1593 | + self.vol_type1_name) |
1594 | + |
1595 | + new2 = volume_types.get_volume_type(self.ctxt, new['id']) |
1596 | + self.assertEqual(new, new2) |
1597 | + |
1598 | + def test_volume_type_search_by_extra_spec(self): |
1599 | + """Ensure volume types get by extra spec returns correct type""" |
1600 | + volume_types.create(self.ctxt, "type1", {"key1": "val1", |
1601 | + "key2": "val2"}) |
1602 | + volume_types.create(self.ctxt, "type2", {"key2": "val2", |
1603 | + "key3": "val3"}) |
1604 | + volume_types.create(self.ctxt, "type3", {"key3": "another_value", |
1605 | + "key4": "val4"}) |
1606 | + |
1607 | + vol_types = volume_types.get_all_types(self.ctxt, |
1608 | + search_opts={'extra_specs': {"key1": "val1"}}) |
1609 | + LOG.info("vol_types: %s" % vol_types) |
1610 | + self.assertEqual(len(vol_types), 1) |
1611 | + self.assertTrue("type1" in vol_types.keys()) |
1612 | + self.assertEqual(vol_types['type1']['extra_specs'], |
1613 | + {"key1": "val1", "key2": "val2"}) |
1614 | + |
1615 | + vol_types = volume_types.get_all_types(self.ctxt, |
1616 | + search_opts={'extra_specs': {"key2": "val2"}}) |
1617 | + LOG.info("vol_types: %s" % vol_types) |
1618 | + self.assertEqual(len(vol_types), 2) |
1619 | + self.assertTrue("type1" in vol_types.keys()) |
1620 | + self.assertTrue("type2" in vol_types.keys()) |
1621 | + |
1622 | + vol_types = volume_types.get_all_types(self.ctxt, |
1623 | + search_opts={'extra_specs': {"key3": "val3"}}) |
1624 | + LOG.info("vol_types: %s" % vol_types) |
1625 | + self.assertEqual(len(vol_types), 1) |
1626 | + self.assertTrue("type2" in vol_types.keys()) |
1627 | + |
1628 | + def test_volume_type_search_by_extra_spec_multiple(self): |
1629 | + """Ensure volume types get by extra spec returns correct type""" |
1630 | + volume_types.create(self.ctxt, "type1", {"key1": "val1", |
1631 | + "key2": "val2", |
1632 | + "key3": "val3"}) |
1633 | + volume_types.create(self.ctxt, "type2", {"key2": "val2", |
1634 | + "key3": "val3"}) |
1635 | + volume_types.create(self.ctxt, "type3", {"key1": "val1", |
1636 | + "key3": "val3", |
1637 | + "key4": "val4"}) |
1638 | + |
1639 | + vol_types = volume_types.get_all_types(self.ctxt, |
1640 | + search_opts={'extra_specs': {"key1": "val1", |
1641 | + "key3": "val3"}}) |
1642 | + LOG.info("vol_types: %s" % vol_types) |
1643 | + self.assertEqual(len(vol_types), 2) |
1644 | + self.assertTrue("type1" in vol_types.keys()) |
1645 | + self.assertTrue("type3" in vol_types.keys()) |
1646 | + self.assertEqual(vol_types['type1']['extra_specs'], |
1647 | + {"key1": "val1", "key2": "val2", "key3": "val3"}) |
1648 | + self.assertEqual(vol_types['type3']['extra_specs'], |
1649 | + {"key1": "val1", "key3": "val3", "key4": "val4"}) |
1650 | |
1651 | === added file 'nova/tests/test_volume_types_extra_specs.py' |
1652 | --- nova/tests/test_volume_types_extra_specs.py 1970-01-01 00:00:00 +0000 |
1653 | +++ nova/tests/test_volume_types_extra_specs.py 2011-08-24 23:44:25 +0000 |
1654 | @@ -0,0 +1,132 @@ |
1655 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1656 | + |
1657 | +# Copyright (c) 2011 Zadara Storage Inc. |
1658 | +# Copyright (c) 2011 OpenStack LLC. |
1659 | +# Copyright 2011 University of Southern California |
1660 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1661 | +# not use this file except in compliance with the License. You may obtain |
1662 | +# a copy of the License at |
1663 | +# |
1664 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1665 | +# |
1666 | +# Unless required by applicable law or agreed to in writing, software |
1667 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1668 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1669 | +# License for the specific language governing permissions and limitations |
1670 | +# under the License. |
1671 | +""" |
1672 | +Unit Tests for volume types extra specs code |
1673 | +""" |
1674 | + |
1675 | +from nova import context |
1676 | +from nova import db |
1677 | +from nova import test |
1678 | +from nova.db.sqlalchemy.session import get_session |
1679 | +from nova.db.sqlalchemy import models |
1680 | + |
1681 | + |
1682 | +class VolumeTypeExtraSpecsTestCase(test.TestCase): |
1683 | + |
1684 | + def setUp(self): |
1685 | + super(VolumeTypeExtraSpecsTestCase, self).setUp() |
1686 | + self.context = context.get_admin_context() |
1687 | + self.vol_type1 = dict(name="TEST: Regular volume test") |
1688 | + self.vol_type1_specs = dict(vol_extra1="value1", |
1689 | + vol_extra2="value2", |
1690 | + vol_extra3=3) |
1691 | + self.vol_type1['extra_specs'] = self.vol_type1_specs |
1692 | + ref = db.api.volume_type_create(self.context, self.vol_type1) |
1693 | + self.volume_type1_id = ref.id |
1694 | + for k, v in self.vol_type1_specs.iteritems(): |
1695 | + self.vol_type1_specs[k] = str(v) |
1696 | + |
1697 | + self.vol_type2_noextra = dict(name="TEST: Volume type without extra") |
1698 | + ref = db.api.volume_type_create(self.context, self.vol_type2_noextra) |
1699 | + self.vol_type2_id = ref.id |
1700 | + |
1701 | + def tearDown(self): |
1702 | + # Remove the instance type from the database |
1703 | + db.api.volume_type_purge(context.get_admin_context(), |
1704 | + self.vol_type1['name']) |
1705 | + db.api.volume_type_purge(context.get_admin_context(), |
1706 | + self.vol_type2_noextra['name']) |
1707 | + super(VolumeTypeExtraSpecsTestCase, self).tearDown() |
1708 | + |
1709 | + def test_volume_type_specs_get(self): |
1710 | + expected_specs = self.vol_type1_specs.copy() |
1711 | + actual_specs = db.api.volume_type_extra_specs_get( |
1712 | + context.get_admin_context(), |
1713 | + self.volume_type1_id) |
1714 | + self.assertEquals(expected_specs, actual_specs) |
1715 | + |
1716 | + def test_volume_type_extra_specs_delete(self): |
1717 | + expected_specs = self.vol_type1_specs.copy() |
1718 | + del expected_specs['vol_extra2'] |
1719 | + db.api.volume_type_extra_specs_delete(context.get_admin_context(), |
1720 | + self.volume_type1_id, |
1721 | + 'vol_extra2') |
1722 | + actual_specs = db.api.volume_type_extra_specs_get( |
1723 | + context.get_admin_context(), |
1724 | + self.volume_type1_id) |
1725 | + self.assertEquals(expected_specs, actual_specs) |
1726 | + |
1727 | + def test_volume_type_extra_specs_update(self): |
1728 | + expected_specs = self.vol_type1_specs.copy() |
1729 | + expected_specs['vol_extra3'] = "4" |
1730 | + db.api.volume_type_extra_specs_update_or_create( |
1731 | + context.get_admin_context(), |
1732 | + self.volume_type1_id, |
1733 | + dict(vol_extra3=4)) |
1734 | + actual_specs = db.api.volume_type_extra_specs_get( |
1735 | + context.get_admin_context(), |
1736 | + self.volume_type1_id) |
1737 | + self.assertEquals(expected_specs, actual_specs) |
1738 | + |
1739 | + def test_volume_type_extra_specs_create(self): |
1740 | + expected_specs = self.vol_type1_specs.copy() |
1741 | + expected_specs['vol_extra4'] = 'value4' |
1742 | + expected_specs['vol_extra5'] = 'value5' |
1743 | + db.api.volume_type_extra_specs_update_or_create( |
1744 | + context.get_admin_context(), |
1745 | + self.volume_type1_id, |
1746 | + dict(vol_extra4="value4", |
1747 | + vol_extra5="value5")) |
1748 | + actual_specs = db.api.volume_type_extra_specs_get( |
1749 | + context.get_admin_context(), |
1750 | + self.volume_type1_id) |
1751 | + self.assertEquals(expected_specs, actual_specs) |
1752 | + |
1753 | + def test_volume_type_get_with_extra_specs(self): |
1754 | + volume_type = db.api.volume_type_get( |
1755 | + context.get_admin_context(), |
1756 | + self.volume_type1_id) |
1757 | + self.assertEquals(volume_type['extra_specs'], |
1758 | + self.vol_type1_specs) |
1759 | + |
1760 | + volume_type = db.api.volume_type_get( |
1761 | + context.get_admin_context(), |
1762 | + self.vol_type2_id) |
1763 | + self.assertEquals(volume_type['extra_specs'], {}) |
1764 | + |
1765 | + def test_volume_type_get_by_name_with_extra_specs(self): |
1766 | + volume_type = db.api.volume_type_get_by_name( |
1767 | + context.get_admin_context(), |
1768 | + self.vol_type1['name']) |
1769 | + self.assertEquals(volume_type['extra_specs'], |
1770 | + self.vol_type1_specs) |
1771 | + |
1772 | + volume_type = db.api.volume_type_get_by_name( |
1773 | + context.get_admin_context(), |
1774 | + self.vol_type2_noextra['name']) |
1775 | + self.assertEquals(volume_type['extra_specs'], {}) |
1776 | + |
1777 | + def test_volume_type_get_all(self): |
1778 | + expected_specs = self.vol_type1_specs.copy() |
1779 | + |
1780 | + types = db.api.volume_type_get_all(context.get_admin_context()) |
1781 | + |
1782 | + self.assertEquals( |
1783 | + types[self.vol_type1['name']]['extra_specs'], expected_specs) |
1784 | + |
1785 | + self.assertEquals( |
1786 | + types[self.vol_type2_noextra['name']]['extra_specs'], {}) |
1787 | |
1788 | === modified file 'nova/volume/api.py' |
1789 | --- nova/volume/api.py 2011-07-25 14:07:32 +0000 |
1790 | +++ nova/volume/api.py 2011-08-24 23:44:25 +0000 |
1791 | @@ -41,7 +41,8 @@ |
1792 | class API(base.Base): |
1793 | """API for interacting with the volume manager.""" |
1794 | |
1795 | - def create(self, context, size, snapshot_id, name, description): |
1796 | + def create(self, context, size, snapshot_id, name, description, |
1797 | + volume_type=None, metadata=None, availability_zone=None): |
1798 | if snapshot_id != None: |
1799 | snapshot = self.get_snapshot(context, snapshot_id) |
1800 | if snapshot['status'] != "available": |
1801 | @@ -57,16 +58,27 @@ |
1802 | raise quota.QuotaError(_("Volume quota exceeded. You cannot " |
1803 | "create a volume of size %sG") % size) |
1804 | |
1805 | + if availability_zone is None: |
1806 | + availability_zone = FLAGS.storage_availability_zone |
1807 | + |
1808 | + if volume_type is None: |
1809 | + volume_type_id = None |
1810 | + else: |
1811 | + volume_type_id = volume_type.get('id', None) |
1812 | + |
1813 | options = { |
1814 | 'size': size, |
1815 | 'user_id': context.user_id, |
1816 | 'project_id': context.project_id, |
1817 | 'snapshot_id': snapshot_id, |
1818 | - 'availability_zone': FLAGS.storage_availability_zone, |
1819 | + 'availability_zone': availability_zone, |
1820 | 'status': "creating", |
1821 | 'attach_status': "detached", |
1822 | 'display_name': name, |
1823 | - 'display_description': description} |
1824 | + 'display_description': description, |
1825 | + 'volume_type_id': volume_type_id, |
1826 | + 'metadata': metadata, |
1827 | + } |
1828 | |
1829 | volume = self.db.volume_create(context, options) |
1830 | rpc.cast(context, |
1831 | @@ -105,10 +117,44 @@ |
1832 | rv = self.db.volume_get(context, volume_id) |
1833 | return dict(rv.iteritems()) |
1834 | |
1835 | - def get_all(self, context): |
1836 | + def get_all(self, context, search_opts={}): |
1837 | if context.is_admin: |
1838 | - return self.db.volume_get_all(context) |
1839 | - return self.db.volume_get_all_by_project(context, context.project_id) |
1840 | + volumes = self.db.volume_get_all(context) |
1841 | + else: |
1842 | + volumes = self.db.volume_get_all_by_project(context, |
1843 | + context.project_id) |
1844 | + |
1845 | + if search_opts: |
1846 | + LOG.debug(_("Searching by: %s") % str(search_opts)) |
1847 | + |
1848 | + def _check_metadata_match(volume, searchdict): |
1849 | + volume_metadata = {} |
1850 | + for i in volume.get('volume_metadata'): |
1851 | + volume_metadata[i['key']] = i['value'] |
1852 | + |
1853 | + for k, v in searchdict: |
1854 | + if k not in volume_metadata.keys()\ |
1855 | + or volume_metadata[k] != v: |
1856 | + return False |
1857 | + return True |
1858 | + |
1859 | + # search_option to filter_name mapping. |
1860 | + filter_mapping = {'metadata': _check_metadata_match} |
1861 | + |
1862 | + for volume in volumes: |
1863 | + # go over all filters in the list |
1864 | + for opt, values in search_opts.iteritems(): |
1865 | + try: |
1866 | + filter_func = filter_mapping[opt] |
1867 | + except KeyError: |
1868 | + # no such filter - ignore it, go to next filter |
1869 | + continue |
1870 | + else: |
1871 | + if filter_func(volume, values) == False: |
1872 | + # if one of conditions didn't match - remove |
1873 | + volumes.remove(volume) |
1874 | + break |
1875 | + return volumes |
1876 | |
1877 | def get_snapshot(self, context, snapshot_id): |
1878 | rv = self.db.snapshot_get(context, snapshot_id) |
1879 | @@ -183,3 +229,29 @@ |
1880 | {"method": "delete_snapshot", |
1881 | "args": {"topic": FLAGS.volume_topic, |
1882 | "snapshot_id": snapshot_id}}) |
1883 | + |
1884 | + def get_volume_metadata(self, context, volume_id): |
1885 | + """Get all metadata associated with a volume.""" |
1886 | + rv = self.db.volume_metadata_get(context, volume_id) |
1887 | + return dict(rv.iteritems()) |
1888 | + |
1889 | + def delete_volume_metadata(self, context, volume_id, key): |
1890 | + """Delete the given metadata item from an volume.""" |
1891 | + self.db.volume_metadata_delete(context, volume_id, key) |
1892 | + |
1893 | + def update_volume_metadata(self, context, volume_id, |
1894 | + metadata, delete=False): |
1895 | + """Updates or creates volume metadata. |
1896 | + |
1897 | + If delete is True, metadata items that are not specified in the |
1898 | + `metadata` argument will be deleted. |
1899 | + |
1900 | + """ |
1901 | + if delete: |
1902 | + _metadata = metadata |
1903 | + else: |
1904 | + _metadata = self.get_volume_metadata(context, volume_id) |
1905 | + _metadata.update(metadata) |
1906 | + |
1907 | + self.db.volume_metadata_update(context, volume_id, _metadata, True) |
1908 | + return _metadata |
1909 | |
1910 | === added file 'nova/volume/volume_types.py' |
1911 | --- nova/volume/volume_types.py 1970-01-01 00:00:00 +0000 |
1912 | +++ nova/volume/volume_types.py 2011-08-24 23:44:25 +0000 |
1913 | @@ -0,0 +1,129 @@ |
1914 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1915 | + |
1916 | +# Copyright (c) 2011 Zadara Storage Inc. |
1917 | +# Copyright (c) 2011 OpenStack LLC. |
1918 | +# Copyright 2010 United States Government as represented by the |
1919 | +# Administrator of the National Aeronautics and Space Administration. |
1920 | +# Copyright (c) 2010 Citrix Systems, Inc. |
1921 | +# Copyright 2011 Ken Pepple |
1922 | +# |
1923 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1924 | +# not use this file except in compliance with the License. You may obtain |
1925 | +# a copy of the License at |
1926 | +# |
1927 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1928 | +# |
1929 | +# Unless required by applicable law or agreed to in writing, software |
1930 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1931 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1932 | +# License for the specific language governing permissions and limitations |
1933 | +# under the License. |
1934 | + |
1935 | +"""Built-in volume type properties.""" |
1936 | + |
1937 | +from nova import context |
1938 | +from nova import db |
1939 | +from nova import exception |
1940 | +from nova import flags |
1941 | +from nova import log as logging |
1942 | + |
1943 | +FLAGS = flags.FLAGS |
1944 | +LOG = logging.getLogger('nova.volume.volume_types') |
1945 | + |
1946 | + |
1947 | +def create(context, name, extra_specs={}): |
1948 | + """Creates volume types.""" |
1949 | + try: |
1950 | + db.volume_type_create(context, |
1951 | + dict(name=name, |
1952 | + extra_specs=extra_specs)) |
1953 | + except exception.DBError, e: |
1954 | + LOG.exception(_('DB error: %s') % e) |
1955 | + raise exception.ApiError(_("Cannot create volume_type with " |
1956 | + "name %(name)s and specs %(extra_specs)s") |
1957 | + % locals()) |
1958 | + |
1959 | + |
1960 | +def destroy(context, name): |
1961 | + """Marks volume types as deleted.""" |
1962 | + if name is None: |
1963 | + raise exception.InvalidVolumeType(volume_type=name) |
1964 | + else: |
1965 | + try: |
1966 | + db.volume_type_destroy(context, name) |
1967 | + except exception.NotFound: |
1968 | + LOG.exception(_('Volume type %s not found for deletion') % name) |
1969 | + raise exception.ApiError(_("Unknown volume type: %s") % name) |
1970 | + |
1971 | + |
1972 | +def purge(context, name): |
1973 | + """Removes volume types from database.""" |
1974 | + if name is None: |
1975 | + raise exception.InvalidVolumeType(volume_type=name) |
1976 | + else: |
1977 | + try: |
1978 | + db.volume_type_purge(context, name) |
1979 | + except exception.NotFound: |
1980 | + LOG.exception(_('Volume type %s not found for purge') % name) |
1981 | + raise exception.ApiError(_("Unknown volume type: %s") % name) |
1982 | + |
1983 | + |
1984 | +def get_all_types(context, inactive=0, search_opts={}): |
1985 | + """Get all non-deleted volume_types. |
1986 | + |
1987 | + Pass true as argument if you want deleted volume types returned also. |
1988 | + |
1989 | + """ |
1990 | + vol_types = db.volume_type_get_all(context, inactive) |
1991 | + |
1992 | + if search_opts: |
1993 | + LOG.debug(_("Searching by: %s") % str(search_opts)) |
1994 | + |
1995 | + def _check_extra_specs_match(vol_type, searchdict): |
1996 | + for k, v in searchdict.iteritems(): |
1997 | + if k not in vol_type['extra_specs'].keys()\ |
1998 | + or vol_type['extra_specs'][k] != v: |
1999 | + return False |
2000 | + return True |
2001 | + |
2002 | + # search_option to filter_name mapping. |
2003 | + filter_mapping = {'extra_specs': _check_extra_specs_match} |
2004 | + |
2005 | + result = {} |
2006 | + for type_name, type_args in vol_types.iteritems(): |
2007 | + # go over all filters in the list |
2008 | + for opt, values in search_opts.iteritems(): |
2009 | + try: |
2010 | + filter_func = filter_mapping[opt] |
2011 | + except KeyError: |
2012 | + # no such filter - ignore it, go to next filter |
2013 | + continue |
2014 | + else: |
2015 | + if filter_func(type_args, values): |
2016 | + # if one of conditions didn't match - remove |
2017 | + result[type_name] = type_args |
2018 | + break |
2019 | + vol_types = result |
2020 | + return vol_types |
2021 | + |
2022 | + |
2023 | +def get_volume_type(context, id): |
2024 | + """Retrieves single volume type by id.""" |
2025 | + if id is None: |
2026 | + raise exception.InvalidVolumeType(volume_type=id) |
2027 | + |
2028 | + try: |
2029 | + return db.volume_type_get(context, id) |
2030 | + except exception.DBError: |
2031 | + raise exception.ApiError(_("Unknown volume type: %s") % id) |
2032 | + |
2033 | + |
2034 | +def get_volume_type_by_name(context, name): |
2035 | + """Retrieves single volume type by name.""" |
2036 | + if name is None: |
2037 | + raise exception.InvalidVolumeType(volume_type=name) |
2038 | + |
2039 | + try: |
2040 | + return db.volume_type_get_by_name(context, name) |
2041 | + except exception.DBError: |
2042 | + raise exception.ApiError(_("Unknown volume type: %s") % name) |
very nice. Tests look good. Unfortunately this needs to merge with milestone-proposed, so it could use a backport. That could be a little challenging because you merged with trunk along the way. Generally what i do is
bzr merge ../trunk
bzr commit
bzr diff old=../
bzr branch trunk -r 1479 new-branch # this is where we branched milestone
cd new-branch type_extradata | patch -p0
bzr diff old=../trunk ../volume_
bzr commit
bzr push --overwrite lp:~vladimir.p/nova/volume_type_extradata
That should have all your changes in one commit based of of 1479. That way we can apply it to both trunk and milestone cleanly.
Aside from that looks great.