1
# Copyright 2011 Justin Santa Barbara
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
16
"""The volumes api."""
20
from xml.dom import minidom
22
from nova.api.openstack import common
23
from nova.api.openstack import wsgi
24
from nova.api.openstack import xmlutil
25
from nova import exception
26
from nova import flags
27
from nova.openstack.common import log as logging
28
from nova import utils
29
from nova import volume
30
from nova.volume import volume_types
33
LOG = logging.getLogger(__name__)
39
def _translate_attachment_detail_view(_context, vol):
40
"""Maps keys for attachment details view."""
42
d = _translate_attachment_summary_view(_context, vol)
44
# No additional data / lookups at the moment
49
def _translate_attachment_summary_view(_context, vol):
50
"""Maps keys for attachment summary view."""
55
# NOTE(justinsb): We use the volume id as the id of the attachment object
58
d['volume_id'] = volume_id
59
d['server_id'] = vol['instance_uuid']
60
if vol.get('mountpoint'):
61
d['device'] = vol['mountpoint']
66
def _translate_volume_detail_view(context, vol, image_id=None):
67
"""Maps keys for volumes details view."""
69
d = _translate_volume_summary_view(context, vol, image_id)
71
# No additional data / lookups at the moment
76
def _translate_volume_summary_view(context, vol, image_id=None):
77
"""Maps keys for volumes summary view."""
81
d['status'] = vol['status']
82
d['size'] = vol['size']
83
d['availability_zone'] = vol['availability_zone']
84
d['created_at'] = vol['created_at']
87
if vol['attach_status'] == 'attached':
88
attachment = _translate_attachment_detail_view(context, vol)
89
d['attachments'].append(attachment)
91
d['display_name'] = vol['display_name']
92
d['display_description'] = vol['display_description']
94
if vol['volume_type_id'] and vol.get('volume_type'):
95
d['volume_type'] = vol['volume_type']['name']
97
# TODO(bcwaldon): remove str cast once we use uuids
98
d['volume_type'] = str(vol['volume_type_id'])
100
d['snapshot_id'] = vol['snapshot_id']
103
d['image_id'] = image_id
105
LOG.audit(_("vol=%s"), vol, context=context)
107
if vol.get('volume_metadata'):
108
metadata = vol.get('volume_metadata')
109
d['metadata'] = dict((item['key'], item['value']) for item in metadata)
116
def make_attachment(elem):
118
elem.set('server_id')
119
elem.set('volume_id')
123
def make_volume(elem):
127
elem.set('availability_zone')
128
elem.set('created_at')
129
elem.set('display_name')
130
elem.set('display_description')
131
elem.set('volume_type')
132
elem.set('snapshot_id')
134
attachments = xmlutil.SubTemplateElement(elem, 'attachments')
135
attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
136
selector='attachments')
137
make_attachment(attachment)
139
# Attach metadata node
140
elem.append(common.MetadataTemplate())
143
class VolumeTemplate(xmlutil.TemplateBuilder):
145
root = xmlutil.TemplateElement('volume', selector='volume')
147
return xmlutil.MasterTemplate(root, 1)
150
class VolumesTemplate(xmlutil.TemplateBuilder):
152
root = xmlutil.TemplateElement('volumes')
153
elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes')
155
return xmlutil.MasterTemplate(root, 1)
158
class CommonDeserializer(wsgi.MetadataXMLDeserializer):
159
"""Common deserializer to handle xml-formatted volume requests.
161
Handles standard volume attributes as well as the optional metadata
165
metadata_deserializer = common.MetadataXMLDeserializer()
167
def _extract_volume(self, node):
168
"""Marshal the volume attribute of a parsed request."""
170
volume_node = self.find_first_child_named(node, 'volume')
172
attributes = ['display_name', 'display_description', 'size',
173
'volume_type', 'availability_zone']
174
for attr in attributes:
175
if volume_node.getAttribute(attr):
176
volume[attr] = volume_node.getAttribute(attr)
178
metadata_node = self.find_first_child_named(volume_node, 'metadata')
179
if metadata_node is not None:
180
volume['metadata'] = self.extract_metadata(metadata_node)
185
class CreateDeserializer(CommonDeserializer):
186
"""Deserializer to handle xml-formatted create volume requests.
188
Handles standard volume attributes as well as the optional metadata
192
def default(self, string):
193
"""Deserialize an xml-formatted volume create request."""
194
dom = minidom.parseString(string)
195
volume = self._extract_volume(dom)
196
return {'body': {'volume': volume}}
199
class VolumeController(wsgi.Controller):
200
"""The Volumes API controller for the OpenStack API."""
202
def __init__(self, ext_mgr):
203
self.volume_api = volume.API()
204
self.ext_mgr = ext_mgr
205
super(VolumeController, self).__init__()
207
@wsgi.serializers(xml=VolumeTemplate)
208
def show(self, req, id):
209
"""Return data about the given volume."""
210
context = req.environ['nova.context']
213
vol = self.volume_api.get(context, id)
214
except exception.NotFound:
215
raise exc.HTTPNotFound()
217
return {'volume': _translate_volume_detail_view(context, vol)}
219
def delete(self, req, id):
220
"""Delete a volume."""
221
context = req.environ['nova.context']
223
LOG.audit(_("Delete volume with id: %s"), id, context=context)
226
volume = self.volume_api.get(context, id)
227
self.volume_api.delete(context, volume)
228
except exception.NotFound:
229
raise exc.HTTPNotFound()
230
return webob.Response(status_int=202)
232
@wsgi.serializers(xml=VolumesTemplate)
233
def index(self, req):
234
"""Returns a summary list of volumes."""
235
return self._items(req, entity_maker=_translate_volume_summary_view)
237
@wsgi.serializers(xml=VolumesTemplate)
238
def detail(self, req):
239
"""Returns a detailed list of volumes."""
240
return self._items(req, entity_maker=_translate_volume_detail_view)
242
def _items(self, req, entity_maker):
243
"""Returns a list of volumes, transformed through entity_maker."""
246
search_opts.update(req.GET)
248
context = req.environ['nova.context']
249
remove_invalid_options(context,
250
search_opts, self._get_volume_search_options())
252
volumes = self.volume_api.get_all(context, search_opts=search_opts)
253
limited_list = common.limited(volumes, req)
254
res = [entity_maker(context, vol) for vol in limited_list]
255
return {'volumes': res}
257
def _image_uuid_from_href(self, image_href):
258
# If the image href was generated by nova api, strip image_href
261
image_uuid = image_href.split('/').pop()
262
except (TypeError, AttributeError):
263
msg = _("Invalid imageRef provided.")
264
raise exc.HTTPBadRequest(explanation=msg)
266
if not utils.is_uuid_like(image_uuid):
267
msg = _("Invalid imageRef provided.")
268
raise exc.HTTPBadRequest(explanation=msg)
272
@wsgi.serializers(xml=VolumeTemplate)
273
@wsgi.deserializers(xml=CreateDeserializer)
274
def create(self, req, body):
275
"""Creates a new volume."""
276
if not self.is_valid_body(body, 'volume'):
277
raise exc.HTTPUnprocessableEntity()
279
context = req.environ['nova.context']
280
volume = body['volume']
284
req_volume_type = volume.get('volume_type', None)
287
kwargs['volume_type'] = volume_types.get_volume_type_by_name(
288
context, req_volume_type)
289
except exception.NotFound:
290
raise exc.HTTPNotFound()
292
kwargs['metadata'] = volume.get('metadata', None)
294
snapshot_id = volume.get('snapshot_id')
295
if snapshot_id is not None:
296
kwargs['snapshot'] = self.volume_api.get_snapshot(context,
299
kwargs['snapshot'] = None
301
size = volume.get('size', None)
302
if size is None and kwargs['snapshot'] is not None:
303
size = kwargs['snapshot']['volume_size']
305
LOG.audit(_("Create volume of %s GB"), size, context=context)
309
if self.ext_mgr.is_loaded('os-image-create'):
310
image_href = volume.get('imageRef')
311
if snapshot_id and image_href:
312
msg = _("Snapshot and image cannot be specified together.")
313
raise exc.HTTPBadRequest(explanation=msg)
315
image_uuid = self._image_uuid_from_href(image_href)
316
kwargs['image_id'] = image_uuid
318
kwargs['availability_zone'] = volume.get('availability_zone', None)
320
new_volume = self.volume_api.create(context,
322
volume.get('display_name'),
323
volume.get('display_description'),
326
# TODO(vish): Instance should be None at db layer instead of
327
# trying to lazy load, but for now we turn it into
328
# a dict to avoid an error.
329
retval = _translate_volume_detail_view(context, dict(new_volume),
332
result = {'volume': retval}
334
location = '%s/%s' % (req.url, new_volume['id'])
336
return wsgi.ResponseObject(result, headers=dict(location=location))
338
def _get_volume_search_options(self):
339
"""Return volume search options allowed by non-admin."""
340
return ('name', 'status')
343
def create_resource(ext_mgr):
344
return wsgi.Resource(VolumeController(ext_mgr))
347
def remove_invalid_options(context, search_options, allowed_search_options):
348
"""Remove search options that are not valid for non-admin API/context."""
352
# Otherwise, strip out all unknown options
353
unknown_options = [opt for opt in search_options
354
if opt not in allowed_search_options]
355
bad_options = ", ".join(unknown_options)
356
log_msg = _("Removing options '%(bad_options)s' from query") % locals()
358
for opt in unknown_options:
359
search_options.pop(opt, None)