~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/compute/servers.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 2010 OpenStack LLC.
 
2
# Copyright 2011 Piston Cloud Computing, Inc
 
3
# All Rights Reserved.
 
4
#
 
5
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
6
#    not use this file except in compliance with the License. You may obtain
 
7
#    a copy of the License at
 
8
#
 
9
#         http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
#    Unless required by applicable law or agreed to in writing, software
 
12
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
13
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
14
#    License for the specific language governing permissions and limitations
 
15
#    under the License.
 
16
 
 
17
import base64
 
18
import os
 
19
import re
 
20
import socket
 
21
 
 
22
import webob
 
23
from webob import exc
 
24
from xml.dom import minidom
 
25
 
 
26
from nova.api.openstack import common
 
27
from nova.api.openstack.compute import ips
 
28
from nova.api.openstack.compute.views import servers as views_servers
 
29
from nova.api.openstack import wsgi
 
30
from nova.api.openstack import xmlutil
 
31
from nova import compute
 
32
from nova.compute import instance_types
 
33
from nova import exception
 
34
from nova import flags
 
35
from nova.openstack.common import importutils
 
36
from nova.openstack.common import log as logging
 
37
from nova.openstack.common.rpc import common as rpc_common
 
38
from nova.openstack.common import timeutils
 
39
from nova import utils
 
40
 
 
41
 
 
42
LOG = logging.getLogger(__name__)
 
43
FLAGS = flags.FLAGS
 
44
 
 
45
 
 
46
def make_fault(elem):
 
47
    fault = xmlutil.SubTemplateElement(elem, 'fault', selector='fault')
 
48
    fault.set('code')
 
49
    fault.set('created')
 
50
    msg = xmlutil.SubTemplateElement(fault, 'message')
 
51
    msg.text = 'message'
 
52
    det = xmlutil.SubTemplateElement(fault, 'details')
 
53
    det.text = 'details'
 
54
 
 
55
 
 
56
def make_server(elem, detailed=False):
 
57
    elem.set('name')
 
58
    elem.set('id')
 
59
 
 
60
    if detailed:
 
61
        elem.set('userId', 'user_id')
 
62
        elem.set('tenantId', 'tenant_id')
 
63
        elem.set('updated')
 
64
        elem.set('created')
 
65
        elem.set('hostId')
 
66
        elem.set('accessIPv4')
 
67
        elem.set('accessIPv6')
 
68
        elem.set('status')
 
69
        elem.set('progress')
 
70
        elem.set('reservation_id')
 
71
 
 
72
        # Attach image node
 
73
        image = xmlutil.SubTemplateElement(elem, 'image', selector='image')
 
74
        image.set('id')
 
75
        xmlutil.make_links(image, 'links')
 
76
 
 
77
        # Attach flavor node
 
78
        flavor = xmlutil.SubTemplateElement(elem, 'flavor', selector='flavor')
 
79
        flavor.set('id')
 
80
        xmlutil.make_links(flavor, 'links')
 
81
 
 
82
        # Attach fault node
 
83
        make_fault(elem)
 
84
 
 
85
        # Attach metadata node
 
86
        elem.append(common.MetadataTemplate())
 
87
 
 
88
        # Attach addresses node
 
89
        elem.append(ips.AddressesTemplate())
 
90
 
 
91
    xmlutil.make_links(elem, 'links')
 
92
 
 
93
 
 
94
server_nsmap = {None: xmlutil.XMLNS_V11, 'atom': xmlutil.XMLNS_ATOM}
 
95
 
 
96
 
 
97
class ServerTemplate(xmlutil.TemplateBuilder):
 
98
    def construct(self):
 
99
        root = xmlutil.TemplateElement('server', selector='server')
 
100
        make_server(root, detailed=True)
 
101
        return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
 
102
 
 
103
 
 
104
class MinimalServersTemplate(xmlutil.TemplateBuilder):
 
105
    def construct(self):
 
106
        root = xmlutil.TemplateElement('servers')
 
107
        elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
 
108
        make_server(elem)
 
109
        xmlutil.make_links(root, 'servers_links')
 
110
        return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
 
111
 
 
112
 
 
113
class ServersTemplate(xmlutil.TemplateBuilder):
 
114
    def construct(self):
 
115
        root = xmlutil.TemplateElement('servers')
 
116
        elem = xmlutil.SubTemplateElement(root, 'server', selector='servers')
 
117
        make_server(elem, detailed=True)
 
118
        return xmlutil.MasterTemplate(root, 1, nsmap=server_nsmap)
 
119
 
 
120
 
 
121
class ServerAdminPassTemplate(xmlutil.TemplateBuilder):
 
122
    def construct(self):
 
123
        root = xmlutil.TemplateElement('server')
 
124
        root.set('adminPass')
 
125
        return xmlutil.SlaveTemplate(root, 1, nsmap=server_nsmap)
 
126
 
 
127
 
 
128
def FullServerTemplate():
 
129
    master = ServerTemplate()
 
130
    master.attach(ServerAdminPassTemplate())
 
131
    return master
 
132
 
 
133
 
 
134
class CommonDeserializer(wsgi.MetadataXMLDeserializer):
 
135
    """Common deserializer to handle xml-formatted server create requests.
 
136
 
 
137
    Handles standard server attributes as well as optional metadata
 
138
    and personality attributes
 
139
    """
 
140
 
 
141
    metadata_deserializer = common.MetadataXMLDeserializer()
 
142
 
 
143
    def _extract_personality(self, server_node):
 
144
        """Marshal the personality attribute of a parsed request."""
 
145
        node = self.find_first_child_named(server_node, "personality")
 
146
        if node is not None:
 
147
            personality = []
 
148
            for file_node in self.find_children_named(node, "file"):
 
149
                item = {}
 
150
                if file_node.hasAttribute("path"):
 
151
                    item["path"] = file_node.getAttribute("path")
 
152
                item["contents"] = self.extract_text(file_node)
 
153
                personality.append(item)
 
154
            return personality
 
155
        else:
 
156
            return None
 
157
 
 
158
    def _extract_server(self, node):
 
159
        """Marshal the server attribute of a parsed request."""
 
160
        server = {}
 
161
        server_node = self.find_first_child_named(node, 'server')
 
162
 
 
163
        attributes = ["name", "imageRef", "flavorRef", "adminPass",
 
164
                      "accessIPv4", "accessIPv6", "key_name",
 
165
                      "availability_zone", "min_count", "max_count"]
 
166
        for attr in attributes:
 
167
            if server_node.getAttribute(attr):
 
168
                server[attr] = server_node.getAttribute(attr)
 
169
 
 
170
        res_id = server_node.getAttribute('return_reservation_id')
 
171
        if res_id:
 
172
            server['return_reservation_id'] = utils.bool_from_str(res_id)
 
173
 
 
174
        scheduler_hints = self._extract_scheduler_hints(server_node)
 
175
        if scheduler_hints:
 
176
            server['OS-SCH-HNT:scheduler_hints'] = scheduler_hints
 
177
 
 
178
        metadata_node = self.find_first_child_named(server_node, "metadata")
 
179
        if metadata_node is not None:
 
180
            server["metadata"] = self.extract_metadata(metadata_node)
 
181
 
 
182
        user_data_node = self.find_first_child_named(server_node, "user_data")
 
183
        if user_data_node is not None:
 
184
            server["user_data"] = self.extract_text(user_data_node)
 
185
 
 
186
        personality = self._extract_personality(server_node)
 
187
        if personality is not None:
 
