484 lines
19 KiB
Python
484 lines
19 KiB
Python
# Copyright 2011 OpenStack LLC.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import base64
|
|
|
|
from webob import exc
|
|
from xml.dom import minidom
|
|
|
|
from nova import db
|
|
from nova import exception
|
|
from nova import flags
|
|
from nova import log as logging
|
|
import nova.image
|
|
from nova import quota
|
|
from nova import utils
|
|
|
|
from nova.compute import instance_types
|
|
from nova.api.openstack import common
|
|
from nova.api.openstack import wsgi
|
|
|
|
|
|
LOG = logging.getLogger('nova.api.openstack.create_instance_helper')
|
|
FLAGS = flags.FLAGS
|
|
|
|
|
|
class CreateFault(exception.NovaException):
|
|
message = _("Invalid parameters given to create_instance.")
|
|
|
|
def __init__(self, fault):
|
|
self.fault = fault
|
|
super(CreateFault, self).__init__()
|
|
|
|
|
|
class CreateInstanceHelper(object):
|
|
"""This is the base class for OS API Controllers that
|
|
are capable of creating instances (currently Servers and Zones).
|
|
|
|
Once we stabilize the Zones portion of the API we may be able
|
|
to move this code back into servers.py
|
|
"""
|
|
|
|
def __init__(self, controller):
|
|
"""We need the image service to create an instance."""
|
|
self.controller = controller
|
|
self._image_service = utils.import_object(FLAGS.image_service)
|
|
super(CreateInstanceHelper, self).__init__()
|
|
|
|
def create_instance(self, req, body, create_method):
|
|
"""Creates a new server for the given user. The approach
|
|
used depends on the create_method. For example, the standard
|
|
POST /server call uses compute.api.create(), while
|
|
POST /zones/server uses compute.api.create_all_at_once().
|
|
|
|
The problem is, both approaches return different values (i.e.
|
|
[instance dicts] vs. reservation_id). So the handling of the
|
|
return type from this method is left to the caller.
|
|
"""
|
|
if not body:
|
|
raise exc.HTTPUnprocessableEntity()
|
|
|
|
if not 'server' in body:
|
|
raise exc.HTTPUnprocessableEntity()
|
|
|
|
server_dict = body['server']
|
|
context = req.environ['nova.context']
|
|
password = self.controller._get_server_admin_password(server_dict)
|
|
|
|
key_name = None
|
|
key_data = None
|
|
# TODO(vish): Key pair access should move into a common library
|
|
# instead of being accessed directly from the db.
|
|
key_pairs = db.key_pair_get_all_by_user(context.elevated(),
|
|
context.user_id)
|
|
if key_pairs:
|
|
key_pair = key_pairs[0]
|
|
key_name = key_pair['name']
|
|
key_data = key_pair['public_key']
|
|
|
|
image_href = self.controller._image_ref_from_req_data(body)
|
|
# If the image href was generated by nova api, strip image_href
|
|
# down to an id and use the default glance connection params
|
|
|
|
if str(image_href).startswith(req.application_url):
|
|
image_href = image_href.split('/').pop()
|
|
try:
|
|
image_service, image_id = nova.image.get_image_service(image_href)
|
|
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
|
|
req, image_id)
|
|
images = set([str(x['id']) for x in image_service.index(context)])
|
|
assert str(image_id) in images
|
|
except Exception, e:
|
|
msg = _("Cannot find requested image %(image_href)s: %(e)s" %
|
|
locals())
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
personality = server_dict.get('personality')
|
|
|
|
injected_files = []
|
|
if personality:
|
|
injected_files = self._get_injected_files(personality)
|
|
|
|
try:
|
|
flavor_id = self.controller._flavor_id_from_req_data(body)
|
|
except ValueError as error:
|
|
msg = _("Invalid flavorRef provided.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
if not 'name' in server_dict:
|
|
msg = _("Server name is not defined")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
zone_blob = server_dict.get('blob')
|
|
user_data = server_dict.get('user_data')
|
|
availability_zone = server_dict.get('availability_zone')
|
|
name = server_dict['name']
|
|
self._validate_server_name(name)
|
|
name = name.strip()
|
|
|
|
reservation_id = server_dict.get('reservation_id')
|
|
min_count = server_dict.get('min_count')
|
|
max_count = server_dict.get('max_count')
|
|
# min_count and max_count are optional. If they exist, they come
|
|
# in as strings. We want to default 'min_count' to 1, and default
|
|
# 'max_count' to be 'min_count'.
|
|
min_count = int(min_count) if min_count else 1
|
|
max_count = int(max_count) if max_count else min_count
|
|
if min_count > max_count:
|
|
min_count = max_count
|
|
|
|
try:
|
|
inst_type = \
|
|
instance_types.get_instance_type_by_flavor_id(flavor_id)
|
|
extra_values = {
|
|
'instance_type': inst_type,
|
|
'image_ref': image_href,
|
|
'password': password}
|
|
|
|
return (extra_values,
|
|
create_method(context,
|
|
inst_type,
|
|
image_id,
|
|
kernel_id=kernel_id,
|
|
ramdisk_id=ramdisk_id,
|
|
display_name=name,
|
|
display_description=name,
|
|
key_name=key_name,
|
|
key_data=key_data,
|
|
metadata=server_dict.get('metadata', {}),
|
|
injected_files=injected_files,
|
|
admin_password=password,
|
|
zone_blob=zone_blob,
|
|
reservation_id=reservation_id,
|
|
min_count=min_count,
|
|
max_count=max_count,
|
|
user_data=user_data,
|
|
availability_zone=availability_zone))
|
|
except quota.QuotaError as error:
|
|
self._handle_quota_error(error)
|
|
except exception.ImageNotFound as error:
|
|
msg = _("Can not find requested image")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
except exception.FlavorNotFound as error:
|
|
msg = _("Invalid flavorRef provided.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
# Let the caller deal with unhandled exceptions.
|
|
|
|
def _handle_quota_error(self, error):
|
|
"""
|
|
Reraise quota errors as api-specific http exceptions
|
|
"""
|
|
if error.code == "OnsetFileLimitExceeded":
|
|
expl = _("Personality file limit exceeded")
|
|
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
|
headers={'Retry-After': 0})
|
|
if error.code == "OnsetFilePathLimitExceeded":
|
|
expl = _("Personality file path too long")
|
|
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
|
headers={'Retry-After': 0})
|
|
if error.code == "OnsetFileContentLimitExceeded":
|
|
expl = _("Personality file content too long")
|
|
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
|
headers={'Retry-After': 0})
|
|
if error.code == "InstanceLimitExceeded":
|
|
expl = _("Instance quotas have been exceeded")
|
|
raise exc.HTTPRequestEntityTooLarge(explanation=error.message,
|
|
headers={'Retry-After': 0})
|
|
# if the original error is okay, just reraise it
|
|
raise error
|
|
|
|
def _deserialize_create(self, request):
|
|
"""
|
|
Deserialize a create request
|
|
|
|
Overrides normal behavior in the case of xml content
|
|
"""
|
|
if request.content_type == "application/xml":
|
|
deserializer = ServerXMLDeserializer()
|
|
return deserializer.deserialize(request.body)
|
|
else:
|
|
return self._deserialize(request.body, request.get_content_type())
|
|
|
|
def _validate_server_name(self, value):
|
|
if not isinstance(value, basestring):
|
|
msg = _("Server name is not a string or unicode")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
if value.strip() == '':
|
|
msg = _("Server name is an empty string")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
def _get_kernel_ramdisk_from_image(self, req, image_id):
|
|
"""Fetch an image from the ImageService, then if present, return the
|
|
associated kernel and ramdisk image IDs.
|
|
"""
|
|
context = req.environ['nova.context']
|
|
image_meta = self._image_service.show(context, image_id)
|
|
# NOTE(sirp): extracted to a separate method to aid unit-testing, the
|
|
# new method doesn't need a request obj or an ImageService stub
|
|
kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image(
|
|
image_meta)
|
|
return kernel_id, ramdisk_id
|
|
|
|
@staticmethod
|
|
def _do_get_kernel_ramdisk_from_image(image_meta):
|
|
"""Given an ImageService image_meta, return kernel and ramdisk image
|
|
ids if present.
|
|
|
|
This is only valid for `ami` style images.
|
|
"""
|
|
image_id = image_meta['id']
|
|
if image_meta['status'] != 'active':
|
|
raise exception.ImageUnacceptable(image_id=image_id,
|
|
reason=_("status is not active"))
|
|
|
|
if image_meta.get('container_format') != 'ami':
|
|
return None, None
|
|
|
|
try:
|
|
kernel_id = image_meta['properties']['kernel_id']
|
|
except KeyError:
|
|
raise exception.KernelNotFoundForImage(image_id=image_id)
|
|
|
|
try:
|
|
ramdisk_id = image_meta['properties']['ramdisk_id']
|
|
except KeyError:
|
|
raise exception.RamdiskNotFoundForImage(image_id=image_id)
|
|
|
|
return kernel_id, ramdisk_id
|
|
|
|
def _get_injected_files(self, personality):
|
|
"""
|
|
Create a list of injected files from the personality attribute
|
|
|
|
At this time, injected_files must be formatted as a list of
|
|
(file_path, file_content) pairs for compatibility with the
|
|
underlying compute service.
|
|
"""
|
|
injected_files = []
|
|
|
|
for item in personality:
|
|
try:
|
|
path = item['path']
|
|
contents = item['contents']
|
|
except KeyError as key:
|
|
expl = _('Bad personality format: missing %s') % key
|
|
raise exc.HTTPBadRequest(explanation=expl)
|
|
except TypeError:
|
|
expl = _('Bad personality format')
|
|
raise exc.HTTPBadRequest(explanation=expl)
|
|
try:
|
|
contents = base64.b64decode(contents)
|
|
except TypeError:
|
|
expl = _('Personality content for %s cannot be decoded') % path
|
|
raise exc.HTTPBadRequest(explanation=expl)
|
|
injected_files.append((path, contents))
|
|
return injected_files
|
|
|
|
def _get_server_admin_password_old_style(self, server):
|
|
""" Determine the admin password for a server on creation """
|
|
return utils.generate_password(16)
|
|
|
|
def _get_server_admin_password_new_style(self, server):
|
|
""" Determine the admin password for a server on creation """
|
|
password = server.get('adminPass')
|
|
|
|
if password is None:
|
|
return utils.generate_password(16)
|
|
if not isinstance(password, basestring) or password == '':
|
|
msg = _("Invalid adminPass")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
return password
|
|
|
|
|
|
class ServerXMLDeserializer(wsgi.XMLDeserializer):
|
|
"""
|
|
Deserializer to handle xml-formatted server create requests.
|
|
|
|
Handles standard server attributes as well as optional metadata
|
|
and personality attributes
|
|
"""
|
|
|
|
metadata_deserializer = common.MetadataXMLDeserializer()
|
|
|
|
def create(self, string):
|
|
"""Deserialize an xml-formatted server create request"""
|
|
dom = minidom.parseString(string)
|
|
server = self._extract_server(dom)
|
|
return {'body': {'server': server}}
|
|
|
|
def _extract_server(self, node):
|
|
"""Marshal the server attribute of a parsed request"""
|
|
server = {}
|
|
server_node = self.find_first_child_named(node, 'server')
|
|
|
|
attributes = ["name", "imageId", "flavorId", "adminPass"]
|
|
for attr in attributes:
|
|
if server_node.getAttribute(attr):
|
|
server[attr] = server_node.getAttribute(attr)
|
|
|
|
metadata_node = self.find_first_child_named(server_node, "metadata")
|
|
server["metadata"] = self.metadata_deserializer.extract_metadata(
|
|
metadata_node)
|
|
|
|
server["personality"] = self._extract_personality(server_node)
|
|
|
|
return server
|
|
|
|
def _extract_personality(self, server_node):
|
|
"""Marshal the personality attribute of a parsed request"""
|
|
node = self.find_first_child_named(server_node, "personality")
|
|
personality = []
|
|
if node is not None:
|
|
for file_node in self.find_children_named(node, "file"):
|
|
item = {}
|
|
if file_node.hasAttribute("path"):
|
|
item["path"] = file_node.getAttribute("path")
|
|
item["contents"] = self.extract_text(file_node)
|
|
personality.append(item)
|
|
return personality
|
|
|
|
|
|
class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer):
|
|
"""
|
|
Deserializer to handle xml-formatted server create requests.
|
|
|
|
Handles standard server attributes as well as optional metadata
|
|
and personality attributes
|
|
"""
|
|
|
|
metadata_deserializer = common.MetadataXMLDeserializer()
|
|
|
|
def action(self, string):
|
|
dom = minidom.parseString(string)
|
|
action_node = dom.childNodes[0]
|
|
action_name = action_node.tagName
|
|
|
|
action_deserializer = {
|
|
'createImage': self._action_create_image,
|
|
'createBackup': self._action_create_backup,
|
|
'changePassword': self._action_change_password,
|
|
'reboot': self._action_reboot,
|
|
'rebuild': self._action_rebuild,
|
|
'resize': self._action_resize,
|
|
'confirmResize': self._action_confirm_resize,
|
|
'revertResize': self._action_revert_resize,
|
|
}.get(action_name, self.default)
|
|
|
|
action_data = action_deserializer(action_node)
|
|
|
|
return {'body': {action_name: action_data}}
|
|
|
|
def _action_create_image(self, node):
|
|
return self._deserialize_image_action(node, ('name',))
|
|
|
|
def _action_create_backup(self, node):
|
|
attributes = ('name', 'backup_type', 'rotation')
|
|
return self._deserialize_image_action(node, attributes)
|
|
|
|
def _action_change_password(self, node):
|
|
if not node.hasAttribute("adminPass"):
|
|
raise AttributeError("No adminPass was specified in request")
|
|
return {"adminPass": node.getAttribute("adminPass")}
|
|
|
|
def _action_reboot(self, node):
|
|
if not node.hasAttribute("type"):
|
|
raise AttributeError("No reboot type was specified in request")
|
|
return {"type": node.getAttribute("type")}
|
|
|
|
def _action_rebuild(self, node):
|
|
rebuild = {}
|
|
if node.hasAttribute("name"):
|
|
rebuild['name'] = node.getAttribute("name")
|
|
|
|
metadata_node = self.find_first_child_named(node, "metadata")
|
|
if metadata_node is not None:
|
|
rebuild["metadata"] = self.extract_metadata(metadata_node)
|
|
|
|
personality = self._extract_personality(node)
|
|
if personality is not None:
|
|
rebuild["personality"] = personality
|
|
|
|
if not node.hasAttribute("imageRef"):
|
|
raise AttributeError("No imageRef was specified in request")
|
|
rebuild["imageRef"] = node.getAttribute("imageRef")
|
|
|
|
return rebuild
|
|
|
|
def _action_resize(self, node):
|
|
if not node.hasAttribute("flavorRef"):
|
|
raise AttributeError("No flavorRef was specified in request")
|
|
return {"flavorRef": node.getAttribute("flavorRef")}
|
|
|
|
def _action_confirm_resize(self, node):
|
|
return None
|
|
|
|
def _action_revert_resize(self, node):
|
|
return None
|
|
|
|
def _deserialize_image_action(self, node, allowed_attributes):
|
|
data = {}
|
|
for attribute in allowed_attributes:
|
|
value = node.getAttribute(attribute)
|
|
if value:
|
|
data[attribute] = value
|
|
metadata_node = self.find_first_child_named(node, 'metadata')
|
|
if metadata_node is not None:
|
|
metadata = self.metadata_deserializer.extract_metadata(
|
|
metadata_node)
|
|
data['metadata'] = metadata
|
|
return data
|
|
|
|
def create(self, string):
|
|
"""Deserialize an xml-formatted server create request"""
|
|
dom = minidom.parseString(string)
|
|
server = self._extract_server(dom)
|
|
return {'body': {'server': server}}
|
|
|
|
def _extract_server(self, node):
|
|
"""Marshal the server attribute of a parsed request"""
|
|
server = {}
|
|
server_node = self.find_first_child_named(node, 'server')
|
|
|
|
attributes = ["name", "imageRef", "flavorRef", "adminPass"]
|
|
for attr in attributes:
|
|
if server_node.getAttribute(attr):
|
|
server[attr] = server_node.getAttribute(attr)
|
|
|
|
metadata_node = self.find_first_child_named(server_node, "metadata")
|
|
if metadata_node is not None:
|
|
server["metadata"] = self.extract_metadata(metadata_node)
|
|
|
|
personality = self._extract_personality(server_node)
|
|
if personality is not None:
|
|
server["personality"] = personality
|
|
|
|
return server
|
|
|
|
def _extract_personality(self, server_node):
|
|
"""Marshal the personality attribute of a parsed request"""
|
|
node = self.find_first_child_named(server_node, "personality")
|
|
if node is not None:
|
|
personality = []
|
|
for file_node in self.find_children_named(node, "file"):
|
|
item = {}
|
|
if file_node.hasAttribute("path"):
|
|
item["path"] = file_node.getAttribute("path")
|
|
item["contents"] = self.extract_text(file_node)
|
|
personality.append(item)
|
|
return personality
|
|
else:
|
|
return None
|