~rackspace-titan/nova/api-profiling

« back to all changes in this revision

Viewing changes to nova/api/openstack/create_instance_helper.py

  • Committer: Tarmac
  • Author(s): Sandy Walsh
  • Date: 2011-06-14 21:11:25 UTC
  • mfrom: (1063.8.27 dist-sched-4)
  • Revision ID: tarmac-20110614211125-sbdntqdts9nn0ha9
Phew ... ok, this is the last dist-scheduler merge before we get into serious testing and minor tweaks. The heavy lifting is largely done.

This branch adds an OS API POST /zone/boot command which returns a reservation ID (unlike POST /servers which returns a single instance_id). 

This branch requires v2.5 of python-novaclient

Additionally GET /servers can now take an optional reservation_id parameter, which will return all the instances with that reservation ID across all zones.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2011 OpenStack LLC.
 
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
import base64
 
17
import re
 
18
import webob
 
19
 
 
20
from webob import exc
 
21
from xml.dom import minidom
 
22
 
 
23
from nova import exception
 
24
from nova import flags
 
25
from nova import log as logging
 
26
import nova.image
 
27
from nova import quota
 
28
from nova import utils
 
29
 
 
30
from nova.compute import instance_types
 
31
from nova.api.openstack import faults
 
32
from nova.api.openstack import wsgi
 
33
from nova.auth import manager as auth_manager
 
34
 
 
35
 
 
36
LOG = logging.getLogger('nova.api.openstack.create_instance_helper')
 
37
FLAGS = flags.FLAGS
 
38
 
 
39
 
 
40
class CreateFault(exception.NovaException):
 
41
    message = _("Invalid parameters given to create_instance.")
 
42
 
 
43
    def __init__(self, fault):
 
44
        self.fault = fault
 
45
        super(CreateFault, self).__init__()
 
46
 
 
47
 
 
48
class CreateInstanceHelper(object):
 
49
    """This is the base class for OS API Controllers that
 
50
    are capable of creating instances (currently Servers and Zones).
 
51
 
 
52
    Once we stabilize the Zones portion of the API we may be able
 
53
    to move this code back into servers.py
 
54
    """
 
55
 
 
56
    def __init__(self, controller):
 
57
        """We need the image service to create an instance."""
 
58
        self.controller = controller
 
59
        self._image_service = utils.import_object(FLAGS.image_service)
 
60
        super(CreateInstanceHelper, self).__init__()
 
61
 
 
62
    def create_instance(self, req, body, create_method):
 
63
        """Creates a new server for the given user. The approach
 
64
        used depends on the create_method. For example, the standard
 
65
        POST /server call uses compute.api.create(), while
 
66
        POST /zones/server uses compute.api.create_all_at_once().
 
67
 
 
68
        The problem is, both approaches return different values (i.e.
 
69
        [instance dicts] vs. reservation_id). So the handling of the
 
70
        return type from this method is left to the caller.
 
71
        """
 
72
        if not body:
 
73
            raise faults.Fault(exc.HTTPUnprocessableEntity())
 
74
 
 
75
        context = req.environ['nova.context']
 
76
 
 
77
        password = self.controller._get_server_admin_password(body['server'])
 
78
 
 
79
        key_name = None
 
80
        key_data = None
 
81
        key_pairs = auth_manager.AuthManager.get_key_pairs(context)
 
82
        if key_pairs:
 
83
            key_pair = key_pairs[0]
 
84
            key_name = key_pair['name']
 
85
            key_data = key_pair['public_key']
 
86
 
 
87
        image_href = self.controller._image_ref_from_req_data(body)
 
88
        try:
 
89
            image_service, image_id = nova.image.get_image_service(image_href)
 
90
            kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
 
91
                                                req, image_id)
 
92
            images = set([str(x['id']) for x in image_service.index(context)])
 
93
            assert str(image_id) in images
 
94
        except Exception, e:
 
95
            msg = _("Cannot find requested image %(image_href)s: %(e)s" %
 
96
                                                                    locals())
 
97
            raise faults.Fault(exc.HTTPBadRequest(msg))
 
98
 
 
99
        personality = body['server'].get('personality')
 
100
 
 
101
        injected_files = []
 
102
        if personality:
 
103
            injected_files = self._get_injected_files(personality)
 
104
 
 
105
        flavor_id = self.controller._flavor_id_from_req_data(body)
 
106
 
 
107
        if not 'name' in body['server']:
 
108
            msg = _("Server name is not defined")
 
109
            raise exc.HTTPBadRequest(msg)
 
110
 
 
111
        zone_blob = body['server'].get('blob')
 
112
        name = body['server']['name']
 
113
        self._validate_server_name(name)
 
114
        name = name.strip()
 
115
 
 
116
        reservation_id = body['server'].get('reservation_id')
 
117
 
 
118
        try:
 