188
            server["personality"] = personality
 
189
 
 
190
        networks = self._extract_networks(server_node)
 
191
        if networks is not None:
 
192
            server["networks"] = networks
 
193
 
 
194
        security_groups = self._extract_security_groups(server_node)
 
195
        if security_groups is not None:
 
196
            server["security_groups"] = security_groups
 
197
 
 
198
        # NOTE(vish): this is not namespaced in json, so leave it without a
 
199
        #             namespace for now
 
200
        block_device_mapping = self._extract_block_device_mapping(server_node)
 
201
        if block_device_mapping is not None:
 
202
            server["block_device_mapping"] = block_device_mapping
 
203
 
 
204
        # NOTE(vish): Support this incorrect version because it was in the code
 
205
        #             base for a while and we don't want to accidentally break
 
206
        #             anyone that might be using it.
 
207
        auto_disk_config = server_node.getAttribute('auto_disk_config')
 
208
        if auto_disk_config:
 
209
            server['OS-DCF:diskConfig'] = utils.bool_from_str(auto_disk_config)
 
210
 
 
211
        auto_disk_config = server_node.getAttribute('OS-DCF:diskConfig')
 
212
        if auto_disk_config:
 
213
            server['OS-DCF:diskConfig'] = utils.bool_from_str(auto_disk_config)
 
214
 
 
215
        return server
 
216
 
 
217
    def _extract_block_device_mapping(self, server_node):
 
218
        """Marshal the block_device_mapping node of a parsed request"""
 
219
        node = self.find_first_child_named(server_node, "block_device_mapping")
 
220
        if node:
 
221
            block_device_mapping = []
 
222
            for child in self.extract_elements(node):
 
223
                if child.nodeName != "mapping":
 
224
                    continue
 
225
                mapping = {}
 
226
                attributes = ["volume_id", "snapshot_id", "device_name",
 
227
                              "virtual_name", "volume_size"]
 
228
                for attr in attributes:
 
229
                    value = child.getAttribute(attr)
 
230
                    if value:
 
231
                        mapping[attr] = value
 
232
                attributes = ["delete_on_termination", "no_device"]
 
233
                for attr in attributes:
 
234
                    value = child.getAttribute(attr)
 
235
                    if value:
 
236
                        mapping[attr] = utils.bool_from_str(value)
 
237
                block_device_mapping.append(mapping)
 
238
            return block_device_mapping
 
239
        else:
 
240
            return None
 
241
 
 
242
    def _extract_scheduler_hints(self, server_node):
 
243
        """Marshal the scheduler hints attribute of a parsed request"""
 
244
        node = self.find_first_child_named_in_namespace(server_node,
 
245
            "http://docs.openstack.org/compute/ext/scheduler-hints/api/v2",
 
246
            "scheduler_hints")
 
247
        if node:
 
248
            scheduler_hints = {}
 
249
            for child in self.extract_elements(node):
 
250
                scheduler_hints.setdefault(child.nodeName, [])
 
251
                value = self.extract_text(child).strip()
 
252
                scheduler_hints[child.nodeName].append(value)
 
253
            return scheduler_hints
 
254
        else:
 
255
            return None
 
256
 
 
257
    def _extract_networks(self, server_node):
 
258
        """Marshal the networks attribute of a parsed request."""
 
259
        node = self.find_first_child_named(server_node, "networks")
 
260
        if node is not None:
 
261
            networks = []
 
262
            for network_node in self.find_children_named(node,
 
263
                                                         "network"):
 
264
                item = {}
 
265
                if network_node.hasAttribute("uuid"):
 
266
                    item["uuid"] = network_node.getAttribute("uuid")
 
267
                if network_node.hasAttribute("fixed_ip"):
 
268
                    item["fixed_ip"] = network_node.getAttribute("fixed_ip")
 
269
                if network_node.hasAttribute("port"):
 
270
                    item["port"] = network_node.getAttribute("port")
 
271
                networks.append(item)
 
272
            return networks
 
273
        else:
 
274
            return None
 
275
 
 
276
    def _extract_security_groups(self, server_node):
 
277
        """Marshal the security_groups attribute of a parsed request."""
 
278
        node = self.find_first_child_named(server_node, "security_groups")
 
279
        if node is not None:
 
280
            security_groups = []
 
281
            for sg_node in self.find_children_named(node, "security_group"):
 
282
                item = {}
 
283
                name = self.find_attribute_or_element(sg_node, 'name')
 
284
                if name:
 
285
                    item["name"] = name
 
286
                    security_groups.append(item)
 
287
            return security_groups
 
288
        else:
 
289
            return None
 
290
 
 
291
 
 
292
class ActionDeserializer(CommonDeserializer):
 
293
    """Deserializer to handle xml-formatted server action requests.
 
294
 
 
295
    Handles standard server attributes as well as optional metadata
 
296
    and personality attributes
 
297
    """
 
298
 
 
299
    def default(self, string):
 
300
        dom = minidom.parseString(string)
 
301
        action_node = dom.childNodes[0]
 
302
        action_name = action_node.tagName
 
303
 
 
304
        action_deserializer = {
 
305
            'createImage': self._action_create_image,
 
306
            'changePassword': self._action_change_password,
 
307
            'reboot': self._action_reboot,
 
308
            'rebuild': self._action_rebuild,
 
309
            'resize': self._action_resize,
 
310
            'confirmResize': self._action_confirm_resize,
 
311
            'revertResize': self._action_revert_resize,
 
312
        }.get(action_name, super(ActionDeserializer, self).default)
 
313
 
 
314
        action_data = action_deserializer(action_node)
 
315
 
 
316
        return {'body': {action_name: action_data}}
 
317
 
 
318
    def _action_create_image(self, node):
 
319
        return self._deserialize_image_action(node, ('name',))
 
320
 
 
321
    def _action_change_password(self, node):
 
322
        if not node.hasAttribute("adminPass"):
 
323
            raise AttributeError("No adminPass was specified in request")
 
324
        return {"adminPass": node.getAttribute("adminPass")}
 
325
 
 
326
    def _action_reboot(self, node):
 
327
        if not node.hasAttribute("type"):
 
328
            raise AttributeError("No reboot type was specified in request")
 
329
        return {"type": node.getAttribute("type")}
 
330
 
 
331
    def _action_rebuild(self, node):
 
332
        rebuild = {}
 
333
        if node.hasAttribute("name"):
 
334
            name = node.getAttribute("name")
 
335
            if not name:
 
336
                raise AttributeError("Name cannot be blank")
 
337
            rebuild['name'] = name
 
338
 
 
339
        if node.hasAttribute("auto_disk_config"):
 
340
            rebuild['auto_disk_config'] = node.getAttribute("auto_disk_config")
 
341
 
 
342
        metadata_node = self.find_first_child_named(node, "metadata")
 
343
        if metadata_node is not None:
 
344
            rebuild["metadata"] = self.extract_metadata(metadata_node)
 
345
 
 
346
        personality = self._extract_personality(node)
 
347
        if personality is not None:
 
348
            rebuild["personality"] = personality
 
349
 
 
350
        if not node.hasAttribute("imageRef"):
 
351
            raise AttributeError("No imageRef was specified in request")
 
352
        rebuild["imageRef"] = node.getAttribute("imageRef")
 
353
 
 
354
        if node.hasAttribute("adminPass"):
 
355
            rebuild["adminPass"] = node.getAttribute("adminPass")
 
356
 
 
357
        if node.hasAttribute("accessIPv4"):
 
