~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/common.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
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2010 OpenStack LLC.
 
4
# All Rights Reserved.
 
5
#
 
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
7
#    not use this file except in compliance with the License. You may obtain
 
8
#    a copy of the License at
 
9
#
 
10
#         http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
12
#    Unless required by applicable law or agreed to in writing, software
 
13
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
15
#    License for the specific language governing permissions and limitations
 
16
#    under the License.
 
17
 
 
18
import functools
 
19
import os
 
20
import re
 
21
import urlparse
 
22
 
 
23
import webob
 
24
from xml.dom import minidom
 
25
 
 
26
from nova.api.openstack import wsgi
 
27
from nova.api.openstack import xmlutil
 
28
from nova.compute import task_states
 
29
from nova.compute import utils as compute_utils
 
30
from nova.compute import vm_states
 
31
from nova import exception
 
32
from nova import flags
 
33
from nova.openstack.common import log as logging
 
34
from nova import quota
 
35
 
 
36
 
 
37
LOG = logging.getLogger(__name__)
 
38
FLAGS = flags.FLAGS
 
39
QUOTAS = quota.QUOTAS
 
40
 
 
41
 
 
42
XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
 
43
 
 
44
 
 
45
_STATE_MAP = {
 
46
    vm_states.ACTIVE: {
 
47
        'default': 'ACTIVE',
 
48
        task_states.REBOOTING: 'REBOOT',
 
49
        task_states.REBOOTING_HARD: 'HARD_REBOOT',
 
50
        task_states.UPDATING_PASSWORD: 'PASSWORD',
 
51
        task_states.REBUILDING: 'REBUILD',
 
52
        task_states.REBUILD_BLOCK_DEVICE_MAPPING: 'REBUILD',
 
53
        task_states.REBUILD_SPAWNING: 'REBUILD',
 
54
        task_states.MIGRATING: 'MIGRATING',
 
55
        task_states.RESIZE_PREP: 'RESIZE',
 
56
        task_states.RESIZE_MIGRATING: 'RESIZE',
 
57
        task_states.RESIZE_MIGRATED: 'RESIZE',
 
58
        task_states.RESIZE_FINISH: 'RESIZE',
 
59
    },
 
60
    vm_states.BUILDING: {
 
61
        'default': 'BUILD',
 
62
    },
 
63
    vm_states.STOPPED: {
 
64
        'default': 'SHUTOFF',
 
65
    },
 
66
    vm_states.RESIZED: {
 
67
        'default': 'VERIFY_RESIZE',
 
68
        # Note(maoy): the OS API spec 1.1 doesn't have CONFIRMING_RESIZE
 
69
        # state so we comment that out for future reference only.
 
70
        #task_states.RESIZE_CONFIRMING: 'CONFIRMING_RESIZE',
 
71
        task_states.RESIZE_REVERTING: 'REVERT_RESIZE',
 
72
    },
 
73
    vm_states.PAUSED: {
 
74
        'default': 'PAUSED',
 
75
    },
 
76
    vm_states.SUSPENDED: {
 
77
        'default': 'SUSPENDED',
 
78
    },
 
79
    vm_states.RESCUED: {
 
80
        'default': 'RESCUE',
 
81
    },
 
82
    vm_states.ERROR: {
 
83
        'default': 'ERROR',
 
84
    },
 
85
    vm_states.DELETED: {
 
86
        'default': 'DELETED',
 
87
    },
 
88
    vm_states.SOFT_DELETED: {
 
89
        'default': 'DELETED',
 
90
    },
 
91
}
 
92
 
 
93
 
 
94
def status_from_state(vm_state, task_state='default'):
 
95
    """Given vm_state and task_state, return a status string."""
 
96
    task_map = _STATE_MAP.get(vm_state, dict(default='UNKNOWN'))
 
97
    status = task_map.get(task_state, task_map['default'])
 
98
    if status == "UNKNOWN":
 
99
        LOG.error(_("status is UNKNOWN from vm_state=%(vm_state)s "
 
100
                    "task_state=%(task_state)s. Bad upgrade or db "
 
101
                    "corrupted?") % locals())
 
102
    return status
 
103
 
 
104
 
 
105
def vm_state_from_status(status):
 
