~ubuntu-branches/ubuntu/quantal/nova/quantal-proposed

« back to all changes in this revision

Viewing changes to .pc/CVE-2013-1664.patch/nova/api/openstack/volume/volumes.py

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-03-22 12:40:07 UTC
  • Revision ID: package-import@ubuntu.com-20130322124007-yulmow8qdfbxsigv
Tags: 2012.2.3-0ubuntu2
* Re-sync with latest security updates.
* SECURITY UPDATE: fix denial of service via fixed IPs when using extensions
  - debian/patches/CVE-2013-1838.patch: add explicit quota for fixed IP
  - CVE-2013-1838
* SECURITY UPDATE: fix VNC token validation
  - debian/patches/CVE-2013-0335.patch: force console auth service to flush
    all tokens associated with an instance when it is deleted
  - CVE-2013-0335
* SECURITY UPDATE: fix denial of service
  - CVE-2013-1664.patch: Add a new utils.safe_minidom_parse_string function
    and update external API facing Nova modules to use it
  - CVE-2013-1664

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2011 Justin Santa Barbara
 
2
# All Rights Reserved.
 
3
#
 
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
 
7
#
 
8
#         http://www.apache.org/licenses/LICENSE-2.0
 
9
#
 
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
 
14
#    under the License.
 
15
 
 
16
"""The volumes api."""
 
17
 
 
18
import webob
 
19
from webob import exc
 
20
from xml.dom import minidom
 
21
 
 
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
 
31
 
 
32
 
 
33
LOG = logging.getLogger(__name__)
 
34
 
 
35
 
 
36
FLAGS = flags.FLAGS
 
37
 
 
38
 
 
39
def _translate_attachment_detail_view(_context, vol):
 
40
    """Maps keys for attachment details view."""
 
41
 
 
42
    d = _translate_attachment_summary_view(_context, vol)
 
43
 
 
44
    # No additional data / lookups at the moment
 
45
 
 
46
    return d
 
47
 
 
48
 
 
49
def _translate_attachment_summary_view(_context, vol):
 
50
    """Maps keys for attachment summary view."""
 
51
    d = {}
 
52
 
 
53
    volume_id = vol['id']
 
54
 
 
55
    # NOTE(justinsb): We use the volume id as the id of the attachment object
 
56
    d['id'] = volume_id
 
57
 
 
58
    d['volume_id'] = volume_id
 
59
    d['server_id'] = vol['instance_uuid']
 
60
    if vol.get('mountpoint'):
 
61
        d['device'] = vol['mountpoint']
 
62
 
 
63
    return d
 
64
 
 
65
 
 
66
def _translate_volume_detail_view(context, vol, image_id=None):
 
67
    """Maps keys for volumes details view."""
 
68
 
 
69
    d = _translate_volume_summary_view(context, vol, image_id)
 
70
 
 
71
    # No additional data / lookups at the moment
 
72
 
 
73
    return d
 
74
 
 
75
 
 
76
def _translate_volume_summary_view(context, vol, image_id=None):
 
77
    """Maps keys for volumes summary view."""
 
78
    d = {}
 
79
 
 
80
    d['id'] = vol['id']
 
81
    d['status'] = vol['status']
 
82
    d['size'] = vol['size']
 
83
    d['availability_zone'] = vol['availability_zone']
 
84
    d['created_at'] = vol['created_at']
 
85
 
 
86
    d['attachments'] = []
 
87
    if vol['attach_status'] == 'attached':
 
88
        attachment = _translate_attachment_detail_view(context, vol)
 
89
        d['attachments'].append(attachment)
 
90
 
 
91
    d['display_name'] = vol['display_name']
 
92
    d['display_description'] = vol['display_description']
 
93
 
 
94
    if vol['volume_type_id'] and vol.get('volume_type'):
 
95
        d['volume_type'] = vol['volume_type']['name']
 
96
    else:
 
97
        # TODO(bcwaldon): remove str cast once we use uuids
 
98
        d['volume_type'] = str(vol['volume_type_id'])
 