358
            rebuild["accessIPv4"] = node.getAttribute("accessIPv4")
 
359
 
 
360
        if node.hasAttribute("accessIPv6"):
 
361
            rebuild["accessIPv6"] = node.getAttribute("accessIPv6")
 
362
 
 
363
        return rebuild
 
364
 
 
365
    def _action_resize(self, node):
 
366
        resize = {}
 
367
 
 
368
        if node.hasAttribute("flavorRef"):
 
369
            resize["flavorRef"] = node.getAttribute("flavorRef")
 
370
        else:
 
371
            raise AttributeError("No flavorRef was specified in request")
 
372
 
 
373
        if node.hasAttribute("auto_disk_config"):
 
374
            resize['auto_disk_config'] = node.getAttribute("auto_disk_config")
 
375
 
 
376
        return resize
 
377
 
 
378
    def _action_confirm_resize(self, node):
 
379
        return None
 
380
 
 
381
    def _action_revert_resize(self, node):
 
382
        return None
 
383
 
 
384
    def _deserialize_image_action(self, node, allowed_attributes):
 
385
        data = {}
 
386
        for attribute in allowed_attributes:
 
387
            value = node.getAttribute(attribute)
 
388
            if value:
 
389
                data[attribute] = value
 
390
        metadata_node = self.find_first_child_named(node, 'metadata')
 
391
        if metadata_node is not None:
 
392
            metadata = self.metadata_deserializer.extract_metadata(
 
393
                                                        metadata_node)
 
394
            data['metadata'] = metadata
 
395
        return data
 
396
 
 
397
 
 
398
class CreateDeserializer(CommonDeserializer):
 
399
    """Deserializer to handle xml-formatted server create requests.
 
400
 
 
401
    Handles standard server attributes as well as optional metadata
 
402
    and personality attributes
 
403
    """
 
404
 
 
405
    def default(self, string):
 
406
        """Deserialize an xml-formatted server create request."""
 
407
        dom = minidom.parseString(string)
 
408
        server = self._extract_server(dom)
 
409
        return {'body': {'server': server}}
 
410
 
 
411
 
 
412
class Controller(wsgi.Controller):
 
413
    """The Server API base controller class for the OpenStack API."""
 
414
 
 
415
    _view_builder_class = views_servers.ViewBuilder
 
416
 
 
417
    @staticmethod
 
418
    def _add_location(robj):
 
419
        # Just in case...
 
420
        if 'server' not in robj.obj:
 
421
            return robj
 
422
 
 
423
        link = filter(lambda l: l['rel'] == 'self',
 
424
                      robj.obj['server']['links'])
 
425
        if link:
 
426
            robj['Location'] = link[0]['href'].encode('utf-8')
 
427
 
 
428
        # Convenience return
 
429
        return robj
 
430
 
 
431
    def __init__(self, ext_mgr=None, **kwargs):
 
432
        super(Controller, self).__init__(**kwargs)
 
433
        self.compute_api = compute.API()
 
434
        self.ext_mgr = ext_mgr
 
435
        self.quantum_attempted = False
 
436
 
 
437
    @wsgi.serializers(xml=MinimalServersTemplate)
 
438
    def index(self, req):
 
439
        """Returns a list of server names and ids for a given user."""
 
440
        try:
 
441
            servers = self._get_servers(req, is_detail=False)
 
442
        except exception.Invalid as err:
 
443
            raise exc.HTTPBadRequest(explanation=str(err))
 
444
        except exception.NotFound:
 
445
            raise exc.HTTPNotFound()
 
446
        return servers
 
447
 
 
448
    @wsgi.serializers(xml=ServersTemplate)
 
449
    def detail(self, req):
 
450
        """Returns a list of server details for a given user."""
 
451
        try:
 
452
            servers = self._get_servers(req, is_detail=True)
 
453
        except exception.Invalid as err:
 
454
            raise exc.HTTPBadRequest(explanation=str(err))
 
455
        except exception.NotFound as err:
 
456
            raise exc.HTTPNotFound()
 
457
        return servers
 
458
 
 
459
    def _add_instance_faults(self, ctxt, instances):
 
460
        faults = self.compute_api.get_instance_faults(ctxt, instances)
 
461
        if faults is not None:
 
462
            for instance in instances:
 
463
                faults_list = faults.get(instance['uuid'], [])
 
464
                try:
 
465
                    instance['fault'] = faults_list[0]
 
466
                except IndexError:
 
467
                    pass
 
468
 
 
469
        return instances
 
470
 
 
471
    def _get_servers(self, req, is_detail):
 
472
        """Returns a list of servers, based on any search options specified."""
 
473
 
 
474
        search_opts = {}
 
475
        search_opts.update(req.GET)
 
476
 
 
477
        context = req.environ['nova.context']
 
478
        remove_invalid_options(context, search_opts,
 
479
                self._get_server_search_options())
 
480
 
 
481
        # Verify search by 'status' contains a valid status.
 
482
        # Convert it to filter by vm_state for compute_api.
 
483
        status = search_opts.pop('status', None)
 
484
        if status is not None:
 
485
            state = common.vm_state_from_status(status)
 
486
            if state is None:
 
487
                msg = _('Invalid server status: %(status)s') % locals()
 
488
                raise exc.HTTPBadRequest(explanation=msg)
 
489
            search_opts['vm_state'] = state
 
490
 
 
491
        if 'changes-since' in search_opts:
 
492
            try:
 
493
                parsed = timeutils.parse_isotime(search_opts['changes-since'])
 
494
            except ValueError:
 
495
                msg = _('Invalid changes-since value')
 
496
                raise exc.HTTPBadRequest(explanation=msg)
 
497
            search_opts['changes-since'] = parsed
 
498
 
 
499
        # By default, compute's get_all() will return deleted instances.
 
500
        # If an admin hasn't specified a 'deleted' search option, we need
 
501
        # to filter out deleted instances by setting the filter ourselves.
 
502
        # ... Unless 'changes-since' is specified, because 'changes-since'
 
503
        # should return recently deleted images according to the API spec.
 
504
 
 
505
        if 'deleted' not in search_opts:
 
506
            if 'changes-since' not in search_opts:
 
507
                # No 'changes-since', so we only want non-deleted servers
 
508
                search_opts['deleted'] = False
 
509
 
 
510
        if search_opts.get("vm_state") == "deleted":
 
511
            if context.is_admin:
 
512
                search_opts['deleted'] = True
 
513
            else:
 
514
                msg = _("Only administrators may list deleted instances")
 
515
                raise exc.HTTPBadRequest(explanation=msg)
 
516
 
 
517
        # NOTE(dprince) This prevents computes' get_all() from returning
 
518
        # instances from multiple tenants when an admin accounts is used.
 
519
        # By default non-admin accounts are always limited to project/user
 
520
        # both here and in the compute API.
 
521
        if not context.is_admin or (context.is_admin and 'all_tenants'
 
522
            not in search_opts):
 
523
            if context.project_id:
 
524
                search_opts['project_id'] = context.project_id
 
525
            else:
 
526
                search_opts['user_id'] = context.user_id
 
527
 
 
528
        limit, marker = common.get_limit_and_marker(req)
 
529
        try:
 
530
            instance_list = self.compute_api.get_all(context,
 
531
                                                     search_opts=search_opts,
 
532
                                                     limit=limit,
 
533
                                                     marker=marker)
 
534
        except exception.MarkerNotFound as e:
 
535
            msg = _('marker [%s] not found') % marker
 
536
            raise webob.exc.HTTPBadRequest(explanation=msg)
 