106
    """Map the server status string to a vm state."""
 
107
    for state, task_map in _STATE_MAP.iteritems():
 
108
        status_string = task_map.get("default")
 
109
        if status.lower() == status_string.lower():
 
110
            return state
 
111
 
 
112
 
 
113
def get_pagination_params(request):
 
114
    """Return marker, limit tuple from request.
 
115
 
 
116
    :param request: `wsgi.Request` possibly containing 'marker' and 'limit'
 
117
                    GET variables. 'marker' is the id of the last element
 
118
                    the client has seen, and 'limit' is the maximum number
 
119
                    of items to return. If 'limit' is not specified, 0, or
 
120
                    > max_limit, we default to max_limit. Negative values
 
121
                    for either marker or limit will cause
 
122
                    exc.HTTPBadRequest() exceptions to be raised.
 
123
 
 
124
    """
 
125
    params = {}
 
126
    if 'limit' in request.GET:
 
127
        params['limit'] = _get_limit_param(request)
 
128
    if 'marker' in request.GET:
 
129
        params['marker'] = _get_marker_param(request)
 
130
    return params
 
131
 
 
132
 
 
133
def _get_limit_param(request):
 
134
    """Extract integer limit from request or fail"""
 
135
    try:
 
136
        limit = int(request.GET['limit'])
 
137
    except ValueError:
 
138
        msg = _('limit param must be an integer')
 
139
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
140
    if limit < 0:
 
141
        msg = _('limit param must be positive')
 
142
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
143
    return limit
 
144
 
 
145
 
 
146
def _get_marker_param(request):
 
147
    """Extract marker id from request or fail"""
 
148
    return request.GET['marker']
 
149
 
 
150
 
 
151
def limited(items, request, max_limit=FLAGS.osapi_max_limit):
 
152
    """Return a slice of items according to requested offset and limit.
 
153
 
 
154
    :param items: A sliceable entity
 
155
    :param request: ``wsgi.Request`` possibly containing 'offset' and 'limit'
 
156
                    GET variables. 'offset' is where to start in the list,
 
157
                    and 'limit' is the maximum number of items to return. If
 
158
                    'limit' is not specified, 0, or > max_limit, we default
 
159
                    to max_limit. Negative values for either offset or limit
 
160
                    will cause exc.HTTPBadRequest() exceptions to be raised.
 
161
    :kwarg max_limit: The maximum number of items to return from 'items'
 
162
    """
 
163
    try:
 
164
        offset = int(request.GET.get('offset', 0))
 
165
    except ValueError:
 
166
        msg = _('offset param must be an integer')
 
167
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
168
 
 
169
    try:
 
170
        limit = int(request.GET.get('limit', max_limit))
 
171
    except ValueError:
 
172
        msg = _('limit param must be an integer')
 
173
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
174
 
 
175
    if limit < 0:
 
176
        msg = _('limit param must be positive')
 
177
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
178
 
 
179
    if offset < 0:
 
180
        msg = _('offset param must be positive')
 
181
        raise webob.exc.HTTPBadRequest(explanation=msg)
 
182
 
 
183
    limit = min(max_limit, limit or max_limit)
 
184
    range_end = offset + limit
 
185
    return items[offset:range_end]
 
186
 
 
187
 
 
188
def get_limit_and_marker(request, max_limit=FLAGS.osapi_max_limit):
 
189
    """get limited parameter from request"""
 
190
    params = get_pagination_params(request)
 
191
    limit = params.get('limit', max_limit)
 
192
    limit = min(max_limit, limit)
 
193
    marker = params.get('marker')
 
194
 
 
195
    return limit, marker
 
196
 
 
197
 
 
198
def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
 
199
    """Return a slice of items according to the requested marker and limit."""
 
200
    limit, marker = get_limit_and_marker(request, max_limit)
 
201
 
 
202
    limit = min(max_limit, limit)
 
203
    start_index = 0
 
204
    if marker:
 
205
        start_index = -1
 
206
        for i, item in enumerate(items):
 
207
            if 'flavorid' in item:
 
208
                if item['flavorid'] == marker:
 
209
                    start_index = i + 1
 
210
                    break
 
