nova/nova/api/openstack/servers.py

731 lines
26 KiB
Python

# Copyright 2010 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
import hashlib
import traceback
from webob import exc
from xml.dom import minidom
from nova import compute
from nova import context
from nova import exception
from nova import flags
from nova import log as logging
from nova import quota
from nova import utils
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
import nova.api.openstack.views.addresses
import nova.api.openstack.views.flavors
import nova.api.openstack.views.servers
from nova.auth import manager as auth_manager
from nova.compute import instance_types
from nova.compute import power_state
import nova.api.openstack
from nova.scheduler import api as scheduler_api
LOG = logging.getLogger('server')
FLAGS = flags.FLAGS
class Controller(common.OpenstackController):
""" The Server API controller for the OpenStack API """
_serialization_metadata = {
"application/xml": {
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
"status", "progress", "adminPass", "flavorRef",
"imageRef"],
"link": ["rel", "type", "href"],
},
},
}
def __init__(self):
self.compute_api = compute.API()
self._image_service = utils.import_object(FLAGS.image_service)
super(Controller, self).__init__()
def ips(self, req, id):
try:
instance = self.compute_api.get(req.environ['nova.context'], id)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
builder = self._get_addresses_view_builder(req)
return builder.build(instance)
def index(self, req):
""" Returns a list of server names and ids for a given user """
return self._items(req, is_detail=False)
def detail(self, req):
""" Returns a list of server details for a given user """
return self._items(req, is_detail=True)
def _items(self, req, is_detail):
"""Returns a list of servers for a given user.
builder - the response model builder
"""
instance_list = self.compute_api.get_all(req.environ['nova.context'])
limited_list = self._limit_items(instance_list, req)
builder = self._get_view_builder(req)
servers = [builder.build(inst, is_detail)['server']
for inst in limited_list]
return dict(servers=servers)
@scheduler_api.redirect_handler
def show(self, req, id):
""" Returns server details by server id """
try:
instance = self.compute_api.routing_get(
req.environ['nova.context'], id)
builder = self._get_view_builder(req)
return builder.build(instance, is_detail=True)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@scheduler_api.redirect_handler
def delete(self, req, id):
""" Destroys a server """
try:
self.compute_api.delete(req.environ['nova.context'], id)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
def create(self, req):
""" Creates a new server for a given user """
env = self._deserialize_create(req)
if not env:
return faults.Fault(exc.HTTPUnprocessableEntity())
context = req.environ['nova.context']
key_name = None
key_data = None
key_pairs = auth_manager.AuthManager.get_key_pairs(context)
if key_pairs:
key_pair = key_pairs[0]
key_name = key_pair['name']
key_data = key_pair['public_key']
requested_image_id = self._image_id_from_req_data(env)
image_id = common.get_image_id_from_image_hash(self._image_service,
context, requested_image_id)
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
req, image_id)
# Metadata is a list, not a Dictionary, because we allow duplicate keys
# (even though JSON can't encode this)
# In future, we may not allow duplicate keys.
# However, the CloudServers API is not definitive on this front,
# and we want to be compatible.
metadata = []
if env['server'].get('metadata'):
for k, v in env['server']['metadata'].items():
metadata.append({'key': k, 'value': v})
personality = env['server'].get('personality')
injected_files = []
if personality:
injected_files = self._get_injected_files(personality)
flavor_id = self._flavor_id_from_req_data(env)
if not 'name' in env['server']:
msg = _("Server name is not defined")
return exc.HTTPBadRequest(msg)
name = env['server']['name']
self._validate_server_name(name)
name = name.strip()
try:
inst_type = \
instance_types.get_instance_type_by_flavor_id(flavor_id)
(inst,) = self.compute_api.create(
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=metadata,
injected_files=injected_files)
except quota.QuotaError as error:
self._handle_quota_error(error)
inst['instance_type'] = inst_type
inst['image_id'] = requested_image_id
builder = self._get_view_builder(req)
server = builder.build(inst, is_detail=True)
password = utils.generate_password(16)
server['server']['adminPass'] = password
self.compute_api.set_admin_password(context, server['server']['id'],
password)
return server
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 = ServerCreateRequestXMLDeserializer()
return deserializer.deserialize(request.body)
else:
return self._deserialize(request.body, request.get_content_type())
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 _handle_quota_error(self, error):
"""
Reraise quota errors as api-specific http exceptions
"""
if error.code == "OnsetFileLimitExceeded":
expl = _("Personality file limit exceeded")
raise exc.HTTPBadRequest(explanation=expl)
if error.code == "OnsetFilePathLimitExceeded":
expl = _("Personality file path too long")
raise exc.HTTPBadRequest(explanation=expl)
if error.code == "OnsetFileContentLimitExceeded":
expl = _("Personality file content too long")
raise exc.HTTPBadRequest(explanation=expl)
# if the original error is okay, just reraise it
raise error
@scheduler_api.redirect_handler
def update(self, req, id):
""" Updates the server name or password """
if len(req.body) == 0:
raise exc.HTTPUnprocessableEntity()
inst_dict = self._deserialize(req.body, req.get_content_type())
if not inst_dict:
return faults.Fault(exc.HTTPUnprocessableEntity())
ctxt = req.environ['nova.context']
update_dict = {}
if 'name' in inst_dict['server']:
name = inst_dict['server']['name']
self._validate_server_name(name)
update_dict['display_name'] = name.strip()
self._parse_update(ctxt, id, inst_dict, update_dict)
try:
self.compute_api.update(ctxt, id, **update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPNoContent()
def _validate_server_name(self, value):
if not isinstance(value, basestring):
msg = _("Server name is not a string or unicode")
raise exc.HTTPBadRequest(msg)
if value.strip() == '':
msg = _("Server name is an empty string")
raise exc.HTTPBadRequest(msg)
def _parse_update(self, context, id, inst_dict, update_dict):
pass
@scheduler_api.redirect_handler
def action(self, req, id):
"""Multi-purpose method used to reboot, rebuild, or
resize a server"""
actions = {
'changePassword': self._action_change_password,
'reboot': self._action_reboot,
'resize': self._action_resize,
'confirmResize': self._action_confirm_resize,
'revertResize': self._action_revert_resize,
'rebuild': self._action_rebuild,
}
input_dict = self._deserialize(req.body, req.get_content_type())
for key in actions.keys():
if key in input_dict:
return actions[key](input_dict, req, id)
return faults.Fault(exc.HTTPNotImplemented())
def _action_change_password(self, input_dict, req, id):
return exc.HTTPNotImplemented()
def _action_confirm_resize(self, input_dict, req, id):
try:
self.compute_api.confirm_resize(req.environ['nova.context'], id)
except Exception, e:
LOG.exception(_("Error in confirm-resize %s"), e)
return faults.Fault(exc.HTTPBadRequest())
return exc.HTTPNoContent()
def _action_revert_resize(self, input_dict, req, id):
try:
self.compute_api.revert_resize(req.environ['nova.context'], id)
except Exception, e:
LOG.exception(_("Error in revert-resize %s"), e)
return faults.Fault(exc.HTTPBadRequest())
return exc.HTTPAccepted()
def _action_rebuild(self, input_dict, req, id):
return faults.Fault(exc.HTTPNotImplemented())
def _action_resize(self, input_dict, req, id):
""" Resizes a given instance to the flavor size requested """
try:
if 'resize' in input_dict and 'flavorId' in input_dict['resize']:
flavor_id = input_dict['resize']['flavorId']
self.compute_api.resize(req.environ['nova.context'], id,
flavor_id)
else:
LOG.exception(_("Missing arguments for resize"))
return faults.Fault(exc.HTTPUnprocessableEntity())
except Exception, e:
LOG.exception(_("Error in resize %s"), e)
return faults.Fault(exc.HTTPBadRequest())
return faults.Fault(exc.HTTPAccepted())
def _action_reboot(self, input_dict, req, id):
try:
reboot_type = input_dict['reboot']['type']
except Exception:
raise faults.Fault(exc.HTTPNotImplemented())
try:
# TODO(gundlach): pass reboot_type, support soft reboot in
# virt driver
self.compute_api.reboot(req.environ['nova.context'], id)
except:
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def lock(self, req, id):
"""
lock the instance with id
admin only operation
"""
context = req.environ['nova.context']
try:
self.compute_api.lock(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::lock %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def unlock(self, req, id):
"""
unlock the instance with id
admin only operation
"""
context = req.environ['nova.context']
try:
self.compute_api.unlock(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unlock %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def get_lock(self, req, id):
"""
return the boolean state of (instance with id)'s lock
"""
context = req.environ['nova.context']
try:
self.compute_api.get_lock(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::get_lock %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def reset_network(self, req, id):
"""
Reset networking on an instance (admin only).
"""
context = req.environ['nova.context']
try:
self.compute_api.reset_network(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::reset_network %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def inject_network_info(self, req, id):
"""
Inject network info for an instance (admin only).
"""
context = req.environ['nova.context']
try:
self.compute_api.inject_network_info(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::inject_network_info %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def pause(self, req, id):
""" Permit Admins to Pause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.pause(ctxt, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::pause %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def unpause(self, req, id):
""" Permit Admins to Unpause the server. """
ctxt = req.environ['nova.context']
try:
self.compute_api.unpause(ctxt, id)
except:
readable = traceback.format_exc()
LOG.exception(_("Compute.api::unpause %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def suspend(self, req, id):
"""permit admins to suspend the server"""
context = req.environ['nova.context']
try:
self.compute_api.suspend(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::suspend %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def resume(self, req, id):
"""permit admins to resume the server from suspend"""
context = req.environ['nova.context']
try:
self.compute_api.resume(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::resume %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def rescue(self, req, id):
"""Permit users to rescue the server."""
context = req.environ["nova.context"]
try:
self.compute_api.rescue(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::rescue %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def unrescue(self, req, id):
"""Permit users to unrescue the server."""
context = req.environ["nova.context"]
try:
self.compute_api.unrescue(context, id)
except:
readable = traceback.format_exc()
LOG.exception(_("compute.api::unrescue %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def get_ajax_console(self, req, id):
"""Returns a url to an instance's ajaxterm console."""
try:
self.compute_api.get_ajax_console(req.environ['nova.context'],
int(id))
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def get_vnc_console(self, req, id):
"""Returns a url to an instance's ajaxterm console."""
try:
self.compute_api.get_vnc_console(req.environ['nova.context'],
int(id))
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
@scheduler_api.redirect_handler
def diagnostics(self, req, id):
"""Permit Admins to retrieve server diagnostics."""
ctxt = req.environ["nova.context"]
return self.compute_api.get_diagnostics(ctxt, id)
def actions(self, req, id):
"""Permit Admins to retrieve server actions."""
ctxt = req.environ["nova.context"]
items = self.compute_api.get_actions(ctxt, id)
actions = []
# TODO(jk0): Do not do pre-serialization here once the default
# serializer is updated
for item in items:
actions.append(dict(
created_at=str(item.created_at),
action=item.action,
error=item.error))
return dict(actions=actions)
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.Invalid(
_("Cannot build from image %(image_id)s, status not active") %
locals())
if image_meta.get('container_format') != 'ami':
return None, None
try:
kernel_id = image_meta['properties']['kernel_id']
except KeyError:
raise exception.NotFound(
_("Kernel not found for image %(image_id)s") % locals())
try:
ramdisk_id = image_meta['properties']['ramdisk_id']
except KeyError:
raise exception.NotFound(
_("Ramdisk not found for image %(image_id)s") % locals())
return kernel_id, ramdisk_id
class ControllerV10(Controller):
def _image_id_from_req_data(self, data):
return data['server']['imageId']
def _flavor_id_from_req_data(self, data):
return data['server']['flavorId']
def _get_view_builder(self, req):
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
return nova.api.openstack.views.servers.ViewBuilderV10(
addresses_builder)
def _get_addresses_view_builder(self, req):
return nova.api.openstack.views.addresses.ViewBuilderV10(req)
def _limit_items(self, items, req):
return common.limited(items, req)
def _parse_update(self, context, server_id, inst_dict, update_dict):
if 'adminPass' in inst_dict['server']:
update_dict['admin_pass'] = inst_dict['server']['adminPass']
try:
self.compute_api.set_admin_password(context, server_id)
except exception.TimeoutException:
return exc.HTTPRequestTimeout()
class ControllerV11(Controller):
def _image_id_from_req_data(self, data):
href = data['server']['imageRef']
return common.get_id_from_href(href)
def _flavor_id_from_req_data(self, data):
href = data['server']['flavorRef']
return common.get_id_from_href(href)
def _get_view_builder(self, req):
base_url = req.application_url
flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
base_url)
image_builder = nova.api.openstack.views.images.ViewBuilderV11(
base_url)
addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
return nova.api.openstack.views.servers.ViewBuilderV11(
addresses_builder, flavor_builder, image_builder, base_url)
def _get_addresses_view_builder(self, req):
return nova.api.openstack.views.addresses.ViewBuilderV11(req)
def _action_change_password(self, input_dict, req, id):
context = req.environ['nova.context']
if (not 'changePassword' in input_dict
or not 'adminPass' in input_dict['changePassword']):
msg = _("No adminPass was specified")
return exc.HTTPBadRequest(msg)
password = input_dict['changePassword']['adminPass']
if not isinstance(password, basestring) or password == '':
msg = _("Invalid adminPass")
return exc.HTTPBadRequest(msg)
self.compute_api.set_admin_password(context, id, password)
return exc.HTTPAccepted()
def _limit_items(self, items, req):
return common.limited_by_marker(items, req)
def get_default_xmlns(self, req):
return common.XML_NS_V11
class ServerCreateRequestXMLDeserializer(object):
"""
Deserializer to handle xml-formatted server create requests.
Handles standard server attributes as well as optional metadata
and personality attributes
"""
def deserialize(self, string):
"""Deserialize an xml-formatted server create request"""
dom = minidom.parseString(string)
server = self._extract_server(dom)
return {'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')
for attr in ["name", "imageId", "flavorId"]:
server[attr] = server_node.getAttribute(attr)
metadata = self._extract_metadata(server_node)
if metadata is not None:
server["metadata"] = metadata
personality = self._extract_personality(server_node)
if personality is not None:
server["personality"] = personality
return server
def _extract_metadata(self, server_node):
"""Marshal the metadata attribute of a parsed request"""
metadata_node = self._find_first_child_named(server_node, "metadata")
if metadata_node is None:
return None
metadata = {}
for meta_node in self._find_children_named(metadata_node, "meta"):
key = meta_node.getAttribute("key")
metadata[key] = self._extract_text(meta_node)
return metadata
def _extract_personality(self, server_node):
"""Marshal the personality attribute of a parsed request"""
personality_node = \
self._find_first_child_named(server_node, "personality")
if personality_node is None:
return None
personality = []
for file_node in self._find_children_named(personality_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
def _find_first_child_named(self, parent, name):
"""Search a nodes children for the first child with a given name"""
for node in parent.childNodes:
if node.nodeName == name:
return node
return None
def _find_children_named(self, parent, name):
"""Return all of a nodes children who have the given name"""
for node in parent.childNodes:
if node.nodeName == name:
yield node
def _extract_text(self, node):
"""Get the text field contained by the given node"""
if len(node.childNodes) == 1:
child = node.childNodes[0]
if child.nodeType == child.TEXT_NODE:
return child.nodeValue
return ""