537
 
 
538
        if is_detail:
 
539
            self._add_instance_faults(context, instance_list)
 
540
            response = self._view_builder.detail(req, instance_list)
 
541
        else:
 
542
            response = self._view_builder.index(req, instance_list)
 
543
        req.cache_db_instances(instance_list)
 
544
        return response
 
545
 
 
546
    def _get_server(self, context, req, instance_uuid):
 
547
        """Utility function for looking up an instance by uuid."""
 
548
        try:
 
549
            instance = self.compute_api.get(context, instance_uuid)
 
550
        except exception.NotFound:
 
551
            raise exc.HTTPNotFound()
 
552
        req.cache_db_instance(instance)
 
553
        return instance
 
554
 
 
555
    def _validate_server_name(self, value):
 
556
        if not isinstance(value, basestring):
 
557
            msg = _("Server name is not a string or unicode")
 
558
            raise exc.HTTPBadRequest(explanation=msg)
 
559
 
 
560
        if not value.strip():
 
561
            msg = _("Server name is an empty string")
 
562
            raise exc.HTTPBadRequest(explanation=msg)
 
563
 
 
564
        if not len(value) < 256:
 
565
            msg = _("Server name must be less than 256 characters.")
 
566
            raise exc.HTTPBadRequest(explanation=msg)
 
567
 
 
568
    def _get_injected_files(self, personality):
 
569
        """Create a list of injected files from the personality attribute.
 
570
 
 
571
        At this time, injected_files must be formatted as a list of
 
572
        (file_path, file_content) pairs for compatibility with the
 
573
        underlying compute service.
 
574
        """
 
575
        injected_files = []
 
576
 
 
577
        for item in personality:
 
578
            try:
 
579
                path = item['path']
 
580
                contents = item['contents']
 
581
            except KeyError as key:
 
582
                expl = _('Bad personality format: missing %s') % key
 
583
                raise exc.HTTPBadRequest(explanation=expl)
 
584
            except TypeError:
 
585
                expl = _('Bad personality format')
 
586
                raise exc.HTTPBadRequest(explanation=expl)
 
587
            contents = self._decode_base64(contents)
 
588
            if contents is None:
 
589
                expl = _('Personality content for %s cannot be decoded') % path
 
590
                raise exc.HTTPBadRequest(explanation=expl)
 
591
            injected_files.append((path, contents))
 
592
        return injected_files
 
593
 
 
594
    def _is_quantum_v2(self):
 
595
        # NOTE(dprince): quantumclient is not a requirement
 
596
        if self.quantum_attempted:
 
597
            return self.have_quantum
 
598
 
 
599
        try:
 
600
            self.quantum_attempted = True
 
601
            from nova.network.quantumv2 import api as quantum_api
 
602
            self.have_quantum = issubclass(
 
603
                importutils.import_class(FLAGS.network_api_class),
 
604
                quantum_api.API)
 
605
        except ImportError:
 
606
            self.have_quantum = False
 
607
 
 
608
        return self.have_quantum
 
609
 
 
610
    def _get_requested_networks(self, requested_networks):
 
611
        """Create a list of requested networks from the networks attribute."""
 
612
        networks = []
 
613
        for network in requested_networks:
 
614
            try:
 
615
                port_id = network.get('port', None)
 
616
                if port_id:
 
617
                    network_uuid = None
 
618
                    if not self._is_quantum_v2():
 
619
                        # port parameter is only for qunatum v2.0
 
620
                        msg = _("Unknown argment : port")
 
621
                        raise exc.HTTPBadRequest(explanation=msg)
 
622
                    if not utils.is_uuid_like(port_id):
 
623
                        msg = _("Bad port format: port uuid is "
 
624
                                "not in proper format "
 
625
                                "(%s)") % port_id
 
626
                        raise exc.HTTPBadRequest(explanation=msg)
 
627
                else:
 
628
                    network_uuid = network['uuid']
 
629
 
 
630
                if not port_id and not utils.is_uuid_like(network_uuid):
 
631
                    br_uuid = network_uuid.split('-', 1)[-1]
 
632
                    if not utils.is_uuid_like(br_uuid):
 
633
                        msg = _("Bad networks format: network uuid is "
 
634
                                "not in proper format "
 
635
                                "(%s)") % network_uuid
 
636
                        raise exc.HTTPBadRequest(explanation=msg)
 
637
 
 
638
                #fixed IP address is optional
 
639
                #if the fixed IP address is not provided then
 
640
                #it will use one of the available IP address from the network
 
641
                address = network.get('fixed_ip', None)
 
642
                if address is not None and not utils.is_valid_ipv4(address):
 
643
                    msg = _("Invalid fixed IP address (%s)") % address
 
644
                    raise exc.HTTPBadRequest(explanation=msg)
 
645
 
 
646
                # For quantumv2, requestd_networks
 
647
                # should be tuple of (network_uuid, fixed_ip, port_id)
 
648
                if self._is_quantum_v2():
 
649
                    networks.append((network_uuid, address, port_id))
 
650
                else:
 
651
                    # check if the network id is already present in the list,
 
652
                    # we don't want duplicate networks to be passed
 
653
                    # at the boot time
 
654
                    for id, ip in networks:
 
655
                        if id == network_uuid:
 
656
                            expl = (_("Duplicate networks"
 
657
                                      " (%s) are not allowed") %
 
658
                                    network_uuid)
 
659
                            raise exc.HTTPBadRequest(explanation=expl)
 
660
                    networks.append((network_uuid, address))
 
661
            except KeyError as key:
 
662
                expl = _('Bad network format: missing %s') % key
 
663
                raise exc.HTTPBadRequest(explanation=expl)
 
664
            except TypeError:
 
665
                expl = _('Bad networks format')
 
666
                raise exc.HTTPBadRequest(explanation=expl)
 
667
 
 
668
        return networks
 
669
 
 
670
    # NOTE(vish): Without this regex, b64decode will happily
 
671
    #             ignore illegal bytes in the base64 encoded
 
672
    #             data.
 
673
    B64_REGEX = re.compile('^(?:[A-Za-z0-9+\/]{4})*'
 
674
                           '(?:[A-Za-z0-9+\/]{2}=='
 
675
                           '|[A-Za-z0-9+\/]{3}=)?$')
 
676
 
 
677
    def _decode_base64(self, data):
 
678
        data = re.sub(r'\s', '', data)
 
679
        if not self.B64_REGEX.match(data):
 
680
            return None
 
681
        try:
 
682
            return base64.b64decode(data)
 
683
        except TypeError:
 
684
            return None
 
685
 
 
686
    def _validate_user_data(self, user_data):
 
687
        """Check if the user_data is encoded properly."""
 
688
        if not user_data:
 
689
            return
 
690
        if self._decode_base64(user_data) is None:
 
691
            expl = _('Userdata content cannot be decoded')
 
692
            raise exc.HTTPBadRequest(explanation=expl)
 
693
 
 
694
    def _validate_access_ipv4(self, address):
 
695
        try:
 
696
            socket.inet_aton(address)
 
697
        except socket.error:
 
698
            expl = _('accessIPv4 is not proper IPv4 format')
 
699
            raise exc.HTTPBadRequest(explanation=expl)
 
700
 
 
701
    def _validate_access_ipv6(self, address):
 
702
        try:
 
703
            socket.inet_pton(socket.AF_INET6, address)
 
704
        except socket.error:
 
705
            expl = _('accessIPv6 is not proper IPv6 format')
 