99
 
 
100
    d['snapshot_id'] = vol['snapshot_id']
 
101
 
 
102
    if image_id:
 
103
        d['image_id'] = image_id
 
104
 
 
105
    LOG.audit(_("vol=%s"), vol, context=context)
 
106
 
 
107
    if vol.get('volume_metadata'):
 
108
        metadata = vol.get('volume_metadata')
 
109
        d['metadata'] = dict((item['key'], item['value']) for item in metadata)
 
110
    else:
 
111
        d['metadata'] = {}
 
112
 
 
113
    return d
 
114
 
 
115
 
 
116
def make_attachment(elem):
 
117
    elem.set('id')
 
118
    elem.set('server_id')
 
119
    elem.set('volume_id')
 
120
    elem.set('device')
 
121
 
 
122
 
 
123
def make_volume(elem):
 
124
    elem.set('id')
 
125
    elem.set('status')
 
126
    elem.set('size')
 
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')
 
133
 
 
134
    attachments = xmlutil.SubTemplateElement(elem, 'attachments')
 
135
    attachment = xmlutil.SubTemplateElement(attachments, 'attachment',
 
136
                                            selector='attachments')
 
137
    make_attachment(attachment)
 
138
 
 
139
    # Attach metadata node
 
140
    elem.append(common.MetadataTemplate())
 
141
 
 
142
 
 
143
class VolumeTemplate(xmlutil.TemplateBuilder):
 
144
    def construct(self):
 
145
        root = xmlutil.TemplateElement('volume', selector='volume')
 
146
        make_volume(root)
 
147
        return xmlutil.MasterTemplate(root, 1)
 
148
 
 
149
 
 
150
class VolumesTemplate(xmlutil.TemplateBuilder):
 
151
    def construct(self):
 
152
        root = xmlutil.TemplateElement('volumes')
 
153
        elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes')
 
154
        make_volume(elem)
 
155
        return xmlutil.MasterTemplate(root, 1)
 
156
 
 
157
 
 
158
class CommonDeserializer(wsgi.MetadataXMLDeserializer):
 
159
    """Common deserializer to handle xml-formatted volume requests.
 
160
 
 
161
       Handles standard volume attributes as well as the optional metadata
 
162
       attribute
 
163
    """
 
164
 
 
165
    metadata_deserializer = common.MetadataXMLDeserializer()
 
166
 
 
167
    def _extract_volume(self, node):
 
168
        """Marshal the volume attribute of a parsed request."""
 
169
        volume = {}
 
170
        volume_node = self.find_first_child_named(node, 'volume')
 
171
 
 
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)
 
177
 
 
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)
 
181
 
 
182
        return volume
 
183
 
 
184
 
 
185
class CreateDeserializer(CommonDeserializer):
 
186
    """Deserializer to handle xml-formatted create volume requests.
 
187
 
 
188
       Handles standard volume attributes as well as the optional metadata
 
189
       attribute
 
190
    """
 
191
 
 
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}}
 
197
 
 
198
 
 
199
class VolumeController(wsgi.Controller):
 
200
    """The Volumes API controller for the OpenStack API."""
 
201
 
 
202
    def __init__(self, ext_mgr):
 
203
        self.volume_api = volume.API()
 
204
        self.ext_mgr = ext_mgr
 
205
        super(VolumeController, self).__init__()
 
206
 
 
207
    @wsgi.serializers(xml=VolumeTemplate)
 
208
    def show(self, req, id):
 
209
        """Return data about the given volume."""
 
210
        context = req.environ['nova.context']
 
211
 
 
212
        try:
 
213
            vol = self.volume_api.get(context, id)
 
214
        except exception.NotFound:
 
215
            raise exc.HTTPNotFound()
 
216
 
 
217
        return {'volume': _translate_volume_detail_view(context, vol)}
 
218
 
 
219
    def delete(self, req, id):
 
220
        """Delete a volume."""
 
221
        context = req.environ['nova.context']
 