119
            inst_type = \
 
120
                    instance_types.get_instance_type_by_flavor_id(flavor_id)
 
121
            extra_values = {
 
122
                'instance_type': inst_type,
 
123
                'image_ref': image_href,
 
124
                'password': password
 
125
            }
 
126
 
 
127
            return (extra_values,
 
128
                    create_method(context,
 
129
                                  inst_type,
 
130
                                  image_id,
 
131
                                  kernel_id=kernel_id,
 
132
                                  ramdisk_id=ramdisk_id,
 
133
                                  display_name=name,
 
134
                                  display_description=name,
 
135
                                  key_name=key_name,
 
136
                                  key_data=key_data,
 
137
                                  metadata=body['server'].get('metadata', {}),
 
138
                                  injected_files=injected_files,
 
139
                                  admin_password=password,
 
140
                                  zone_blob=zone_blob,
 
141
                                  reservation_id=reservation_id
 
142
                    )
 
143
                )
 
144
        except quota.QuotaError as error:
 
145
            self._handle_quota_error(error)
 
146
        except exception.ImageNotFound as error:
 
147
            msg = _("Can not find requested image")
 
148
            raise faults.Fault(exc.HTTPBadRequest(msg))
 
149
 
 
150
        # Let the caller deal with unhandled exceptions.
 
151
 
 
152
    def _handle_quota_error(self, error):
 
153
        """
 
154
        Reraise quota errors as api-specific http exceptions
 
155
        """
 
156
        if error.code == "OnsetFileLimitExceeded":
 
157
            expl = _("Personality file limit exceeded")
 
158
            raise exc.HTTPBadRequest(explanation=expl)
 
159
        if error.code == "OnsetFilePathLimitExceeded":
 
160
            expl = _("Personality file path too long")
 
161
            raise exc.HTTPBadRequest(explanation=expl)
 
162
        if error.code == "OnsetFileContentLimitExceeded":
 
163
            expl = _("Personality file content too long")
 
164
            raise exc.HTTPBadRequest(explanation=expl)
 
165
        # if the original error is okay, just reraise it
 
166
        raise error
 
167
 
 
168
    def _deserialize_create(self, request):
 
169
        """
 
170
        Deserialize a create request
 
171
 
 
172
        Overrides normal behavior in the case of xml content
 
173
        """
 
174
        if request.content_type == "application/xml":
 
175
            deserializer = ServerCreateRequestXMLDeserializer()
 
176
            return deserializer.deserialize(request.body)
 
177
        else:
 
178
            return self._deserialize(request.body, request.get_content_type())
 
179
 
 
180
    def _validate_server_name(self, value):
 
181
        if not isinstance(value, basestring):
 
182
            msg = _("Server name is not a string or unicode")
 
183
            raise exc.HTTPBadRequest(msg)
 
184
 
 
185
        if value.strip() == '':
 
186
            msg = _("Server name is an empty string")
 
187
            raise exc.HTTPBadRequest(msg)
 
188
 
 
189
    def _get_kernel_ramdisk_from_image(self, req, image_id):
 
190
        """Fetch an image from the ImageService, then if present, return the
 
191
        associated kernel and ramdisk image IDs.
 
192
        """
 
193
        context = req.environ['nova.context']
 
194
        image_meta = self._image_service.show(context, image_id)
 
195
        # NOTE(sirp): extracted to a separate method to aid unit-testing, the
 
196
        # new method doesn't need a request obj or an ImageService stub
 
197
        kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image(
 
198
            image_meta)
 
199
        return kernel_id, ramdisk_id
 
200
 
 
201
    @staticmethod
 
202
    def  _do_get_kernel_ramdisk_from_image(image_meta):
 
203
        """Given an ImageService image_meta, return kernel and ramdisk image
 
204
        ids if present.
 
205
 
 
206
        This is only valid for `ami` style images.
 
207
        """
 
208
        image_id = image_meta['id']
 
209
        if image_meta['status'] != 'active':
 
210
            raise exception.ImageUnacceptable(image_id=image_id,
 
211
                                              reason=_("status is not active"))
 
212
 
 
213
        if image_meta.get('container_format') != 'ami':
 
214
            return None, None
 
215
 
 
216
        try:
 
217
            kernel_id = image_meta['properties']['kernel_id']
 
218
        except KeyError:
 
219
            raise exception.KernelNotFoundForImage(image_id=image_id)
 
220
 
 
221
        try:
 
222
            ramdisk_id = image_meta['properties']['ramdisk_id']
 
223
        except KeyError:
 
224
            raise exception.RamdiskNotFoundForImage(image_id=image_id)
 
225
 
 
226
        return kernel_id, ramdisk_id
 
227
 
 
228
    def _get_injected_files(self, personality):
 