706
            raise exc.HTTPBadRequest(explanation=expl)
 
707
 
 
708
    @wsgi.serializers(xml=ServerTemplate)
 
709
    def show(self, req, id):
 
710
        """Returns server details by server id."""
 
711
        try:
 
712
            context = req.environ['nova.context']
 
713
            instance = self.compute_api.get(context, id)
 
714
            req.cache_db_instance(instance)
 
715
            self._add_instance_faults(context, [instance])
 
716
            return self._view_builder.show(req, instance)
 
717
        except exception.NotFound:
 
718
            raise exc.HTTPNotFound()
 
719
 
 
720
    @wsgi.response(202)
 
721
    @wsgi.serializers(xml=FullServerTemplate)
 
722
    @wsgi.deserializers(xml=CreateDeserializer)
 
723
    def create(self, req, body):
 
724
        """Creates a new server for a given user."""
 
725
        if not self.is_valid_body(body, 'server'):
 
726
            raise exc.HTTPUnprocessableEntity()
 
727
 
 
728
        context = req.environ['nova.context']
 
729
        server_dict = body['server']
 
730
        password = self._get_server_admin_password(server_dict)
 
731
 
 
732
        if not 'name' in server_dict:
 
733
            msg = _("Server name is not defined")
 
734
            raise exc.HTTPBadRequest(explanation=msg)
 
735
 
 
736
        name = server_dict['name']
 
737
        self._validate_server_name(name)
 
738
        name = name.strip()
 
739
 
 
740
        image_href = self._image_ref_from_req_data(body)
 
741
        image_href = self._image_uuid_from_href(image_href)
 
742
 
 
743
        personality = server_dict.get('personality')
 
744
        config_drive = None
 
745
        if self.ext_mgr.is_loaded('os-config-drive'):
 
746
            config_drive = server_dict.get('config_drive')
 
747
 
 
748
        injected_files = []
 
749
        if personality:
 
750
            injected_files = self._get_injected_files(personality)
 
751
 
 
752
        sg_names = []
 
753
        if self.ext_mgr.is_loaded('os-security-groups'):
 
754
            security_groups = server_dict.get('security_groups')
 
755
            if security_groups is not None:
 
756
                sg_names = [sg['name'] for sg in security_groups
 
757
                            if sg.get('name')]
 
758
        if not sg_names:
 
759
            sg_names.append('default')
 
760
 
 
761
        sg_names = list(set(sg_names))
 
762
 
 
763
        requested_networks = None
 
764
        if self.ext_mgr.is_loaded('os-networks'):
 
765
            requested_networks = server_dict.get('networks')
 
766
 
 
767
        if requested_networks is not None:
 
768
            requested_networks = self._get_requested_networks(
 
769
                requested_networks)
 
770
 
 
771
        (access_ip_v4, ) = server_dict.get('accessIPv4'),
 
772
        if access_ip_v4 is not None:
 
773
            self._validate_access_ipv4(access_ip_v4)
 
774
 
 
775
        (access_ip_v6, ) = server_dict.get('accessIPv6'),
 
776
        if access_ip_v6 is not None:
 
777
            self._validate_access_ipv6(access_ip_v6)
 
778
 
 
779
        try:
 
780
            flavor_id = self._flavor_id_from_req_data(body)
 
781
        except ValueError as error:
 
782
            msg = _("Invalid flavorRef provided.")
 
783
            raise exc.HTTPBadRequest(explanation=msg)
 
784
 
 
785
        # optional openstack extensions:
 
786
        key_name = None
 
787
        if self.ext_mgr.is_loaded('os-keypairs'):
 
788
            key_name = server_dict.get('key_name')
 
789
 
 
790
        user_data = None
 
791
        if self.ext_mgr.is_loaded('os-user-data'):
 
792
            user_data = server_dict.get('user_data')
 
793
        self._validate_user_data(user_data)
 
794
 
 
795
        availability_zone = None
 
796
        if self.ext_mgr.is_loaded('os-availability-zone'):
 
797
            availability_zone = server_dict.get('availability_zone')
 
798
 
 
799
        block_device_mapping = None
 
800
        if self.ext_mgr.is_loaded('os-volumes'):
 
801
            block_device_mapping = server_dict.get('block_device_mapping', [])
 
802
            for bdm in block_device_mapping:
 
803
                if 'delete_on_termination' in bdm:
 
804
                    bdm['delete_on_termination'] = utils.bool_from_str(
 
805
                        bdm['delete_on_termination'])
 
806
 
 
807
        ret_resv_id = False
 
808
        # min_count and max_count are optional.  If they exist, they may come
 
809
        # in as strings.  Verify that they are valid integers and > 0.
 
810
        # Also, we want to default 'min_count' to 1, and default
 
811
        # 'max_count' to be 'min_count'.
 
812
        min_count = 1
 
813
        max_count = 1
 
814
        if self.ext_mgr.is_loaded('os-multiple-create'):
 
815
            ret_resv_id = server_dict.get('return_reservation_id', False)
 
816
            min_count = server_dict.get('min_count', 1)
 
817
            max_count = server_dict.get('max_count', min_count)
 
818
 
 
819
        try:
 
820
            min_count = int(min_count)
 
821
        except ValueError:
 
822
            raise webob.exc.HTTPBadRequest(_('min_count must be an '
 
823
                                             'integer value'))
 
824
        if min_count < 1:
 
825
            raise webob.exc.HTTPBadRequest(_('min_count must be > 0'))
 
826
 
 
827
        try:
 
828
            max_count = int(max_count)
 
829
        except ValueError:
 
830
            raise webob.exc.HTTPBadRequest(_('max_count must be an '
 
831
                                             'integer value'))
 
832
        if max_count < 1:
 
833
            raise webob.exc.HTTPBadRequest(_('max_count must be > 0'))
 
834
 
 
835
        if min_count > max_count:
 
836
            raise webob.exc.HTTPBadRequest(_('min_count must be <= max_count'))
 
837
 
 
838
        auto_disk_config = False
 
839
        if self.ext_mgr.is_loaded('OS-DCF'):
 
840
            auto_disk_config = server_dict.get('auto_disk_config')
 
841
 
 
842
        scheduler_hints = {}
 
843
        if self.ext_mgr.is_loaded('OS-SCH-HNT'):
 
844
            scheduler_hints = server_dict.get('scheduler_hints', {})
 
845
 
 
846
        try:
 
847
            _get_inst_type = instance_types.get_instance_type_by_flavor_id
 
848
            inst_type = _get_inst_type(flavor_id, read_deleted="no")
 
849
 
 
850
            (instances, resv_id) = self.compute_api.create(context,
 
851
                            inst_type,
 
852
                            image_href,
 
853
                            display_name=name,
 
854
                            display_description=name,
 
855
                            key_name=key_name,
 
856
                            metadata=server_dict.get('metadata', {}),
 
857
                            access_ip_v4=access_ip_v4,
 
858
                            access_ip_v6=access_ip_v6,
 
859
                            injected_files=injected_files,
 
860
                            admin_password=password,
 
861
                            min_count=min_count,
 
862
                            max_count=max_count,
 
863
                            requested_networks=requested_networks,
 
864
                            security_group=sg_names,
 
865
                            user_data=user_data,
 
866
                            availability_zone=availability_zone,
 
867
                            config_drive=config_drive,
 
868
                            block_device_mapping=block_device_mapping,
 
869
                            auto_disk_config=auto_disk_config,
 
870
                            scheduler_hints=scheduler_hints)
 
871
        except exception.QuotaError as error:
 