211
            elif item['id'] == marker or item.get('uuid') == marker:
 
212
                start_index = i + 1
 
213
                break
 
214
        if start_index < 0:
 
215
            msg = _('marker [%s] not found') % marker
 
216
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
217
    range_end = start_index + limit
 
218
    return items[start_index:range_end]
 
219
 
 
220
 
 
221
def get_id_from_href(href):
 
222
    """Return the id or uuid portion of a url.
 
223
 
 
224
    Given: 'http://www.foo.com/bar/123?q=4'
 
225
    Returns: '123'
 
226
 
 
227
    Given: 'http://www.foo.com/bar/abc123?q=4'
 
228
    Returns: 'abc123'
 
229
 
 
230
    """
 
231
    return urlparse.urlsplit("%s" % href).path.split('/')[-1]
 
232
 
 
233
 
 
234
def remove_version_from_href(href):
 
235
    """Removes the first api version from the href.
 
236
 
 
237
    Given: 'http://www.nova.com/v1.1/123'
 
238
    Returns: 'http://www.nova.com/123'
 
239
 
 
240
    Given: 'http://www.nova.com/v1.1'
 
241
    Returns: 'http://www.nova.com'
 
242
 
 
243
    """
 
244
    parsed_url = urlparse.urlsplit(href)
 
245
    url_parts = parsed_url.path.split('/', 2)
 
246
 
 
247
    # NOTE: this should match vX.X or vX
 
248
    expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
 
249
    if expression.match(url_parts[1]):
 
250
        del url_parts[1]
 
251
 
 
252
    new_path = '/'.join(url_parts)
 
253
 
 
254
    if new_path == parsed_url.path:
 
255
        msg = _('href %s does not contain version') % href
 
256
        LOG.debug(msg)
 
257
        raise ValueError(msg)
 
258
 
 
259
    parsed_url = list(parsed_url)
 
260
    parsed_url[2] = new_path
 
261
    return urlparse.urlunsplit(parsed_url)
 
262
 
 
263
 
 
264
def check_img_metadata_properties_quota(context, metadata):
 
265
    if metadata is None:
 
266
        return
 
267
    try:
 
268
        QUOTAS.limit_check(context, metadata_items=len(metadata))
 
269
    except exception.OverQuota:
 
270
        expl = _("Image metadata limit exceeded")
 
271
        raise webob.exc.HTTPRequestEntityTooLarge(explanation=expl,
 
272
                                                headers={'Retry-After': 0})
 
273
 
 
274
    #  check the key length.
 
275
    if isinstance(metadata, dict):
 
276
        for key, value in metadata.iteritems():
 
277
            if len(key) == 0:
 
278
                expl = _("Image metadata key cannot be blank")
 
279
                raise webob.exc.HTTPBadRequest(explanation=expl)
 
280
            if len(key) > 255:
 
281
                expl = _("Image metadata key too long")
 
282
                raise webob.exc.HTTPBadRequest(explanation=expl)
 
283
    else:
 
284
        expl = _("Invalid image metadata")
 
285
        raise webob.exc.HTTPBadRequest(explanation=expl)
 
286
 
 
287
 
 
288
def dict_to_query_str(params):
 
289
    # TODO(throughnothing): we should just use urllib.urlencode instead of this
 
290
    # But currently we don't work with urlencoded url's
 
291
    param_str = ""
 
292
    for key, val in params.iteritems():
 
293
        param_str = param_str + '='.join([str(key), str(val)]) + '&'
 
294
 
 
295
    return param_str.rstrip('&')
 
296
 
 
297
 
 
298
def get_networks_for_instance_from_nw_info(nw_info):
 
299
    networks = {}
 
300
    for vif in nw_info:
 
301
        ips = vif.fixed_ips()
 
302
        floaters = vif.floating_ips()
 
303
        label = vif['network']['label']
 
304
        if label not in networks:
 
305
            networks[label] = {'ips': [], 'floating_ips': []}
 
306
 
 
307
        networks[label]['ips'].extend(ips)
 
308
        networks[label]['floating_ips'].extend(floaters)
 
309
    return networks
 
310
 
 
311
 
 
312
def get_networks_for_instance(context, instance):
 
