999 lines
36 KiB
Python
999 lines
36 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 os
|
|
import traceback
|
|
|
|
from webob import exc
|
|
from xml.dom import minidom
|
|
import webob
|
|
|
|
from nova import compute
|
|
from nova import exception
|
|
from nova import flags
|
|
from nova import log as logging
|
|
from nova import utils
|
|
from nova.api.openstack import common
|
|
from nova.api.openstack import create_instance_helper as helper
|
|
from nova.api.openstack import ips
|
|
from nova.api.openstack import wsgi
|
|
from nova.compute import instance_types
|
|
from nova.scheduler import api as scheduler_api
|
|
import nova.api.openstack
|
|
import nova.api.openstack.views.addresses
|
|
import nova.api.openstack.views.flavors
|
|
import nova.api.openstack.views.images
|
|
import nova.api.openstack.views.servers
|
|
|
|
|
|
LOG = logging.getLogger('nova.api.openstack.servers')
|
|
FLAGS = flags.FLAGS
|
|
|
|
|
|
class Controller(object):
|
|
""" The Server API base controller class for the OpenStack API """
|
|
|
|
def __init__(self):
|
|
self.compute_api = compute.API()
|
|
self.helper = helper.CreateInstanceHelper(self)
|
|
|
|
def index(self, req):
|
|
""" Returns a list of server names and ids for a given user """
|
|
try:
|
|
servers = self._get_servers(req, is_detail=False)
|
|
except exception.Invalid as err:
|
|
return exc.HTTPBadRequest(explanation=str(err))
|
|
except exception.NotFound:
|
|
return exc.HTTPNotFound()
|
|
return servers
|
|
|
|
def detail(self, req):
|
|
""" Returns a list of server details for a given user """
|
|
try:
|
|
servers = self._get_servers(req, is_detail=True)
|
|
except exception.Invalid as err:
|
|
return exc.HTTPBadRequest(explanation=str(err))
|
|
except exception.NotFound as err:
|
|
return exc.HTTPNotFound()
|
|
return servers
|
|
|
|
def _build_view(self, req, instance, is_detail=False):
|
|
raise NotImplementedError()
|
|
|
|
def _limit_items(self, items, req):
|
|
raise NotImplementedError()
|
|
|
|
def _action_rebuild(self, info, request, instance_id):
|
|
raise NotImplementedError()
|
|
|
|
def _get_servers(self, req, is_detail):
|
|
"""Returns a list of servers, taking into account any search
|
|
options specified.
|
|
"""
|
|
|
|
search_opts = {}
|
|
search_opts.update(req.str_GET)
|
|
|
|
context = req.environ['nova.context']
|
|
remove_invalid_options(context, search_opts,
|
|
self._get_server_search_options())
|
|
|
|
# Convert recurse_zones into a boolean
|
|
search_opts['recurse_zones'] = utils.bool_from_str(
|
|
search_opts.get('recurse_zones', False))
|
|
|
|
# If search by 'status', we need to convert it to 'state'
|
|
# If the status is unknown, bail.
|
|
# Leave 'state' in search_opts so compute can pass it on to
|
|
# child zones..
|
|
if 'status' in search_opts:
|
|
status = search_opts['status']
|
|
search_opts['state'] = common.power_states_from_status(status)
|
|
if len(search_opts['state']) == 0:
|
|
reason = _('Invalid server status: %(status)s') % locals()
|
|
LOG.error(reason)
|
|
raise exception.InvalidInput(reason=reason)
|
|
|
|
# By default, compute's get_all() will return deleted instances.
|
|
# If an admin hasn't specified a 'deleted' search option, we need
|
|
# to filter out deleted instances by setting the filter ourselves.
|
|
# ... Unless 'changes-since' is specified, because 'changes-since'
|
|
# should return recently deleted images according to the API spec.
|
|
|
|
if 'deleted' not in search_opts:
|
|
# Admin hasn't specified deleted filter
|
|
if 'changes-since' not in search_opts:
|
|
# No 'changes-since', so we need to find non-deleted servers
|
|
search_opts['deleted'] = False
|
|
else:
|
|
# This is the default, but just in case..
|
|
search_opts['deleted'] = True
|
|
|
|
instance_list = self.compute_api.get_all(
|
|
context, search_opts=search_opts)
|
|
|
|
# FIXME(comstud): 'changes-since' is not fully implemented. Where
|
|
# should this be filtered?
|
|
|
|
limited_list = self._limit_items(instance_list, req)
|
|
servers = [self._build_view(req, 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)
|
|
return self._build_view(req, instance, is_detail=True)
|
|
except exception.NotFound:
|
|
raise exc.HTTPNotFound()
|
|
|
|
def create(self, req, body):
|
|
""" Creates a new server for a given user """
|
|
extra_values = None
|
|
result = None
|
|
extra_values, instances = self.helper.create_instance(
|
|
req, body, self.compute_api.create)
|
|
|
|
# We can only return 1 instance via the API, if we happen to
|
|
# build more than one... instances is a list, so we'll just
|
|
# use the first one..
|
|
inst = instances[0]
|
|
for key in ['instance_type', 'image_ref']:
|
|
inst[key] = extra_values[key]
|
|
|
|
server = self._build_view(req, inst, is_detail=True)
|
|
server['server']['adminPass'] = extra_values['password']
|
|
return server
|
|
|
|
@scheduler_api.redirect_handler
|
|
def update(self, req, id, body):
|
|
"""Update server name then pass on to version-specific controller"""
|
|
if len(req.body) == 0:
|
|
raise exc.HTTPUnprocessableEntity()
|
|
|
|
if not body:
|
|
raise exc.HTTPUnprocessableEntity()
|
|
|
|
ctxt = req.environ['nova.context']
|
|
update_dict = {}
|
|
|
|
if 'name' in body['server']:
|
|
name = body['server']['name']
|
|
self.helper._validate_server_name(name)
|
|
update_dict['display_name'] = name.strip()
|
|
|
|
try:
|
|
self.compute_api.update(ctxt, id, **update_dict)
|
|
except exception.NotFound:
|
|
raise exc.HTTPNotFound()
|
|
|
|
return self._update(ctxt, req, id, body)
|
|
|
|
def _update(self, context, req, id, inst_dict):
|
|
return exc.HTTPNotImplemented()
|
|
|
|
@scheduler_api.redirect_handler
|
|
def action(self, req, id, body):
|
|
"""Multi-purpose method used to take actions on a server"""
|
|
|
|
self.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,
|
|
'createImage': self._action_create_image,
|
|
}
|
|
|
|
if FLAGS.allow_admin_api:
|
|
admin_actions = {
|
|
'createBackup': self._action_create_backup,
|
|
}
|
|
self.actions.update(admin_actions)
|
|
|
|
for key in body:
|
|
if key in self.actions:
|
|
return self.actions[key](body, req, id)
|
|
else:
|
|
msg = _("There is no such server action: %s") % (key,)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
msg = _("Invalid request body")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
def _action_create_backup(self, input_dict, req, instance_id):
|
|
"""Backup a server instance.
|
|
|
|
Images now have an `image_type` associated with them, which can be
|
|
'snapshot' or the backup type, like 'daily' or 'weekly'.
|
|
|
|
If the image_type is backup-like, then the rotation factor can be
|
|
included and that will cause the oldest backups that exceed the
|
|
rotation factor to be deleted.
|
|
|
|
"""
|
|
entity = input_dict["createBackup"]
|
|
|
|
try:
|
|
image_name = entity["name"]
|
|
backup_type = entity["backup_type"]
|
|
rotation = entity["rotation"]
|
|
|
|
except KeyError as missing_key:
|
|
msg = _("createBackup entity requires %s attribute") % missing_key
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
except TypeError:
|
|
msg = _("Malformed createBackup entity")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
try:
|
|
rotation = int(rotation)
|
|
except ValueError:
|
|
msg = _("createBackup attribute 'rotation' must be an integer")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
# preserve link to server in image properties
|
|
server_ref = os.path.join(req.application_url,
|
|
'servers',
|
|
str(instance_id))
|
|
props = {'instance_ref': server_ref}
|
|
|
|
metadata = entity.get('metadata', {})
|
|
context = req.environ["nova.context"]
|
|
common.check_img_metadata_quota_limit(context, metadata)
|
|
try:
|
|
props.update(metadata)
|
|
except ValueError:
|
|
msg = _("Invalid metadata")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
image = self.compute_api.backup(context,
|
|
instance_id,
|
|
image_name,
|
|
backup_type,
|
|
rotation,
|
|
extra_properties=props)
|
|
|
|
# build location of newly-created image entity
|
|
image_id = str(image['id'])
|
|
image_ref = os.path.join(req.application_url, 'images', image_id)
|
|
|
|
resp = webob.Response(status_int=202)
|
|
resp.headers['Location'] = image_ref
|
|
return resp
|
|
|
|
@common.check_snapshots_enabled
|
|
def _action_create_image(self, input_dict, req, id):
|
|
return 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)
|
|
raise 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)
|
|
raise exc.HTTPBadRequest()
|
|
return webob.Response(status_int=202)
|
|
|
|
def _action_resize(self, input_dict, req, id):
|
|
return exc.HTTPNotImplemented()
|
|
|
|
def _action_reboot(self, input_dict, req, id):
|
|
if 'reboot' in input_dict and 'type' in input_dict['reboot']:
|
|
valid_reboot_types = ['HARD', 'SOFT']
|
|
reboot_type = input_dict['reboot']['type'].upper()
|
|
if not valid_reboot_types.count(reboot_type):
|
|
msg = _("Argument 'type' for reboot is not HARD or SOFT")
|
|
LOG.exception(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
else:
|
|
msg = _("Missing argument 'type' for reboot")
|
|
LOG.exception(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
try:
|
|
# TODO(gundlach): pass reboot_type, support soft reboot in
|
|
# virt driver
|
|
self.compute_api.reboot(req.environ['nova.context'], id)
|
|
except Exception, e:
|
|
LOG.exception(_("Error in reboot %s"), e)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::lock %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::unlock %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::get_lock %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::reset_network %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::inject_network_info %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::pause %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("Compute.api::unpause %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("compute.api::suspend %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("compute.api::resume %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@scheduler_api.redirect_handler
|
|
def migrate(self, req, id):
|
|
try:
|
|
self.compute_api.resize(req.environ['nova.context'], id)
|
|
except Exception, e:
|
|
LOG.exception(_("Error in migrate %s"), e)
|
|
raise exc.HTTPBadRequest()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("compute.api::rescue %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 Exception:
|
|
readable = traceback.format_exc()
|
|
LOG.exception(_("compute.api::unrescue %s"), readable)
|
|
raise exc.HTTPUnprocessableEntity()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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:
|
|
raise exc.HTTPNotFound()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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:
|
|
raise exc.HTTPNotFound()
|
|
return webob.Response(status_int=202)
|
|
|
|
@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 resize(self, req, instance_id, flavor_id):
|
|
"""Begin the resize process with given instance/flavor."""
|
|
context = req.environ["nova.context"]
|
|
|
|
try:
|
|
self.compute_api.resize(context, instance_id, flavor_id)
|
|
except exception.FlavorNotFound:
|
|
msg = _("Unable to locate requested flavor.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
except exception.CannotResizeToSameSize:
|
|
msg = _("Resize requires a change in size.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
except exception.CannotResizeToSmallerSize:
|
|
msg = _("Resizing to a smaller size is not supported.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
|
|
class ControllerV10(Controller):
|
|
"""v1.0 OpenStack API controller"""
|
|
|
|
@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:
|
|
raise exc.HTTPNotFound()
|
|
return webob.Response(status_int=202)
|
|
|
|
def _image_ref_from_req_data(self, data):
|
|
return data['server']['imageId']
|
|
|
|
def _flavor_id_from_req_data(self, data):
|
|
return data['server']['flavorId']
|
|
|
|
def _build_view(self, req, instance, is_detail=False):
|
|
addresses = nova.api.openstack.views.addresses.ViewBuilderV10()
|
|
builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
|
|
return builder.build(instance, is_detail=is_detail)
|
|
|
|
def _limit_items(self, items, req):
|
|
return common.limited(items, req)
|
|
|
|
def _update(self, context, req, id, inst_dict):
|
|
if 'adminPass' in inst_dict['server']:
|
|
self.compute_api.set_admin_password(context, id,
|
|
inst_dict['server']['adminPass'])
|
|
return exc.HTTPNoContent()
|
|
|
|
def _action_resize(self, input_dict, req, id):
|
|
""" Resizes a given instance to the flavor size requested """
|
|
try:
|
|
flavor_id = input_dict["resize"]["flavorId"]
|
|
except (KeyError, TypeError):
|
|
msg = _("Resize requests require 'flavorId' attribute.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
return self.resize(req, id, flavor_id)
|
|
|
|
def _action_rebuild(self, info, request, instance_id):
|
|
context = request.environ['nova.context']
|
|
|
|
try:
|
|
image_id = info["rebuild"]["imageId"]
|
|
except (KeyError, TypeError):
|
|
msg = _("Could not parse imageId from request.")
|
|
LOG.debug(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
try:
|
|
self.compute_api.rebuild(context, instance_id, image_id)
|
|
except exception.BuildInProgress:
|
|
msg = _("Instance %s is currently being rebuilt.") % instance_id
|
|
LOG.debug(msg)
|
|
raise exc.HTTPConflict(explanation=msg)
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
def _get_server_admin_password(self, server):
|
|
""" Determine the admin password for a server on creation """
|
|
return self.helper._get_server_admin_password_old_style(server)
|
|
|
|
def _get_server_search_options(self):
|
|
"""Return server search options allowed by non-admin"""
|
|
return 'reservation_id', 'fixed_ip', 'name', 'recurse_zones'
|
|
|
|
|
|
class ControllerV11(Controller):
|
|
"""v1.1 OpenStack API controller"""
|
|
|
|
@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:
|
|
raise exc.HTTPNotFound()
|
|
|
|
def _image_ref_from_req_data(self, data):
|
|
try:
|
|
return data['server']['imageRef']
|
|
except (TypeError, KeyError):
|
|
msg = _("Missing imageRef attribute")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
def _flavor_id_from_req_data(self, data):
|
|
try:
|
|
flavor_ref = data['server']['flavorRef']
|
|
except (TypeError, KeyError):
|
|
msg = _("Missing flavorRef attribute")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
return common.get_id_from_href(flavor_ref)
|
|
|
|
def _build_view(self, req, instance, is_detail=False):
|
|
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()
|
|
builder = nova.api.openstack.views.servers.ViewBuilderV11(
|
|
addresses_builder, flavor_builder, image_builder, base_url)
|
|
|
|
return builder.build(instance, is_detail=is_detail)
|
|
|
|
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(explanation=msg)
|
|
password = input_dict['changePassword']['adminPass']
|
|
if not isinstance(password, basestring) or password == '':
|
|
msg = _("Invalid adminPass")
|
|
return exc.HTTPBadRequest(explanation=msg)
|
|
self.compute_api.set_admin_password(context, id, password)
|
|
return webob.Response(status_int=202)
|
|
|
|
def _limit_items(self, items, req):
|
|
return common.limited_by_marker(items, req)
|
|
|
|
def _validate_metadata(self, metadata):
|
|
"""Ensure that we can work with the metadata given."""
|
|
try:
|
|
metadata.iteritems()
|
|
except AttributeError as ex:
|
|
msg = _("Unable to parse metadata key/value pairs.")
|
|
LOG.debug(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
def _decode_personalities(self, personalities):
|
|
"""Decode the Base64-encoded personalities."""
|
|
for personality in personalities:
|
|
try:
|
|
path = personality["path"]
|
|
contents = personality["contents"]
|
|
except (KeyError, TypeError):
|
|
msg = _("Unable to parse personality path/contents.")
|
|
LOG.info(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
try:
|
|
personality["contents"] = base64.b64decode(contents)
|
|
except TypeError:
|
|
msg = _("Personality content could not be Base64 decoded.")
|
|
LOG.info(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
def _update(self, context, req, id, inst_dict):
|
|
instance = self.compute_api.routing_get(context, id)
|
|
return self._build_view(req, instance, is_detail=True)
|
|
|
|
def _action_resize(self, input_dict, req, id):
|
|
""" Resizes a given instance to the flavor size requested """
|
|
try:
|
|
flavor_ref = input_dict["resize"]["flavorRef"]
|
|
if not flavor_ref:
|
|
msg = _("Resize request has invalid 'flavorRef' attribute.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
except (KeyError, TypeError):
|
|
msg = _("Resize requests require 'flavorRef' attribute.")
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
return self.resize(req, id, flavor_ref)
|
|
|
|
def _action_rebuild(self, info, request, instance_id):
|
|
context = request.environ['nova.context']
|
|
|
|
try:
|
|
image_href = info["rebuild"]["imageRef"]
|
|
except (KeyError, TypeError):
|
|
msg = _("Could not parse imageRef from request.")
|
|
LOG.debug(msg)
|
|
raise exc.HTTPBadRequest(explanation=msg)
|
|
|
|
personalities = info["rebuild"].get("personality", [])
|
|
metadata = info["rebuild"].get("metadata")
|
|
name = info["rebuild"].get("name")
|
|
|
|
if metadata:
|
|
self._validate_metadata(metadata)
|
|
self._decode_personalities(personalities)
|
|
|
|
try:
|
|
self.compute_api.rebuild(context, instance_id, image_href, name,
|
|
metadata, personalities)
|
|
except exception.BuildInProgress:
|
|
msg = _("Instance %s is currently being rebuilt.") % instance_id
|
|
LOG.debug(msg)
|
|
raise exc.HTTPConflict(explanation=msg)
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
@common.check_snapshots_enabled
|
|
def _action_create_image(self, input_dict, req, instance_id):
|
|
"""Snapshot a server instance."""
|
|
entity = input_dict.get("createImage", {})
|
|
|
|
try:
|
|
image_name = entity["name"]
|
|
|
|
except KeyError:
|
|
msg = _("createImage entity requires name attribute")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
except TypeError:
|
|
msg = _("Malformed createImage entity")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
# preserve link to server in image properties
|
|
server_ref = os.path.join(req.application_url,
|
|
'servers',
|
|
str(instance_id))
|
|
props = {'instance_ref': server_ref}
|
|
|
|
metadata = entity.get('metadata', {})
|
|
context = req.environ['nova.context']
|
|
common.check_img_metadata_quota_limit(context, metadata)
|
|
try:
|
|
props.update(metadata)
|
|
except ValueError:
|
|
msg = _("Invalid metadata")
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
|
|
image = self.compute_api.snapshot(context,
|
|
instance_id,
|
|
image_name,
|
|
extra_properties=props)
|
|
|
|
# build location of newly-created image entity
|
|
image_id = str(image['id'])
|
|
image_ref = os.path.join(req.application_url, 'images', image_id)
|
|
|
|
resp = webob.Response(status_int=202)
|
|
resp.headers['Location'] = image_ref
|
|
return resp
|
|
|
|
def get_default_xmlns(self, req):
|
|
return common.XML_NS_V11
|
|
|
|
def _get_server_admin_password(self, server):
|
|
""" Determine the admin password for a server on creation """
|
|
return self.helper._get_server_admin_password_new_style(server)
|
|
|
|
def _get_server_search_options(self):
|
|
"""Return server search options allowed by non-admin"""
|
|
return ('reservation_id', 'name', 'recurse_zones',
|
|
'status', 'image', 'flavor', 'changes-since')
|
|
|
|
|
|
class HeadersSerializer(wsgi.ResponseHeadersSerializer):
|
|
|
|
def create(self, response, data):
|
|
response.status_int = 202
|
|
|
|
def delete(self, response, data):
|
|
response.status_int = 204
|
|
|
|
|
|
class ServerXMLSerializer(wsgi.XMLDictSerializer):
|
|
|
|
xmlns = wsgi.XMLNS_V11
|
|
|
|
def __init__(self):
|
|
self.metadata_serializer = common.MetadataXMLSerializer()
|
|
self.addresses_serializer = ips.IPXMLSerializer()
|
|
|
|
def _create_basic_entity_node(self, xml_doc, id, links, name):
|
|
basic_node = xml_doc.createElement(name)
|
|
basic_node.setAttribute('id', str(id))
|
|
link_nodes = self._create_link_nodes(xml_doc, links)
|
|
for link_node in link_nodes:
|
|
basic_node.appendChild(link_node)
|
|
return basic_node
|
|
|
|
def _create_metadata_node(self, xml_doc, metadata):
|
|
return self.metadata_serializer.meta_list_to_xml(xml_doc, metadata)
|
|
|
|
def _create_addresses_node(self, xml_doc, addresses):
|
|
return self.addresses_serializer.networks_to_xml(xml_doc, addresses)
|
|
|
|
def _add_server_attributes(self, node, server):
|
|
node.setAttribute('id', str(server['id']))
|
|
node.setAttribute('uuid', str(server['uuid']))
|
|
node.setAttribute('hostId', str(server['hostId']))
|
|
node.setAttribute('name', server['name'])
|
|
node.setAttribute('created', str(server['created']))
|
|
node.setAttribute('updated', str(server['updated']))
|
|
node.setAttribute('status', server['status'])
|
|
if 'progress' in server:
|
|
node.setAttribute('progress', str(server['progress']))
|
|
|
|
def _server_to_xml(self, xml_doc, server):
|
|
server_node = xml_doc.createElement('server')
|
|
server_node.setAttribute('id', str(server['id']))
|
|
server_node.setAttribute('name', server['name'])
|
|
link_nodes = self._create_link_nodes(xml_doc,
|
|
server['links'])
|
|
for link_node in link_nodes:
|
|
server_node.appendChild(link_node)
|
|
return server_node
|
|
|
|
def _server_to_xml_detailed(self, xml_doc, server):
|
|
server_node = xml_doc.createElement('server')
|
|
self._add_server_attributes(server_node, server)
|
|
|
|
link_nodes = self._create_link_nodes(xml_doc,
|
|
server['links'])
|
|
for link_node in link_nodes:
|
|
server_node.appendChild(link_node)
|
|
|
|
if 'image' in server:
|
|
image_node = self._create_basic_entity_node(xml_doc,
|
|
server['image']['id'],
|
|
server['image']['links'],
|
|
'image')
|
|
server_node.appendChild(image_node)
|
|
|
|
if 'flavor' in server:
|
|
flavor_node = self._create_basic_entity_node(xml_doc,
|
|
server['flavor']['id'],
|
|
server['flavor']['links'],
|
|
'flavor')
|
|
server_node.appendChild(flavor_node)
|
|
|
|
metadata = server.get('metadata', {}).items()
|
|
if len(metadata) > 0:
|
|
metadata_node = self._create_metadata_node(xml_doc, metadata)
|
|
server_node.appendChild(metadata_node)
|
|
|
|
addresses_node = self._create_addresses_node(xml_doc,
|
|
server['addresses'])
|
|
server_node.appendChild(addresses_node)
|
|
|
|
return server_node
|
|
|
|
def _server_list_to_xml(self, xml_doc, servers, detailed):
|
|
container_node = xml_doc.createElement('servers')
|
|
if detailed:
|
|
server_to_xml = self._server_to_xml_detailed
|
|
else:
|
|
server_to_xml = self._server_to_xml
|
|
|
|
for server in servers:
|
|
item_node = server_to_xml(xml_doc, server)
|
|
container_node.appendChild(item_node)
|
|
return container_node
|
|
|
|
def index(self, servers_dict):
|
|
xml_doc = minidom.Document()
|
|
node = self._server_list_to_xml(xml_doc,
|
|
servers_dict['servers'],
|
|
detailed=False)
|
|
return self.to_xml_string(node, True)
|
|
|
|
def detail(self, servers_dict):
|
|
xml_doc = minidom.Document()
|
|
node = self._server_list_to_xml(xml_doc,
|
|
servers_dict['servers'],
|
|
detailed=True)
|
|
return self.to_xml_string(node, True)
|
|
|
|
def show(self, server_dict):
|
|
xml_doc = minidom.Document()
|
|
node = self._server_to_xml_detailed(xml_doc,
|
|
server_dict['server'])
|
|
return self.to_xml_string(node, True)
|
|
|
|
def create(self, server_dict):
|
|
xml_doc = minidom.Document()
|
|
node = self._server_to_xml_detailed(xml_doc,
|
|
server_dict['server'])
|
|
node.setAttribute('adminPass', server_dict['server']['adminPass'])
|
|
return self.to_xml_string(node, True)
|
|
|
|
def update(self, server_dict):
|
|
xml_doc = minidom.Document()
|
|
node = self._server_to_xml_detailed(xml_doc,
|
|
server_dict['server'])
|
|
return self.to_xml_string(node, True)
|
|
|
|
|
|
def create_resource(version='1.0'):
|
|
controller = {
|
|
'1.0': ControllerV10,
|
|
'1.1': ControllerV11,
|
|
}[version]()
|
|
|
|
metadata = {
|
|
"attributes": {
|
|
"server": ["id", "imageId", "name", "flavorId", "hostId",
|
|
"status", "progress", "adminPass", "flavorRef",
|
|
"imageRef"],
|
|
"link": ["rel", "type", "href"],
|
|
},
|
|
"dict_collections": {
|
|
"metadata": {"item_name": "meta", "item_key": "key"},
|
|
},
|
|
"list_collections": {
|
|
"public": {"item_name": "ip", "item_key": "addr"},
|
|
"private": {"item_name": "ip", "item_key": "addr"},
|
|
},
|
|
}
|
|
|
|
xmlns = {
|
|
'1.0': wsgi.XMLNS_V10,
|
|
'1.1': wsgi.XMLNS_V11,
|
|
}[version]
|
|
|
|
headers_serializer = HeadersSerializer()
|
|
|
|
xml_serializer = {
|
|
'1.0': wsgi.XMLDictSerializer(metadata, wsgi.XMLNS_V10),
|
|
'1.1': ServerXMLSerializer(),
|
|
}[version]
|
|
|
|
body_serializers = {
|
|
'application/xml': xml_serializer,
|
|
}
|
|
|
|
xml_deserializer = {
|
|
'1.0': helper.ServerXMLDeserializer(),
|
|
'1.1': helper.ServerXMLDeserializerV11(),
|
|
}[version]
|
|
|
|
body_deserializers = {
|
|
'application/xml': xml_deserializer,
|
|
}
|
|
|
|
serializer = wsgi.ResponseSerializer(body_serializers, headers_serializer)
|
|
deserializer = wsgi.RequestDeserializer(body_deserializers)
|
|
|
|
return wsgi.Resource(controller, deserializer, serializer)
|
|
|
|
|
|
def remove_invalid_options(context, search_options, allowed_search_options):
|
|
"""Remove search options that are not valid for non-admin API/context"""
|
|
if FLAGS.allow_admin_api and context.is_admin:
|
|
# Allow all options
|
|
return
|
|
# Otherwise, strip out all unknown options
|
|
unknown_options = [opt for opt in search_options
|
|
if opt not in allowed_search_options]
|
|
unk_opt_str = ", ".join(unknown_options)
|
|
log_msg = _("Removing options '%(unk_opt_str)s' from query") % locals()
|
|
LOG.debug(log_msg)
|
|
for opt in unknown_options:
|
|
search_options.pop(opt, None)
|