872
            raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error),
 
873
                                                headers={'Retry-After': 0})
 
874
        except exception.InstanceTypeMemoryTooSmall as error:
 
875
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
876
        except exception.InstanceTypeNotFound as error:
 
877
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
878
        except exception.InstanceTypeDiskTooSmall as error:
 
879
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
880
        except exception.InvalidMetadata as error:
 
881
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
882
        except exception.InvalidMetadataSize as error:
 
883
            raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error))
 
884
        except exception.ImageNotFound as error:
 
885
            msg = _("Can not find requested image")
 
886
            raise exc.HTTPBadRequest(explanation=msg)
 
887
        except exception.FlavorNotFound as error:
 
888
            msg = _("Invalid flavorRef provided.")
 
889
            raise exc.HTTPBadRequest(explanation=msg)
 
890
        except exception.KeypairNotFound as error:
 
891
            msg = _("Invalid key_name provided.")
 
892
            raise exc.HTTPBadRequest(explanation=msg)
 
893
        except exception.SecurityGroupNotFound as error:
 
894
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
895
        except rpc_common.RemoteError as err:
 
896
            msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
 
897
                                                 'err_msg': err.value}
 
898
            raise exc.HTTPBadRequest(explanation=msg)
 
899
        except UnicodeDecodeError as error:
 
900
            msg = "UnicodeError: %s" % unicode(error)
 
901
            raise exc.HTTPBadRequest(explanation=msg)
 
902
        # Let the caller deal with unhandled exceptions.
 
903
 
 
904
        # If the caller wanted a reservation_id, return it
 
905
 
 
906
        # NOTE(treinish): XML serialization will not work without a root
 
907
        # selector of 'server' however JSON return is not expecting a server
 
908
        # field/object
 
909
        if ret_resv_id and (req.get_content_type() == 'application/xml'):
 
910
            return {'server': {'reservation_id': resv_id}}
 
911
        elif ret_resv_id:
 
912
            return {'reservation_id': resv_id}
 
913
 
 
914
        req.cache_db_instances(instances)
 
915
        server = self._view_builder.create(req, instances[0])
 
916
 
 
917
        if '_is_precooked' in server['server'].keys():
 
918
            del server['server']['_is_precooked']
 
919
        else:
 
920
            if FLAGS.enable_instance_password:
 
921
                server['server']['adminPass'] = password
 
922
 
 
923
        robj = wsgi.ResponseObject(server)
 
924
 
 
925
        return self._add_location(robj)
 
926
 
 
927
    def _delete(self, context, req, instance_uuid):
 
928
        instance = self._get_server(context, req, instance_uuid)
 
929
        if FLAGS.reclaim_instance_interval:
 
930
            self.compute_api.soft_delete(context, instance)
 
931
        else:
 
932
            self.compute_api.delete(context, instance)
 
933
 
 
934
    @wsgi.serializers(xml=ServerTemplate)
 
935
    def update(self, req, id, body):
 
936
        """Update server then pass on to version-specific controller."""
 
937
        if not self.is_valid_body(body, 'server'):
 
938
            raise exc.HTTPUnprocessableEntity()
 
939
 
 
940
        ctxt = req.environ['nova.context']
 
941
        update_dict = {}
 
942
 
 
943
        if 'name' in body['server']:
 
944
            name = body['server']['name']
 
945
            self._validate_server_name(name)
 
946
            update_dict['display_name'] = name.strip()
 
947
 
 
948
        if 'accessIPv4' in body['server']:
 
949
            access_ipv4 = body['server']['accessIPv4']
 
950
            if access_ipv4 is None:
 
951
                access_ipv4 = ''
 
952
            if access_ipv4:
 
953
                self._validate_access_ipv4(access_ipv4)
 
954
            update_dict['access_ip_v4'] = access_ipv4.strip()
 
955
 
 
956
        if 'accessIPv6' in body['server']:
 
957
            access_ipv6 = body['server']['accessIPv6']
 
958
            if access_ipv6 is None:
 
959
                access_ipv6 = ''
 
960
            if access_ipv6:
 
961
                self._validate_access_ipv6(access_ipv6)
 
962
            update_dict['access_ip_v6'] = access_ipv6.strip()
 
963
 
 
964
        if 'auto_disk_config' in body['server']:
 
965
            auto_disk_config = utils.bool_from_str(
 
966
                    body['server']['auto_disk_config'])
 
967
            update_dict['auto_disk_config'] = auto_disk_config
 
968
 
 
969
        if 'hostId' in body['server']:
 
970
            msg = _("HostId cannot be updated.")
 
971
            raise exc.HTTPBadRequest(explanation=msg)
 
972
 
 
973
        try:
 
974
            instance = self.compute_api.get(ctxt, id)
 
975
            req.cache_db_instance(instance)
 
976
            self.compute_api.update(ctxt, instance, **update_dict)
 
977
        except exception.NotFound:
 
978
            raise exc.HTTPNotFound()
 
979
 
 
980
        instance.update(update_dict)
 
981
 
 
982
        self._add_instance_faults(ctxt, [instance])
 
983
        return self._view_builder.show(req, instance)
 
984
 
 
985
    @wsgi.response(202)
 
986
    @wsgi.serializers(xml=FullServerTemplate)
 
987
    @wsgi.deserializers(xml=ActionDeserializer)
 
988
    @wsgi.action('confirmResize')
 
989
    def _action_confirm_resize(self, req, id, body):
 
990
        context = req.environ['nova.context']
 
991
        instance = self._get_server(context, req, id)
 
992
        try:
 
993
            self.compute_api.confirm_resize(context, instance)
 
994
        except exception.MigrationNotFound:
 
995
            msg = _("Instance has not been resized.")
 
996
            raise exc.HTTPBadRequest(explanation=msg)
 
997
        except exception.InstanceInvalidState as state_error:
 
998
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
999
                    'confirmResize')
 
1000
        except Exception, e:
 
1001
            LOG.exception(_("Error in confirm-resize %s"), e)
 
1002
            raise exc.HTTPBadRequest()
 
1003
        return exc.HTTPNoContent()
 
1004
 
 
1005
    @wsgi.response(202)
 
1006
    @wsgi.serializers(xml=FullServerTemplate)
 
1007
    @wsgi.deserializers(xml=ActionDeserializer)
 
1008
    @wsgi.action('revertResize')
 
1009
    def _action_revert_resize(self, req, id, body):
 
1010
        context = req.environ['nova.context']
 
1011
        instance = self._get_server(context, req, id)
 
1012
        try:
 
1013
            self.compute_api.revert_resize(context, instance)
 
1014
        except exception.MigrationNotFound:
 
1015
            msg = _("Instance has not been resized.")
 
1016
            raise exc.HTTPBadRequest(explanation=msg)
 
1017
        except exception.InstanceInvalidState as state_error:
 
1018
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1019
                    'revertResize')
 
1020
        except Exception, e:
 
1021
            LOG.exception(_("Error in revert-resize %s"), e)
 
1022
            raise exc.HTTPBadRequest()
 
1023
        return webob.Response(status_int=202)
 
1024
 
 
1025
    @wsgi.response(202)
 
1026
    @wsgi.serializers(xml=FullServerTemplate)
 
1027
    @wsgi.deserializers(xml=ActionDeserializer)
 
1028
    @wsgi.action('reboot')
 
1029
    def _action_reboot(self, req, id, body):
 
1030
        if 'reboot' in body and 'type' in body['reboot']:
 
1031
            valid_reboot_types = ['HARD', 'SOFT']
 
