1
# Copyright 2011 OpenStack LLC.
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
21
from xml.dom import minidom
23
from nova import exception
24
from nova import flags
25
from nova import log as logging
27
from nova import quota
28
from nova import utils
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
36
LOG = logging.getLogger('nova.api.openstack.create_instance_helper')
40
class CreateFault(exception.NovaException):
41
message = _("Invalid parameters given to create_instance.")
43
def __init__(self, fault):
45
super(CreateFault, self).__init__()
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).
52
Once we stabilize the Zones portion of the API we may be able
53
to move this code back into servers.py
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__()
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().
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.
73
raise faults.Fault(exc.HTTPUnprocessableEntity())
75
context = req.environ['nova.context']
77
password = self.controller._get_server_admin_password(body['server'])
81
key_pairs = auth_manager.AuthManager.get_key_pairs(context)
83
key_pair = key_pairs[0]
84
key_name = key_pair['name']
85
key_data = key_pair['public_key']
87
image_href = self.controller._image_ref_from_req_data(body)
89
image_service, image_id = nova.image.get_image_service(image_href)
90
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
92
images = set([str(x['id']) for x in image_service.index(context)])
93
assert str(image_id) in images
95
msg = _("Cannot find requested image %(image_href)s: %(e)s" %
97
raise faults.Fault(exc.HTTPBadRequest(msg))
99
personality = body['server'].get('personality')
103
injected_files = self._get_injected_files(personality)
105
flavor_id = self.controller._flavor_id_from_req_data(body)
107
if not 'name' in body['server']:
108
msg = _("Server name is not defined")
109
raise exc.HTTPBadRequest(msg)
111
zone_blob = body['server'].get('blob')
112
name = body['server']['name']
113
self._validate_server_name(name)
116
reservation_id = body['server'].get('reservation_id')
120
instance_types.get_instance_type_by_flavor_id(flavor_id)
122
'instance_type': inst_type,
123
'image_ref': image_href,
127
return (extra_values,
128
create_method(context,
132
ramdisk_id=ramdisk_id,
134
display_description=name,
137
metadata=body['server'].get('metadata', {}),
138
injected_files=injected_files,
139
admin_password=password,
141
reservation_id=reservation_id
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))
150
# Let the caller deal with unhandled exceptions.
152
def _handle_quota_error(self, error):
154
Reraise quota errors as api-specific http exceptions
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
168
def _deserialize_create(self, request):
170
Deserialize a create request
172
Overrides normal behavior in the case of xml content
174
if request.content_type == "application/xml":
175
deserializer = ServerCreateRequestXMLDeserializer()
176
return deserializer.deserialize(request.body)
178
return self._deserialize(request.body, request.get_content_type())
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)
185
if value.strip() == '':
186
msg = _("Server name is an empty string")
187
raise exc.HTTPBadRequest(msg)
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.
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(
199
return kernel_id, ramdisk_id
202
def _do_get_kernel_ramdisk_from_image(image_meta):
203
"""Given an ImageService image_meta, return kernel and ramdisk image
206
This is only valid for `ami` style images.
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"))
213
if image_meta.get('container_format') != 'ami':
217
kernel_id = image_meta['properties']['kernel_id']
219
raise exception.KernelNotFoundForImage(image_id=image_id)
222
ramdisk_id = image_meta['properties']['ramdisk_id']
224
raise exception.RamdiskNotFoundForImage(image_id=image_id)
226
return kernel_id, ramdisk_id
228
def _get_injected_files(self, personality):
230
Create a list of injected files from the personality attribute
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.
238
for item in personality:
241
contents = item['contents']
242
except KeyError as key:
243
expl = _('Bad personality format: missing %s') % key
244
raise exc.HTTPBadRequest(explanation=expl)
246
expl = _('Bad personality format')
247
raise exc.HTTPBadRequest(explanation=expl)
249
contents = base64.b64decode(contents)
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
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)
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')
265
return utils.generate_password(16)
266
if not isinstance(password, basestring) or password == '':
267
msg = _("Invalid adminPass")
268
raise exc.HTTPBadRequest(msg)
272
class ServerXMLDeserializer(wsgi.XMLDeserializer):
274
Deserializer to handle xml-formatted server create requests.
276
Handles standard server attributes as well as optional metadata
277
and personality attributes
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}
286
def _extract_server(self, node):
287
"""Marshal the server attribute of a parsed request"""
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
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:
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)
312
def _extract_personality(self, server_node):
313
"""Marshal the personality attribute of a parsed request"""
315
self._find_first_child_named(server_node, "personality")
316
if personality_node is None:
319
for file_node in self._find_children_named(personality_node, "file"):
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)
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:
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:
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