229
        """
 
230
        Create a list of injected files from the personality attribute
 
231
 
 
232
        At this time, injected_files must be formatted as a list of
 
233
        (file_path, file_content) pairs for compatibility with the
 
234
        underlying compute service.
 
235
        """
 
236
        injected_files = []
 
237
 
 
238
        for item in personality:
 
239
            try:
 
240
                path = item['path']
 
241
                contents = item['contents']
 
242
            except KeyError as key:
 
243
                expl = _('Bad personality format: missing %s') % key
 
244
                raise exc.HTTPBadRequest(explanation=expl)
 
245
            except TypeError:
 
246
                expl = _('Bad personality format')
 
247
                raise exc.HTTPBadRequest(explanation=expl)
 
248
            try:
 
249
                contents = base64.b64decode(contents)
 
250
            except TypeError:
 
251
                expl = _('Personality content for %s cannot be decoded') % path
 
252
                raise exc.HTTPBadRequest(explanation=expl)
 
253
            injected_files.append((path, contents))
 
254
        return injected_files
 
255
 
 
256
    def _get_server_admin_password_old_style(self, server):
 
257
        """ Determine the admin password for a server on creation """
 
258
        return utils.generate_password(16)
 
259
 
 
260
    def _get_server_admin_password_new_style(self, server):
 
261
        """ Determine the admin password for a server on creation """
 
262
        password = server.get('adminPass')
 
263
 
 
264
        if password is None:
 
265
            return utils.generate_password(16)
 
266
        if not isinstance(password, basestring) or password == '':
 
267
            msg = _("Invalid adminPass")
 
268
            raise exc.HTTPBadRequest(msg)
 
269
        return password
 
270
 
 
271
 
 
272
class ServerXMLDeserializer(wsgi.XMLDeserializer):
 
273
    """
 
274
    Deserializer to handle xml-formatted server create requests.
 
275
 
 
276
    Handles standard server attributes as well as optional metadata
 
277
    and personality attributes
 
278
    """
 
279
 
 
280
    def create(self, string):
 
281
        """Deserialize an xml-formatted server create request"""
 
282
        dom = minidom.parseString(string)
 
283
        server = self._extract_server(dom)
 
284
        return {'server': server}
 
285
 
 
286
    def _extract_server(self, node):
 
287
        """Marshal the server attribute of a parsed request"""
 
288
        server = {}
 
289
        server_node = self._find_first_child_named(node, 'server')
 
290
        for attr in ["name", "imageId", "flavorId", "imageRef", "flavorRef"]:
 
291
            if server_node.getAttribute(attr):
 
292
                server[attr] = server_node.getAttribute(attr)
 
293
        metadata = self._extract_metadata(server_node)
 
294
        if metadata is not None:
 
295
            server["metadata"] = metadata
 
296
        personality = self._extract_personality(server_node)
 
297
        if personality is not None:
 
298
            server["personality"] = personality
 
299
        return server
 
300
 
 
301
    def _extract_metadata(self, server_node):
 
302
        """Marshal the metadata attribute of a parsed request"""
 
303
        metadata_node = self._find_first_child_named(server_node, "metadata")
 
304
        if metadata_node is None:
 
305
            return None
 
306
        metadata = {}
 
307
        for meta_node in self._find_children_named(metadata_node, "meta"):
 
308
            key = meta_node.getAttribute("key")
 
309
            metadata[key] = self._extract_text(meta_node)
 
310
        return metadata
 
311
 
 
312
    def _extract_personality(self, server_node):
 
313
        """Marshal the personality attribute of a parsed request"""
 
314
        personality_node = \
 
315
                self._find_first_child_named(server_node, "personality")
 
316
        if personality_node is None:
 
317
            return None
 
318
        personality = []
 
319
        for file_node in self._find_children_named(personality_node, "file"):
 
320
            item = {}
 
321
            if file_node.hasAttribute("path"):
 
322
                item["path"] = file_node.getAttribute("path")
 
323
            item["contents"] = self._extract_text(file_node)
 
324
            personality.append(item)
 
325
        return personality
 
326
 
 
327
    def _find_first_child_named(self, parent, name):
 
328
        """Search a nodes children for the first child with a given name"""
 
329
        for node in parent.childNodes:
 
330
            if node.nodeName == name:
 
331
                return node
 
332
        return None
 
333
 
 
334
    def _find_children_named(self, parent, name):
 
335
        """Return all of a nodes children who have the given name"""
 
336
        for node in parent.childNodes:
 
337
            if node.nodeName == name:
 
338
                yield node
 
339
 
 
340
    def _extract_text(self, node):
 
341
        """Get the text field contained by the given node"""
 
342
        if len(node.childNodes) == 1:
 
343
            child = node.childNodes[0]
 
344
            if child.nodeType == child.TEXT_NODE:
 
345
                return child.nodeValue
 
346
        return ""