1032
            reboot_type = body['reboot']['type'].upper()
 
1033
            if not valid_reboot_types.count(reboot_type):
 
1034
                msg = _("Argument 'type' for reboot is not HARD or SOFT")
 
1035
                LOG.error(msg)
 
1036
                raise exc.HTTPBadRequest(explanation=msg)
 
1037
        else:
 
1038
            msg = _("Missing argument 'type' for reboot")
 
1039
            LOG.error(msg)
 
1040
            raise exc.HTTPBadRequest(explanation=msg)
 
1041
 
 
1042
        context = req.environ['nova.context']
 
1043
        instance = self._get_server(context, req, id)
 
1044
 
 
1045
        try:
 
1046
            self.compute_api.reboot(context, instance, reboot_type)
 
1047
        except exception.InstanceInvalidState as state_error:
 
1048
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1049
                    'reboot')
 
1050
        except Exception, e:
 
1051
            LOG.exception(_("Error in reboot %s"), e, instance=instance)
 
1052
            raise exc.HTTPUnprocessableEntity()
 
1053
        return webob.Response(status_int=202)
 
1054
 
 
1055
    def _resize(self, req, instance_id, flavor_id, **kwargs):
 
1056
        """Begin the resize process with given instance/flavor."""
 
1057
        context = req.environ["nova.context"]
 
1058
        instance = self._get_server(context, req, instance_id)
 
1059
 
 
1060
        try:
 
1061
            self.compute_api.resize(context, instance, flavor_id, **kwargs)
 
1062
        except exception.FlavorNotFound:
 
1063
            msg = _("Unable to locate requested flavor.")
 
1064
            raise exc.HTTPBadRequest(explanation=msg)
 
1065
        except exception.CannotResizeToSameFlavor:
 
1066
            msg = _("Resize requires a flavor change.")
 
1067
            raise exc.HTTPBadRequest(explanation=msg)
 
1068
        except exception.InstanceInvalidState as state_error:
 
1069
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1070
                    'resize')
 
1071
 
 
1072
        return webob.Response(status_int=202)
 
1073
 
 
1074
    @wsgi.response(204)
 
1075
    def delete(self, req, id):
 
1076
        """Destroys a server."""
 
1077
        try:
 
1078
            self._delete(req.environ['nova.context'], req, id)
 
1079
        except exception.NotFound:
 
1080
            raise exc.HTTPNotFound()
 
1081
        except exception.InstanceInvalidState as state_error:
 
1082
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1083
                    'delete')
 
1084
 
 
1085
    def _image_ref_from_req_data(self, data):
 
1086
        try:
 
1087
            return unicode(data['server']['imageRef'])
 
1088
        except (TypeError, KeyError):
 
1089
            msg = _("Missing imageRef attribute")
 
1090
            raise exc.HTTPBadRequest(explanation=msg)
 
1091
 
 
1092
    def _image_uuid_from_href(self, image_href):
 
1093
        # If the image href was generated by nova api, strip image_href
 
1094
        # down to an id and use the default glance connection params
 
1095
        image_uuid = image_href.split('/').pop()
 
1096
 
 
1097
        if not utils.is_uuid_like(image_uuid):
 
1098
            msg = _("Invalid imageRef provided.")
 
1099
            raise exc.HTTPBadRequest(explanation=msg)
 
1100
 
 
1101
        return image_uuid
 
1102
 
 
1103
    def _flavor_id_from_req_data(self, data):
 
1104
        try:
 
1105
            flavor_ref = data['server']['flavorRef']
 
1106
        except (TypeError, KeyError):
 
1107
            msg = _("Missing flavorRef attribute")
 
1108
            raise exc.HTTPBadRequest(explanation=msg)
 
1109
 
 
1110
        return common.get_id_from_href(flavor_ref)
 
1111
 
 
1112
    @wsgi.response(202)
 
1113
    @wsgi.serializers(xml=FullServerTemplate)
 
1114
    @wsgi.deserializers(xml=ActionDeserializer)
 
1115
    @wsgi.action('changePassword')
 
1116
    def _action_change_password(self, req, id, body):
 
1117
        context = req.environ['nova.context']
 
1118
        if (not 'changePassword' in body
 
1119
            or not 'adminPass' in body['changePassword']):
 
1120
            msg = _("No adminPass was specified")
 
1121
            raise exc.HTTPBadRequest(explanation=msg)
 
1122
        password = body['changePassword']['adminPass']
 
1123
        if not isinstance(password, basestring):
 
1124
            msg = _("Invalid adminPass")
 
1125
            raise exc.HTTPBadRequest(explanation=msg)
 
1126
        server = self._get_server(context, req, id)
 
1127
        self.compute_api.set_admin_password(context, server, password)
 
1128
        return webob.Response(status_int=202)
 
1129
 
 
1130
    def _validate_metadata(self, metadata):
 
1131
        """Ensure that we can work with the metadata given."""
 
1132
        try:
 
1133
            metadata.iteritems()
 
1134
        except AttributeError:
 
1135
            msg = _("Unable to parse metadata key/value pairs.")
 
1136
            LOG.debug(msg)
 
1137
            raise exc.HTTPBadRequest(explanation=msg)
 
1138
 
 
1139
    @wsgi.response(202)
 
1140
    @wsgi.serializers(xml=FullServerTemplate)
 
1141
    @wsgi.deserializers(xml=ActionDeserializer)
 
1142
    @wsgi.action('resize')
 
1143
    def _action_resize(self, req, id, body):
 
1144
        """Resizes a given instance to the flavor size requested."""
 
1145
        try:
 
1146
            flavor_ref = body["resize"]["flavorRef"]
 
1147
            if not flavor_ref:
 
1148
                msg = _("Resize request has invalid 'flavorRef' attribute.")
 
1149
                raise exc.HTTPBadRequest(explanation=msg)
 
1150
        except (KeyError, TypeError):
 
1151
            msg = _("Resize requests require 'flavorRef' attribute.")
 
1152
            raise exc.HTTPBadRequest(explanation=msg)
 
1153
 
 
1154
        kwargs = {}
 
1155
        if 'auto_disk_config' in body['resize']:
 
1156
            kwargs['auto_disk_config'] = body['resize']['auto_disk_config']
 
1157
 
 
1158
        return self._resize(req, id, flavor_ref, **kwargs)
 
1159
 
 
1160
    @wsgi.response(202)
 
1161
    @wsgi.serializers(xml=FullServerTemplate)
 
1162
    @wsgi.deserializers(xml=ActionDeserializer)
 
1163
    @wsgi.action('rebuild')
 
1164
    def _action_rebuild(self, req, id, body):
 
1165
        """Rebuild an instance with the given attributes."""
 
1166
        try:
 
1167
            body = body['rebuild']
 
1168
        except (KeyError, TypeError):
 
1169
            raise exc.HTTPBadRequest(_("Invalid request body"))
 
1170
 
 
1171
        try:
 
1172
            image_href = body["imageRef"]
 
1173
        except (KeyError, TypeError):
 
1174
            msg = _("Could not parse imageRef from request.")
 
1175
            raise exc.HTTPBadRequest(explanation=msg)
 
1176
 
 
1177
        image_href = self._image_uuid_from_href(image_href)
 
1178
 
 
1179
        try:
 
1180
            password = body['adminPass']
 
1181
        except (KeyError, TypeError):
 
1182
            password = utils.generate_password(FLAGS.password_length)
 
1183
 
 
1184
        context = req.environ['nova.context']
 