313
    """Returns a prepared nw_info list for passing into the view builders
 
314
 
 
315
    We end up with a data structure like::
 
316
 
 
317
        {'public': {'ips': [{'addr': '10.0.0.1', 'version': 4},
 
318
                            {'addr': '2001::1', 'version': 6}],
 
319
                    'floating_ips': [{'addr': '172.16.0.1', 'version': 4},
 
320
                                     {'addr': '172.16.2.1', 'version': 4}]},
 
321
         ...}
 
322
    """
 
323
    nw_info = compute_utils.get_nw_info_for_instance(instance)
 
324
    return get_networks_for_instance_from_nw_info(nw_info)
 
325
 
 
326
 
 
327
def raise_http_conflict_for_instance_invalid_state(exc, action):
 
328
    """Return a webob.exc.HTTPConflict instance containing a message
 
329
    appropriate to return via the API based on the original
 
330
    InstanceInvalidState exception.
 
331
    """
 
332
    attr = exc.kwargs.get('attr')
 
333
    state = exc.kwargs.get('state')
 
334
    if attr and state:
 
335
        msg = _("Cannot '%(action)s' while instance is in %(attr)s %(state)s")
 
336
    else:
 
337
        # At least give some meaningful message
 
338
        msg = _("Instance is in an invalid state for '%(action)s'")
 
339
    raise webob.exc.HTTPConflict(explanation=msg % locals())
 
340
 
 
341
 
 
342
class MetadataDeserializer(wsgi.MetadataXMLDeserializer):
 
343
    def deserialize(self, text):
 
344
        dom = minidom.parseString(text)
 
345
        metadata_node = self.find_first_child_named(dom, "metadata")
 
346
        metadata = self.extract_metadata(metadata_node)
 
347
        return {'body': {'metadata': metadata}}
 
348
 
 
349
 
 
350
class MetaItemDeserializer(wsgi.MetadataXMLDeserializer):
 
351
    def deserialize(self, text):
 
352
        dom = minidom.parseString(text)
 
353
        metadata_item = self.extract_metadata(dom)
 
354
        return {'body': {'meta': metadata_item}}
 
355
 
 
356
 
 
357
class MetadataXMLDeserializer(wsgi.XMLDeserializer):
 
358
 
 
359
    def extract_metadata(self, metadata_node):
 
360
        """Marshal the metadata attribute of a parsed request"""
 
361
        if metadata_node is None:
 
362
            return {}
 
363
        metadata = {}
 
364
        for meta_node in self.find_children_named(metadata_node, "meta"):
 
365
            key = meta_node.getAttribute("key")
 
366
            metadata[key] = self.extract_text(meta_node)
 
367
        return metadata
 
368
 
 
369
    def _extract_metadata_container(self, datastring):
 
370
        dom = minidom.parseString(datastring)
 
371
        metadata_node = self.find_first_child_named(dom, "metadata")
 
372
        metadata = self.extract_metadata(metadata_node)
 
373
        return {'body': {'metadata': metadata}}
 
374
 
 
375
    def create(self, datastring):
 
376
        return self._extract_metadata_container(datastring)
 
377
 
 
378
    def update_all(self, datastring):
 
379
        return self._extract_metadata_container(datastring)
 
380
 
 
381
    def update(self, datastring):
 
382
        dom = minidom.parseString(datastring)
 
383
        metadata_item = self.extract_metadata(dom)
 
384
        return {'body': {'meta': metadata_item}}
 
385
 
 
386
 
 
387
metadata_nsmap = {None: xmlutil.XMLNS_V11}
 
388
 
 
389
 
 
390
class MetaItemTemplate(xmlutil.TemplateBuilder):
 
391
    def construct(self):
 
392
        sel = xmlutil.Selector('meta', xmlutil.get_items, 0)
 
393
        root = xmlutil.TemplateElement('meta', selector=sel)
 
394
        root.set('key', 0)
 
395
        root.text = 1
 
396
        return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap)
 
397
 
 
398
 
 
399
class MetadataTemplateElement(xmlutil.TemplateElement):
 
400
    def will_render(self, datum):
 
401
        return True
 
402
 
 
403
 
 
404
class MetadataTemplate(xmlutil.TemplateBuilder):
 