222
 
 
223
        LOG.audit(_("Delete volume with id: %s"), id, context=context)
 
224
 
 
225
        try:
 
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)
 
231
 
 
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)
 
236
 
 
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)
 
241
 
 
242
    def _items(self, req, entity_maker):
 
243
        """Returns a list of volumes, transformed through entity_maker."""
 
244
 
 
245
        search_opts = {}
 
246
        search_opts.update(req.GET)
 
247
 
 
248
        context = req.environ['nova.context']
 
249
        remove_invalid_options(context,
 
250
                               search_opts, self._get_volume_search_options())
 
251
 
 
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}
 
256
 
 
257
    def _image_uuid_from_href(self, image_href):
 
258
        # If the image href was generated by nova api, strip image_href
 
259
        # down to an id.
 
260
        try:
 
261
            image_uuid = image_href.split('/').pop()
 
262
        except (TypeError, AttributeError):
 
263
            msg = _("Invalid imageRef provided.")
 
264
            raise exc.HTTPBadRequest(explanation=msg)
 
265
 
 
266
        if not utils.is_uuid_like(image_uuid):
 
267
            msg = _("Invalid imageRef provided.")
 
268
            raise exc.HTTPBadRequest(explanation=msg)
 
269
 
 
270
        return image_uuid
 
271
 
 
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()
 
278
 
 
279
        context = req.environ['nova.context']
 
280
        volume = body['volume']
 
281
 
 
282
        kwargs = {}
 
283
 
 
284
        req_volume_type = volume.get('volume_type', None)
 
285
        if req_volume_type:
 
286
            try:
 
287
                kwargs['volume_type'] = volume_types.get_volume_type_by_name(
 
288
                        context, req_volume_type)
 
289
            except exception.NotFound:
 
290
                raise exc.HTTPNotFound()
 
291
 
 
292
        kwargs['metadata'] = volume.get('metadata', None)
 
293
 
 
294
        snapshot_id = volume.get('snapshot_id')
 
295
        if snapshot_id is not None:
 
296
            kwargs['snapshot'] = self.volume_api.get_snapshot(context,
 
297
                                                              snapshot_id)
 
298
        else:
 
299
            kwargs['snapshot'] = None
 
300
 
 
301
        size = volume.get('size', None)
 
302
        if size is None and kwargs['snapshot'] is not None:
 
303
            size = kwargs['snapshot']['volume_size']
 
304
 
 
305
        LOG.audit(_("Create volume of %s GB"), size, context=context)
 
306
 
 
307
        image_href = None
 
308
        image_uuid = None
 
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)
 
314
            if image_href:
 
315
                image_uuid = self._image_uuid_from_href(image_href)
 
316
                kwargs['image_id'] = image_uuid
 
317
 
 
318
        kwargs['availability_zone'] = volume.get('availability_zone', None)
 
319
 
 
320
        new_volume = self.volume_api.create(context,
 
321
                                            size,
 
322
                                            volume.get('display_name'),
 
323
                                            volume.get('display_description'),
 
324
                                            **kwargs)
 
325
 
 
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),
 
330
                                               image_uuid)
 
331
 
 
332
        result = {'volume': retval}
 
333
 
 
334
        location = '%s/%s' % (req.url, new_volume['id'])
 
335
 
 
336
        return wsgi.ResponseObject(result, headers=dict(location=location))
 
337
 
 
338
    def _get_volume_search_options(self):
 
339
        """Return volume search options allowed by non-admin."""
 
340
        return ('name', 'status')
 
341
 
 
342
 
 
343
def create_resource(ext_mgr):
 
344
    return wsgi.Resource(VolumeController(ext_mgr))
 
345
 
 
346
 
 
347
def remove_invalid_options(context, search_options, allowed_search_options):
 
348
    """Remove search options that are not valid for non-admin API/context."""
 
349
    if context.is_admin:
 
350
        # Allow all options
 
351
        return
 
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()
 
357
    LOG.debug(log_msg)
 
358
    for opt in unknown_options:
 
359
        search_options.pop(opt, None)