1185
        instance = self._get_server(context, req, id)
 
1186
 
 
1187
        attr_map = {
 
1188
            'personality': 'files_to_inject',
 
1189
            'name': 'display_name',
 
1190
            'accessIPv4': 'access_ip_v4',
 
1191
            'accessIPv6': 'access_ip_v6',
 
1192
            'metadata': 'metadata',
 
1193
            'auto_disk_config': 'auto_disk_config',
 
1194
        }
 
1195
 
 
1196
        if 'accessIPv4' in body:
 
1197
            self._validate_access_ipv4(body['accessIPv4'])
 
1198
 
 
1199
        if 'accessIPv6' in body:
 
1200
            self._validate_access_ipv6(body['accessIPv6'])
 
1201
 
 
1202
        if 'name' in body:
 
1203
            self._validate_server_name(body['name'])
 
1204
 
 
1205
        kwargs = {}
 
1206
 
 
1207
        for request_attribute, instance_attribute in attr_map.items():
 
1208
            try:
 
1209
                kwargs[instance_attribute] = body[request_attribute]
 
1210
            except (KeyError, TypeError):
 
1211
                pass
 
1212
 
 
1213
        self._validate_metadata(kwargs.get('metadata', {}))
 
1214
 
 
1215
        if 'files_to_inject' in kwargs:
 
1216
            personality = kwargs['files_to_inject']
 
1217
            kwargs['files_to_inject'] = self._get_injected_files(personality)
 
1218
 
 
1219
        try:
 
1220
            self.compute_api.rebuild(context,
 
1221
                                     instance,
 
1222
                                     image_href,
 
1223
                                     password,
 
1224
                                     **kwargs)
 
1225
        except exception.InstanceInvalidState as state_error:
 
1226
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1227
                    'rebuild')
 
1228
        except exception.InstanceNotFound:
 
1229
            msg = _("Instance could not be found")
 
1230
            raise exc.HTTPNotFound(explanation=msg)
 
1231
        except exception.InvalidMetadata as error:
 
1232
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
1233
        except exception.InvalidMetadataSize as error:
 
1234
            raise exc.HTTPRequestEntityTooLarge(explanation=unicode(error))
 
1235
        except exception.ImageNotFound:
 
1236
            msg = _("Cannot find image for rebuild")
 
1237
            raise exc.HTTPBadRequest(explanation=msg)
 
1238
        except exception.InstanceTypeMemoryTooSmall as error:
 
1239
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
1240
        except exception.InstanceTypeDiskTooSmall as error:
 
1241
            raise exc.HTTPBadRequest(explanation=unicode(error))
 
1242
 
 
1243
        instance = self._get_server(context, req, id)
 
1244
 
 
1245
        self._add_instance_faults(context, [instance])
 
1246
        view = self._view_builder.show(req, instance)
 
1247
 
 
1248
        # Add on the adminPass attribute since the view doesn't do it
 
1249
        # unless instance passwords are disabled
 
1250
        if FLAGS.enable_instance_password:
 
1251
            view['server']['adminPass'] = password
 
1252
 
 
1253
        robj = wsgi.ResponseObject(view)
 
1254
        return self._add_location(robj)
 
1255
 
 
1256
    @wsgi.response(202)
 
1257
    @wsgi.serializers(xml=FullServerTemplate)
 
1258
    @wsgi.deserializers(xml=ActionDeserializer)
 
1259
    @wsgi.action('createImage')
 
1260
    @common.check_snapshots_enabled
 
1261
    def _action_create_image(self, req, id, body):
 
1262
        """Snapshot a server instance."""
 
1263
        context = req.environ['nova.context']
 
1264
        entity = body.get("createImage", {})
 
1265
 
 
1266
        image_name = entity.get("name")
 
1267
 
 
1268
        if not image_name:
 
1269
            msg = _("createImage entity requires name attribute")
 
1270
            raise exc.HTTPBadRequest(explanation=msg)
 
1271
 
 
1272
        props = {}
 
1273
        metadata = entity.get('metadata', {})
 
1274
        common.check_img_metadata_properties_quota(context, metadata)
 
1275
        try:
 
1276
            props.update(metadata)
 
1277
        except ValueError:
 
1278
            msg = _("Invalid metadata")
 
1279
            raise exc.HTTPBadRequest(explanation=msg)
 
1280
 
 
1281
        instance = self._get_server(context, req, id)
 
1282
 
 
1283
        bdms = self.compute_api.get_instance_bdms(context, instance)
 
1284
 
 
1285
        try:
 
1286
            if self.compute_api.is_volume_backed_instance(context, instance,
 
1287
                                                          bdms):
 
1288
                img = instance['image_ref']
 
1289
                src_image = self.compute_api.image_service.show(context, img)
 
1290
                image_meta = dict(src_image)
 
1291
 
 
1292
                image = self.compute_api.snapshot_volume_backed(
 
1293
                                                       context,
 
1294
                                                       instance,
 
1295
                                                       image_meta,
 
1296
                                                       image_name,
 
1297
                                                       extra_properties=props)
 
1298
            else:
 
1299
                image = self.compute_api.snapshot(context,
 
1300
                                                  instance,
 
1301
                                                  image_name,
 
1302
                                                  extra_properties=props)
 
1303
        except exception.InstanceInvalidState as state_error:
 
1304
            common.raise_http_conflict_for_instance_invalid_state(state_error,
 
1305
                        'createImage')
 
1306
 
 
1307
        # build location of newly-created image entity
 
1308
        image_id = str(image['id'])
 
1309
        image_ref = os.path.join(req.application_url,
 
1310
                                 context.project_id,
 
1311
                                 'images',
 
1312
                                 image_id)
 
1313
 
 
1314
        resp = webob.Response(status_int=202)
 
1315
        resp.headers['Location'] = image_ref
 
1316
        return resp
 
1317
 
 
1318
    def _get_server_admin_password(self, server):
 
1319
        """Determine the admin password for a server on creation."""
 
1320
        try:
 
1321
            password = server['adminPass']
 
1322
            self._validate_admin_password(password)
 
1323
        except KeyError:
 
1324
            password = utils.generate_password(FLAGS.password_length)
 
1325
        except ValueError:
 
1326
            raise exc.HTTPBadRequest(explanation=_("Invalid adminPass"))
 
1327
 
 
1328
        return password
 
1329
 
 
1330
    def _validate_admin_password(self, password):
 
1331
        if not isinstance(password, basestring):
 
1332
            raise ValueError()
 
1333
 
 
1334
    def _get_server_search_options(self):
 
1335
        """Return server search options allowed by non-admin."""
 
1336
        return ('reservation_id', 'name', 'status', 'image', 'flavor',
 
1337
                'changes-since')
 
1338
 
 
1339
 
 
1340
def create_resource(ext_mgr):
 
1341
    return wsgi.Resource(Controller(ext_mgr))
 
1342
 
 
1343
 
 
1344
def remove_invalid_options(context, search_options, allowed_search_options):
 
1345
    """Remove search options that are not valid for non-admin API/context."""
 
1346
    if context.is_admin:
 
1347
        # Allow all options
 
1348
        return
 
1349
    # Otherwise, strip out all unknown options
 
1350
    unknown_options = [opt for opt in search_options
 
1351
            if opt not in allowed_search_options]
 
1352
    unk_opt_str = ", ".join(unknown_options)
 
1353
    log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals()
 
1354
    LOG.debug(log_msg)
 
1355
    for opt in unknown_options:
 
1356
        search_options.pop(opt, None)