405
    def construct(self):
 
406
        root = MetadataTemplateElement('metadata', selector='metadata')
 
407
        elem = xmlutil.SubTemplateElement(root, 'meta',
 
408
                                          selector=xmlutil.get_items)
 
409
        elem.set('key', 0)
 
410
        elem.text = 1
 
411
        return xmlutil.MasterTemplate(root, 1, nsmap=metadata_nsmap)
 
412
 
 
413
 
 
414
def check_snapshots_enabled(f):
 
415
    @functools.wraps(f)
 
416
    def inner(*args, **kwargs):
 
417
        if not FLAGS.allow_instance_snapshots:
 
418
            LOG.warn(_('Rejecting snapshot request, snapshots currently'
 
419
                       ' disabled'))
 
420
            msg = _("Instance snapshots are not permitted at this time.")
 
421
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
422
        return f(*args, **kwargs)
 
423
    return inner
 
424
 
 
425
 
 
426
class ViewBuilder(object):
 
427
    """Model API responses as dictionaries."""
 
428
 
 
429
    def _get_links(self, request, identifier, collection_name):
 
430
        return [{
 
431
            "rel": "self",
 
432
            "href": self._get_href_link(request, identifier, collection_name),
 
433
        },
 
434
        {
 
435
            "rel": "bookmark",
 
436
            "href": self._get_bookmark_link(request,
 
437
                                            identifier,
 
438
                                            collection_name),
 
439
        }]
 
440
 
 
441
    def _get_next_link(self, request, identifier, collection_name):
 
442
        """Return href string with proper limit and marker params."""
 
443
        params = request.params.copy()
 
444
        params["marker"] = identifier
 
445
        prefix = self._update_link_prefix(request.application_url,
 
446
                                          FLAGS.osapi_compute_link_prefix)
 
447
        url = os.path.join(prefix,
 
448
                           request.environ["nova.context"].project_id,
 
449
                           collection_name)
 
450
        return "%s?%s" % (url, dict_to_query_str(params))
 
451
 
 
452
    def _get_href_link(self, request, identifier, collection_name):
 
453
        """Return an href string pointing to this object."""
 
454
        prefix = self._update_link_prefix(request.application_url,
 
455
                                          FLAGS.osapi_compute_link_prefix)
 
456
        return os.path.join(prefix,
 
457
                            request.environ["nova.context"].project_id,
 
458
                            collection_name,
 
459
                            str(identifier))
 
460
 
 
461
    def _get_bookmark_link(self, request, identifier, collection_name):
 
462
        """Create a URL that refers to a specific resource."""
 
463
        base_url = remove_version_from_href(request.application_url)
 
464
        base_url = self._update_link_prefix(base_url,
 
465
                                            FLAGS.osapi_compute_link_prefix)
 
466
        return os.path.join(base_url,
 
467
                            request.environ["nova.context"].project_id,
 
468
                            collection_name,
 
469
                            str(identifier))
 
470
 
 
471
    def _get_collection_links(self,
 
472
                              request,
 
473
                              items,
 
474
                              collection_name,
 
475
                              id_key="uuid"):
 
476
        """Retrieve 'next' link, if applicable."""
 
477
        links = []
 
478
        limit = int(request.params.get("limit", 0))
 
479
        if limit and limit == len(items):
 
480
            last_item = items[-1]
 
481
            if id_key in last_item:
 
482
                last_item_id = last_item[id_key]
 
483
            elif 'id' in last_item:
 
484
                last_item_id = last_item["id"]
 
485
            else:
 
486
                last_item_id = last_item["flavorid"]
 
487
            links.append({
 
488
                "rel": "next",
 
489
                "href": self._get_next_link(request,
 
490
                                            last_item_id,
 
491
                                            collection_name),
 
492
            })
 
493
        return links
 
494
 
 
495
    def _update_link_prefix(self, orig_url, prefix):
 
496
        if not prefix:
 
497
            return orig_url
 
498
        url_parts = list(urlparse.urlsplit(orig_url))
 
499
        prefix_parts = list(urlparse.urlsplit(prefix))
 
500
        url_parts[0:2] = prefix_parts[0:2]
 
501
        return urlparse.urlunsplit(url_parts)