Merge lp:~vladimir.p/nova/volume_type_extradata into lp:~hudson-openstack/nova/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
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

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 :

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
bzr diff old=../trunk ../volume_type_extradata | patch -p0
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.

review: Needs Fixing
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

Revision history for this message
Dan Prince (dan-prince) wrote :

Looks good.

review: Approve
Revision history for this message
Ben McGraw (mcgrue) wrote :

lgtm

review: Approve

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)