summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrey Morris <trey.morris@rackspace.com>2011-06-16 14:03:40 -0500
committerTrey Morris <trey.morris@rackspace.com>2011-06-16 14:03:40 -0500
commit728a5f877b2ff0b04260187c25673ddd6bed085b (patch)
treeac0cddb3a979e74d72f225faedff70a9fa796a49
parent09dc2c32e18692c2e3d3743d126a52dd73cf598d (diff)
parent7c583997df2c243c4bd21f876da4658331efe37d (diff)
merged trunk
-rw-r--r--nova/api/openstack/__init__.py71
-rw-r--r--nova/api/openstack/common.py2
-rw-r--r--nova/api/openstack/create_instance_helper.py346
-rw-r--r--nova/api/openstack/servers.py287
-rw-r--r--nova/api/openstack/views/servers.py12
-rw-r--r--nova/api/openstack/wsgi.py27
-rw-r--r--nova/api/openstack/zones.py68
-rw-r--r--nova/compute/api.py47
-rw-r--r--nova/db/sqlalchemy/api.py6
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/023_add_vm_mode_to_instances.py45
-rw-r--r--nova/db/sqlalchemy/models.py1
-rw-r--r--nova/exception.py4
-rw-r--r--nova/scheduler/api.py18
-rw-r--r--nova/scheduler/manager.py4
-rw-r--r--nova/scheduler/zone_aware_scheduler.py19
-rw-r--r--nova/scheduler/zone_manager.py3
-rw-r--r--nova/tests/api/openstack/test_api.py21
-rw-r--r--nova/tests/api/openstack/test_servers.py146
-rw-r--r--nova/tests/scheduler/test_scheduler.py8
-rw-r--r--nova/virt/xenapi/vmops.py21
-rw-r--r--plugins/xenserver/xenapi/etc/xapi.d/plugins/migration2
-rw-r--r--run_tests.py90
-rwxr-xr-xrun_tests.sh10
-rw-r--r--tools/pip-requires2
24 files changed, 880 insertions, 380 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index c116e4220..ddd9580d7 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -81,7 +81,9 @@ class APIRouter(base_wsgi.Router):
self._setup_routes(mapper)
super(APIRouter, self).__init__(mapper)
- def _setup_routes(self, mapper):
+ def _setup_routes(self, mapper, version):
+ """Routes common to all versions."""
+
server_members = self.server_members
server_members['action'] = 'POST'
if FLAGS.allow_admin_api:
@@ -98,11 +100,6 @@ class APIRouter(base_wsgi.Router):
server_members['reset_network'] = 'POST'
server_members['inject_network_info'] = 'POST'
- mapper.resource("zone", "zones",
- controller=zones.create_resource(),
- collection={'detail': 'GET', 'info': 'GET',
- 'select': 'POST'})
-
mapper.resource("user", "users",
controller=users.create_resource(),
collection={'detail': 'GET'})
@@ -111,10 +108,34 @@ class APIRouter(base_wsgi.Router):
controller=accounts.create_resource(),
collection={'detail': 'GET'})
+ mapper.resource("zone", "zones",
+ controller=zones.create_resource(version),
+ collection={'detail': 'GET',
+ 'info': 'GET',
+ 'select': 'POST',
+ 'boot': 'POST'
+ })
+
mapper.resource("console", "consoles",
- controller=consoles.create_resource(),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
+ controller=consoles.create_resource(),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
+ mapper.resource("server", "servers",
+ controller=servers.create_resource(version),
+ collection={'detail': 'GET'},
+ member=self.server_members)
+
+ mapper.resource("image", "images",
+ controller=images.create_resource(version),
+ collection={'detail': 'GET'})
+
+ mapper.resource("limit", "limits",
+ controller=limits.create_resource(version))
+
+ mapper.resource("flavor", "flavors",
+ controller=flavors.create_resource(version),
+ collection={'detail': 'GET'})
super(APIRouter, self).__init__(mapper)
@@ -123,20 +144,11 @@ class APIRouterV10(APIRouter):
"""Define routes specific to OpenStack API V1.0."""
def _setup_routes(self, mapper):
- super(APIRouterV10, self)._setup_routes(mapper)
- mapper.resource("server", "servers",
- controller=servers.create_resource('1.0'),
- collection={'detail': 'GET'},
- member=self.server_members)
-
+ super(APIRouterV10, self)._setup_routes(mapper, '1.0')
mapper.resource("image", "images",
controller=images.create_resource('1.0'),
collection={'detail': 'GET'})
- mapper.resource("flavor", "flavors",
- controller=flavors.create_resource('1.0'),
- collection={'detail': 'GET'})
-
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
controller=shared_ip_groups.create_resource())
@@ -146,9 +158,6 @@ class APIRouterV10(APIRouter):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("limit", "limits",
- controller=limits.create_resource('1.0'))
-
mapper.resource("ip", "ips", controller=ips.create_resource(),
collection=dict(public='GET', private='GET'),
parent_resource=dict(member_name='server',
@@ -159,16 +168,7 @@ class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
def _setup_routes(self, mapper):
- super(APIRouterV11, self)._setup_routes(mapper)
- mapper.resource("server", "servers",
- controller=servers.create_resource('1.1'),
- collection={'detail': 'GET'},
- member=self.server_members)
-
- mapper.resource("image", "images",
- controller=images.create_resource('1.1'),
- collection={'detail': 'GET'})
-
+ super(APIRouterV11, self)._setup_routes(mapper, '1.1')
mapper.resource("image_meta", "meta",
controller=image_metadata.create_resource(),
parent_resource=dict(member_name='image',
@@ -178,10 +178,3 @@ class APIRouterV11(APIRouter):
controller=server_metadata.create_resource(),
parent_resource=dict(member_name='server',
collection_name='servers'))
-
- mapper.resource("flavor", "flavors",
- controller=flavors.create_resource('1.1'),
- collection={'detail': 'GET'})
-
- mapper.resource("limit", "limits",
- controller=limits.create_resource('1.1'))
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index ce7e2805c..4da7ec0ef 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -26,8 +26,6 @@ from nova import log as logging
LOG = logging.getLogger('nova.api.openstack.common')
-
-
FLAGS = flags.FLAGS
diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py
new file mode 100644
index 000000000..fbc6318ef
--- /dev/null
+++ b/nova/api/openstack/create_instance_helper.py
@@ -0,0 +1,346 @@
+# 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
+import re
+import webob
+
+from webob import exc
+from xml.dom import minidom
+
+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 faults
+from nova.api.openstack import wsgi
+from nova.auth import manager as auth_manager
+
+
+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 faults.Fault(exc.HTTPUnprocessableEntity())
+
+ context = req.environ['nova.context']
+
+ password = self.controller._get_server_admin_password(body['server'])
+
+ 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']
+
+ image_href = self.controller._image_ref_from_req_data(body)
+ 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 faults.Fault(exc.HTTPBadRequest(msg))
+
+ personality = body['server'].get('personality')
+
+ injected_files = []
+ if personality:
+ injected_files = self._get_injected_files(personality)
+
+ flavor_id = self.controller._flavor_id_from_req_data(body)
+
+ if not 'name' in body['server']:
+ msg = _("Server name is not defined")
+ raise exc.HTTPBadRequest(msg)
+
+ zone_blob = body['server'].get('blob')
+ name = body['server']['name']
+ self._validate_server_name(name)
+ name = name.strip()
+
+ reservation_id = body['server'].get('reservation_id')
+
+ 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=body['server'].get('metadata', {}),
+ injected_files=injected_files,
+ admin_password=password,
+ zone_blob=zone_blob,
+ reservation_id=reservation_id
+ )
+ )
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
+ except exception.ImageNotFound as error:
+ msg = _("Can not find requested image")
+ raise faults.Fault(exc.HTTPBadRequest(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.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
+
+ 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 _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 _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(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
+ """
+
+ def create(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", "imageRef", "flavorRef"]:
+ if server_node.getAttribute(attr):
+ 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 ""
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 9cf5e8721..798fdd7f7 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -17,24 +17,20 @@ import base64
import traceback
from webob import exc
-from xml.dom import minidom
from nova import compute
from nova import exception
from nova import flags
-import nova.image
from nova import log as logging
-from nova import quota
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 faults
import nova.api.openstack.views.addresses
import nova.api.openstack.views.flavors
import nova.api.openstack.views.images
import nova.api.openstack.views.servers
from nova.api.openstack import wsgi
-from nova.auth import manager as auth_manager
-from nova.compute import instance_types
import nova.api.openstack
from nova.scheduler import api as scheduler_api
@@ -48,7 +44,7 @@ class Controller(object):
def __init__(self):
self.compute_api = compute.API()
- self._image_service = utils.import_object(FLAGS.image_service)
+ self.helper = helper.CreateInstanceHelper(self)
def index(self, req):
""" Returns a list of server names and ids for a given user """
@@ -66,12 +62,6 @@ class Controller(object):
return exc.HTTPBadRequest(str(err))
return servers
- def _image_ref_from_req_data(self, data):
- raise NotImplementedError()
-
- def _flavor_id_from_req_data(self, data):
- raise NotImplementedError()
-
def _get_view_builder(self, req):
raise NotImplementedError()
@@ -86,7 +76,10 @@ class Controller(object):
builder - the response model builder
"""
- instance_list = self.compute_api.get_all(req.environ['nova.context'])
+ reservation_id = req.str_GET.get('reservation_id')
+ instance_list = self.compute_api.get_all(
+ req.environ['nova.context'],
+ reservation_id=reservation_id)
limited_list = self._limit_items(instance_list, req)
builder = self._get_view_builder(req)
servers = [builder.build(inst, is_detail)['server']
@@ -115,128 +108,25 @@ class Controller(object):
def create(self, req, body):
""" Creates a new server for a given user """
- if not body:
- return faults.Fault(exc.HTTPUnprocessableEntity())
-
- context = req.environ['nova.context']
-
- password = self._get_server_admin_password(body['server'])
-
- 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']
-
- image_href = self._image_ref_from_req_data(body)
+ extra_values = None
+ result = None
try:
- image_service, image_id = nova.image.get_image_service(image_href)
- kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
- req, image_service, image_id)
- images = set([str(x['id']) for x in image_service.index(context)])
- assert str(image_id) in images
- except:
- msg = _("Cannot find requested image %s") % image_href
- return faults.Fault(exc.HTTPBadRequest(msg))
-
- personality = body['server'].get('personality')
+ extra_values, result = self.helper.create_instance(
+ req, body, self.compute_api.create)
+ except faults.Fault, f:
+ return f
- injected_files = []
- if personality:
- injected_files = self._get_injected_files(personality)
+ instances = result
- flavor_id = self._flavor_id_from_req_data(body)
-
- if not 'name' in body['server']:
- msg = _("Server name is not defined")
- return exc.HTTPBadRequest(msg)
-
- zone_blob = body['server'].get('blob')
- name = body['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_href,
- kernel_id=kernel_id,
- ramdisk_id=ramdisk_id,
- display_name=name,
- display_description=name,
- key_name=key_name,
- key_data=key_data,
- metadata=body['server'].get('metadata', {}),
- injected_files=injected_files,
- admin_password=password,
- zone_blob=zone_blob)
- except quota.QuotaError as error:
- self._handle_quota_error(error)
- except exception.ImageNotFound as error:
- msg = _("Can not find requested image")
- return faults.Fault(exc.HTTPBadRequest(msg))
-
- inst['instance_type'] = inst_type
- inst['image_ref'] = image_href
+ (inst, ) = instances
+ for key in ['instance_type', 'image_ref']:
+ inst[key] = extra_values[key]
builder = self._get_view_builder(req)
server = builder.build(inst, is_detail=True)
- server['server']['adminPass'] = password
+ server['server']['adminPass'] = extra_values['password']
return server
- 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
-
- def _get_server_admin_password(self, server):
- """ Determine the admin password for a server on creation """
- return utils.generate_password(16)
-
@scheduler_api.redirect_handler
def update(self, req, id, body):
""" Updates the server name or password """
@@ -251,7 +141,7 @@ class Controller(object):
if 'name' in body['server']:
name = body['server']['name']
- self._validate_server_name(name)
+ self.helper._validate_server_name(name)
update_dict['display_name'] = name.strip()
self._parse_update(ctxt, id, body, update_dict)
@@ -263,15 +153,6 @@ class Controller(object):
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
@@ -520,45 +401,9 @@ class Controller(object):
error=item.error))
return dict(actions=actions)
- def _get_kernel_ramdisk_from_image(self, req, image_service, 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 = 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
- return self._do_get_kernel_ramdisk_from_image(image_meta)
-
- @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
-
class ControllerV10(Controller):
+
def _image_ref_from_req_data(self, data):
return data['server']['imageId']
@@ -615,6 +460,10 @@ class ControllerV10(Controller):
response.empty_body = True
return response
+ 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)
+
class ControllerV11(Controller):
def _image_ref_from_req_data(self, data):
@@ -724,92 +573,12 @@ class ControllerV11(Controller):
response.empty_body = True
return response
+ 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 """
- 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(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
- """
-
- def create(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", "imageRef", "flavorRef"]:
- if server_node.getAttribute(attr):
- 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 ""
+ return self.helper._get_server_admin_password_new_style(server)
def create_resource(version='1.0'):
@@ -845,7 +614,7 @@ def create_resource(version='1.0'):
}
deserializers = {
- 'application/xml': ServerXMLDeserializer(),
+ 'application/xml': helper.ServerXMLDeserializer(),
}
return wsgi.Resource(controller, serializers=serializers,
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index b2352e3fd..245d0e3fa 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -42,12 +42,15 @@ class ViewBuilder(object):
def build(self, inst, is_detail):
"""Return a dict that represenst a server."""
- if is_detail:
- server = self._build_detail(inst)
+ if inst.get('_is_precooked', False):
+ server = dict(server=inst)
else:
- server = self._build_simple(inst)
+ if is_detail:
+ server = self._build_detail(inst)
+ else:
+ server = self._build_simple(inst)
- self._build_extra(server, inst)
+ self._build_extra(server, inst)
return server
@@ -79,6 +82,7 @@ class ViewBuilder(object):
ctxt = nova.context.get_admin_context()
compute_api = nova.compute.API()
+
if compute_api.has_finished_migration(ctxt, inst['id']):
inst_dict['status'] = 'RESIZE-CONFIRM'
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index b0e2cab2c..3f8acf339 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -2,7 +2,9 @@
import json
import webob
from xml.dom import minidom
+from xml.parsers import expat
+import faults
from nova import exception
from nova import log as logging
from nova import utils
@@ -71,7 +73,11 @@ class TextDeserializer(object):
class JSONDeserializer(TextDeserializer):
def default(self, datastring):
- return utils.loads(datastring)
+ try:
+ return utils.loads(datastring)
+ except ValueError:
+ raise exception.MalformedRequestBody(
+ reason=_("malformed JSON in request body"))
class XMLDeserializer(TextDeserializer):
@@ -86,8 +92,13 @@ class XMLDeserializer(TextDeserializer):
def default(self, datastring):
plurals = set(self.metadata.get('plurals', {}))
- node = minidom.parseString(datastring).childNodes[0]
- return {node.nodeName: self._from_xml_node(node, plurals)}
+
+ try:
+ node = minidom.parseString(datastring).childNodes[0]
+ return {node.nodeName: self._from_xml_node(node, plurals)}
+ except expat.ExpatError:
+ raise exception.MalformedRequestBody(
+ reason=_("malformed XML in request body"))
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
@@ -353,6 +364,10 @@ class Resource(wsgi.Application):
request)
except exception.InvalidContentType:
return webob.exc.HTTPBadRequest(_("Unsupported Content-Type"))
+ except exception.MalformedRequestBody:
+ explanation = _("Malformed request body")
+ return faults.Fault(webob.exc.HTTPBadRequest(
+ explanation=explanation))
action_result = self.dispatch(request, action, action_args)
@@ -365,9 +380,9 @@ class Resource(wsgi.Application):
try:
msg_dict = dict(url=request.url, status=response.status_int)
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
- except AttributeError:
- msg_dict = dict(url=request.url)
- msg = _("%(url)s returned a fault")
+ except AttributeError, e:
+ msg_dict = dict(url=request.url, e=e)
+ msg = _("%(url)s returned a fault: %(e)s" % msg_dict)
LOG.debug(msg)
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index b2f7898cb..8864f825b 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -21,9 +21,14 @@ from nova import db
from nova import exception
from nova import flags
from nova import log as logging
+
+from nova.compute import api as compute
+from nova.scheduler import api
+
+from nova.api.openstack import create_instance_helper as helper
from nova.api.openstack import common
+from nova.api.openstack import faults
from nova.api.openstack import wsgi
-from nova.scheduler import api
FLAGS = flags.FLAGS
@@ -59,6 +64,11 @@ def check_encryption_key(func):
class Controller(object):
+ """Controller for Zone resources."""
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.helper = helper.CreateInstanceHelper(self)
def index(self, req):
"""Return all zones in brief"""
@@ -93,21 +103,39 @@ class Controller(object):
return dict(zone=_scrub_zone(zone))
def delete(self, req, id):
+ """Delete a child zone entry."""
zone_id = int(id)
api.zone_delete(req.environ['nova.context'], zone_id)
return {}
def create(self, req, body):
+ """Create a child zone entry."""
context = req.environ['nova.context']
zone = api.zone_create(context, body["zone"])
return dict(zone=_scrub_zone(zone))
def update(self, req, id, body):
+ """Update a child zone entry."""
context = req.environ['nova.context']
zone_id = int(id)
zone = api.zone_update(context, zone_id, body["zone"])
return dict(zone=_scrub_zone(zone))
+ def boot(self, req, body):
+ """Creates a new server for a given user while being Zone aware.
+
+ Returns a reservation ID (a UUID).
+ """
+ result = None
+ try:
+ extra_values, result = self.helper.create_instance(req, body,
+ self.compute_api.create_all_at_once)
+ except faults.Fault, f:
+ return f
+
+ reservation_id = result
+ return {'reservation_id': reservation_id}
+
@check_encryption_key
def select(self, req, body):
"""Returns a weighted list of costs to create instances
@@ -131,8 +159,37 @@ class Controller(object):
blob=cipher_text))
return cooked
+ 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 _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)
+
+
+class ControllerV11(object):
+ """Controller for 1.1 Zone resources."""
+
+ 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 _image_ref_from_req_data(self, data):
+ return data['server']['imageRef']
+
+ def _flavor_id_from_req_data(self, data):
+ return data['server']['flavorRef']
+
+
+def create_resource(version):
+ controller = {
+ '1.0': Controller,
+ '1.1': ControllerV11,
+ }[version]()
-def create_resource():
metadata = {
"attributes": {
"zone": ["id", "api_url", "name", "capabilities"],
@@ -144,4 +201,9 @@ def create_resource():
metadata=metadata),
}
- return wsgi.Resource(Controller(), serializers=serializers)
+ deserializers = {
+ 'application/xml': helper.ServerXMLDeserializer(),
+ }
+
+ return wsgi.Resource(controller, serializers=serializers,
+ deserializers=deserializers)
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 83a31707a..b05db3ca5 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -117,7 +117,8 @@ class API(base.Base):
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
availability_zone=None, user_data=None, metadata={},
- injected_files=None, admin_password=None, zone_blob=None):
+ injected_files=None, admin_password=None, zone_blob=None,
+ reservation_id=None):
"""Verify all the input parameters regardless of the provisioning
strategy being performed."""
@@ -147,6 +148,9 @@ class API(base.Base):
os_type = None
if 'properties' in image and 'os_type' in image['properties']:
os_type = image['properties']['os_type']
+ vm_mode = None
+ if 'properties' in image and 'vm_mode' in image['properties']:
+ vm_mode = image['properties']['vm_mode']
if kernel_id is None:
kernel_id = image['properties'].get('kernel_id', None)
@@ -183,8 +187,11 @@ class API(base.Base):
key_pair = db.key_pair_get(context, context.user_id, key_name)
key_data = key_pair['public_key']
+ if reservation_id is None:
+ reservation_id = utils.generate_uid('r')
+
base_options = {
- 'reservation_id': utils.generate_uid('r'),
+ 'reservation_id': reservation_id,
'image_ref': image_href,
'kernel_id': kernel_id or '',
'ramdisk_id': ramdisk_id or '',
@@ -205,7 +212,8 @@ class API(base.Base):
'locked': False,
'metadata': metadata,
'availability_zone': availability_zone,
- 'os_type': os_type}
+ 'os_type': os_type,
+ 'vm_mode': vm_mode}
return (num_instances, base_options, security_groups)
@@ -282,7 +290,8 @@ class API(base.Base):
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
availability_zone=None, user_data=None, metadata={},
- injected_files=None, admin_password=None, zone_blob=None):
+ injected_files=None, admin_password=None, zone_blob=None,
+ reservation_id=None):
"""Provision the instances by passing the whole request to
the Scheduler for execution. Returns a Reservation ID
related to the creation of all of these instances."""
@@ -294,7 +303,8 @@ class API(base.Base):
display_name, display_description,
key_name, key_data, security_group,
availability_zone, user_data, metadata,
- injected_files, admin_password, zone_blob)
+ injected_files, admin_password, zone_blob,
+ reservation_id)
self._ask_scheduler_to_create_instance(context, base_options,
instance_type, zone_blob,
@@ -310,7 +320,8 @@ class API(base.Base):
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
availability_zone=None, user_data=None, metadata={},
- injected_files=None, admin_password=None, zone_blob=None):
+ injected_files=None, admin_password=None, zone_blob=None,
+ reservation_id=None):
"""
Provision the instances by sending off a series of single
instance requests to the Schedulers. This is fine for trival
@@ -328,7 +339,8 @@ class API(base.Base):
display_name, display_description,
key_name, key_data, security_group,
availability_zone, user_data, metadata,
- injected_files, admin_password, zone_blob)
+ injected_files, admin_password, zone_blob,
+ reservation_id)
instances = []
LOG.debug(_("Going to run %s instances..."), num_instances)
@@ -492,6 +504,24 @@ class API(base.Base):
"""
return self.get(context, instance_id)
+ def get_all_across_zones(self, context, reservation_id):
+ """Get all instances with this reservation_id, across
+ all available Zones (if any).
+ """
+ instances = self.db.instance_get_all_by_reservation(
+ context, reservation_id)
+
+ children = scheduler_api.call_zone_method(context, "list",
+ novaclient_collection_name="servers",
+ reservation_id=reservation_id)
+
+ for zone, servers in children:
+ for server in servers:
+ # Results are ready to send to user. No need to scrub.
+ server._info['_is_precooked'] = True
+ instances.append(server._info)
+ return instances
+
def get_all(self, context, project_id=None, reservation_id=None,
fixed_ip=None):
"""Get all instances filtered by one of the given parameters.
@@ -500,8 +530,7 @@ class API(base.Base):
all instances in the system.
"""
if reservation_id is not None:
- return self.db.instance_get_all_by_reservation(
- context, reservation_id)
+ return self.get_all_across_zones(context, reservation_id)
if fixed_ip is not None:
return self.db.fixed_ip_get_instance(context, fixed_ip)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 46f5e7494..c882c587c 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -1084,6 +1084,7 @@ def instance_get_all_by_host(context, host):
options(joinedload('virtual_interfaces')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(host=host).\
filter_by(deleted=can_read_deleted(context)).\
@@ -1100,6 +1101,7 @@ def instance_get_all_by_project(context, project_id):
options(joinedload('virtual_interfaces')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(project_id=project_id).\
filter_by(deleted=can_read_deleted(context)).\
@@ -1116,6 +1118,7 @@ def instance_get_all_by_reservation(context, reservation_id):
options(joinedload('virtual_interfaces')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(reservation_id=reservation_id).\
filter_by(deleted=can_read_deleted(context)).\
@@ -1126,6 +1129,7 @@ def instance_get_all_by_reservation(context, reservation_id):
options(joinedload('virtual_interfaces')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ips.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(project_id=context.project_id).\
filter_by(reservation_id=reservation_id).\
@@ -1140,6 +1144,8 @@ def instance_get_project_vpn(context, project_id):
options(joinedload_all('fixed_ips.floating_ips')).\
options(joinedload('virtual_interfaces')).\
options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(project_id=project_id).\
filter_by(image_ref=str(FLAGS.vpn_image_id)).\
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/023_add_vm_mode_to_instances.py b/nova/db/sqlalchemy/migrate_repo/versions/023_add_vm_mode_to_instances.py
new file mode 100644
index 000000000..0c587f569
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/023_add_vm_mode_to_instances.py
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+#
+# 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.
+
+from sqlalchemy import Column, Integer, MetaData, String, Table
+
+meta = MetaData()
+
+instances_vm_mode = Column('vm_mode',
+ String(length=255, convert_unicode=False,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False),
+ nullable=True)
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+
+ instances = Table('instances', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ instances.create_column(instances_vm_mode)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ instances = Table('instances', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ instances.drop_column('vm_mode')
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 605e6126a..ddf068565 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -232,6 +232,7 @@ class Instance(BASE, NovaBase):
locked = Column(Boolean)
os_type = Column(String(255))
+ vm_mode = Column(String(255))
# TODO(vish): see Ewan's email about state improvements, probably
# should be in a driver base class or some such
diff --git a/nova/exception.py b/nova/exception.py
index 2c24d883a..6346c8931 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -605,3 +605,7 @@ class InstanceExists(Duplicate):
class MigrationError(NovaException):
message = _("Migration error") + ": %(reason)s"
+
+
+class MalformedRequestBody(NovaException):
+ message = _("Malformed message body: %(reason)s")
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 09e7c9140..3b3195c2e 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -106,12 +106,14 @@ def _wrap_method(function, self):
def _process(func, zone):
"""Worker stub for green thread pool. Give the worker
an authenticated nova client and zone info."""
- nova = novaclient.OpenStack(zone.username, zone.password, zone.api_url)
+ nova = novaclient.OpenStack(zone.username, zone.password, None,
+ zone.api_url)
nova.authenticate()
return func(nova, zone)
-def call_zone_method(context, method, errors_to_ignore=None, *args, **kwargs):
+def call_zone_method(context, method_name, errors_to_ignore=None,
+ novaclient_collection_name='zones', *args, **kwargs):
"""Returns a list of (zone, call_result) objects."""
if not isinstance(errors_to_ignore, (list, tuple)):
# This will also handle the default None
@@ -121,7 +123,7 @@ def call_zone_method(context, method, errors_to_ignore=None, *args, **kwargs):
results = []
for zone in db.zone_get_all(context):
try:
- nova = novaclient.OpenStack(zone.username, zone.password,
+ nova = novaclient.OpenStack(zone.username, zone.password, None,
zone.api_url)
nova.authenticate()
except novaclient.exceptions.BadRequest, e:
@@ -131,18 +133,16 @@ def call_zone_method(context, method, errors_to_ignore=None, *args, **kwargs):
#TODO (dabo) - add logic for failure counts per zone,
# with escalation after a given number of failures.
continue
- zone_method = getattr(nova.zones, method)
+ novaclient_collection = getattr(nova, novaclient_collection_name)
+ collection_method = getattr(novaclient_collection, method_name)
def _error_trap(*args, **kwargs):
try:
- return zone_method(*args, **kwargs)
+ return collection_method(*args, **kwargs)
except Exception as e:
if type(e) in errors_to_ignore:
return None
- # TODO (dabo) - want to be able to re-raise here.
- # Returning a string now; raising was causing issues.
- # raise e
- return "ERROR", "%s" % e
+ raise
res = pool.spawn(_error_trap, *args, **kwargs)
results.append((zone, res))
diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py
index a29703aaf..6cb75aa8d 100644
--- a/nova/scheduler/manager.py
+++ b/nova/scheduler/manager.py
@@ -89,8 +89,8 @@ class SchedulerManager(manager.Manager):
host = getattr(self.driver, driver_method)(elevated, *args,
**kwargs)
except AttributeError, e:
- LOG.exception(_("Driver Method %(driver_method)s missing: %(e)s")
- % locals())
+ LOG.warning(_("Driver Method %(driver_method)s missing: %(e)s."
+ "Reverting to schedule()") % locals())
host = self.driver.schedule(elevated, topic, *args, **kwargs)
if not host:
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
index faa969124..e7bff2faa 100644
--- a/nova/scheduler/zone_aware_scheduler.py
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -88,9 +88,10 @@ class ZoneAwareScheduler(driver.Scheduler):
instance_properties = request_spec['instance_properties']
name = instance_properties['display_name']
- image_id = instance_properties['image_id']
+ image_ref = instance_properties['image_ref']
meta = instance_properties['metadata']
flavor_id = instance_type['flavorid']
+ reservation_id = instance_properties['reservation_id']
files = kwargs['injected_files']
ipgroup = None # Not supported in OS API ... yet
@@ -99,18 +100,20 @@ class ZoneAwareScheduler(driver.Scheduler):
child_blob = zone_info['child_blob']
zone = db.zone_get(context, child_zone)
url = zone.api_url
- LOG.debug(_("Forwarding instance create call to child zone %(url)s")
+ LOG.debug(_("Forwarding instance create call to child zone %(url)s"
+ ". ReservationID=%(reservation_id)s")
% locals())
nova = None
try:
- nova = novaclient.OpenStack(zone.username, zone.password, url)
+ nova = novaclient.OpenStack(zone.username, zone.password, None,
+ url)
nova.authenticate()
except novaclient.exceptions.BadRequest, e:
raise exception.NotAuthorized(_("Bad credentials attempting "
"to talk to zone at %(url)s.") % locals())
- nova.servers.create(name, image_id, flavor_id, ipgroup, meta, files,
- child_blob)
+ nova.servers.create(name, image_ref, flavor_id, ipgroup, meta, files,
+ child_blob, reservation_id=reservation_id)
def _provision_resource_from_blob(self, context, item, instance_id,
request_spec, kwargs):
@@ -182,7 +185,11 @@ class ZoneAwareScheduler(driver.Scheduler):
if not build_plan:
raise driver.NoValidHost(_('No hosts were available'))
- for item in build_plan:
+ for num in xrange(request_spec['num_instances']):
+ if not build_plan:
+ break
+
+ item = build_plan.pop(0)
self._provision_resource(context, item, instance_id, request_spec,
kwargs)
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index 3f483adff..ba7403c15 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -89,7 +89,8 @@ class ZoneState(object):
def _call_novaclient(zone):
"""Call novaclient. Broken out for testing purposes."""
- client = novaclient.OpenStack(zone.username, zone.password, zone.api_url)
+ client = novaclient.OpenStack(zone.username, zone.password, None,
+ zone.api_url)
return client.zones.info()._info
diff --git a/nova/tests/api/openstack/test_api.py b/nova/tests/api/openstack/test_api.py
index c63431a45..7321c329f 100644
--- a/nova/tests/api/openstack/test_api.py
+++ b/nova/tests/api/openstack/test_api.py
@@ -15,6 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
+
import webob.exc
import webob.dec
@@ -23,6 +25,7 @@ from webob import Request
from nova import test
from nova.api import openstack
from nova.api.openstack import faults
+from nova.tests.api.openstack import fakes
class APITest(test.TestCase):
@@ -31,6 +34,24 @@ class APITest(test.TestCase):
# simpler version of the app than fakes.wsgi_app
return openstack.FaultWrapper(inner_app)
+ def test_malformed_json(self):
+ req = webob.Request.blank('/')
+ req.method = 'POST'
+ req.body = '{'
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_malformed_xml(self):
+ req = webob.Request.blank('/')
+ req.method = 'POST'
+ req.body = '<hi im not xml>'
+ req.headers["content-type"] = "application/xml"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
def test_exceptions_are_converted_to_faults(self):
@webob.dec.wsgify
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 515ade37c..54963a38e 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -31,10 +31,12 @@ from nova import test
from nova import utils
import nova.api.openstack
from nova.api.openstack import servers
+from nova.api.openstack import create_instance_helper
import nova.compute.api
from nova.compute import instance_types
from nova.compute import power_state
import nova.db.api
+import nova.scheduler.api
from nova.db.sqlalchemy.models import Instance
from nova.db.sqlalchemy.models import InstanceMetadata
import nova.image.fake
@@ -68,6 +70,34 @@ def return_servers(context, user_id=1):
return [stub_instance(i, user_id) for i in xrange(5)]
+def return_servers_by_reservation(context, reservation_id=""):
+ return [stub_instance(i, reservation_id) for i in xrange(5)]
+
+
+def return_servers_by_reservation_empty(context, reservation_id=""):
+ return []
+
+
+def return_servers_from_child_zones_empty(*args, **kwargs):
+ return []
+
+
+def return_servers_from_child_zones(*args, **kwargs):
+ class Server(object):
+ pass
+
+ zones = []
+ for zone in xrange(3):
+ servers = []
+ for server_id in xrange(5):
+ server = Server()
+ server._info = stub_instance(server_id, reservation_id="child")
+ servers.append(server)
+
+ zones.append(("Zone%d" % zone, servers))
+ return zones
+
+
def return_security_group(context, instance_id, security_group_id):
pass
@@ -81,7 +111,7 @@ def instance_addresses(context, instance_id):
def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
- host=None, power_state=0):
+ host=None, power_state=0, reservation_id=""):
metadata = []
metadata.append(InstanceMetadata(key='seq', value=id))
@@ -93,6 +123,11 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
if host is not None:
host = str(host)
+ # ReservationID isn't sent back, hack it in there.
+ server_name = "server%s" % id
+ if reservation_id != "":
+ server_name = "reservation_%s" % (reservation_id, )
+
instance = {
"id": id,
"admin_pass": "",
@@ -113,13 +148,13 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
"host": host,
"instance_type": dict(inst_type),
"user_data": "",
- "reservation_id": "",
+ "reservation_id": reservation_id,
"mac_address": "",
"scheduled_at": utils.utcnow(),
"launched_at": utils.utcnow(),
"terminated_at": utils.utcnow(),
"availability_zone": "",
- "display_name": "server%s" % id,
+ "display_name": server_name,
"display_description": "",
"locked": False,
"metadata": metadata}
@@ -365,6 +400,57 @@ class ServersTest(test.TestCase):
self.assertEqual(s.get('imageId', None), None)
i += 1
+ def test_get_server_list_with_reservation_id(self):
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_reservation',
+ return_servers_by_reservation)
+ self.stubs.Set(nova.scheduler.api, 'call_zone_method',
+ return_servers_from_child_zones)
+ req = webob.Request.blank('/v1.0/servers?reservation_id=foo')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ i = 0
+ for s in res_dict['servers']:
+ if '_is_precooked' in s:
+ self.assertEqual(s.get('reservation_id'), 'child')
+ else:
+ self.assertEqual(s.get('name'), 'server%d' % i)
+ i += 1
+
+ def test_get_server_list_with_reservation_id_empty(self):
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_reservation',
+ return_servers_by_reservation_empty)
+ self.stubs.Set(nova.scheduler.api, 'call_zone_method',
+ return_servers_from_child_zones_empty)
+ req = webob.Request.blank('/v1.0/servers/detail?reservation_id=foo')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ i = 0
+ for s in res_dict['servers']:
+ if '_is_precooked' in s:
+ self.assertEqual(s.get('reservation_id'), 'child')
+ else:
+ self.assertEqual(s.get('name'), 'server%d' % i)
+ i += 1
+
+ def test_get_server_list_with_reservation_id_details(self):
+ self.stubs.Set(nova.db.api, 'instance_get_all_by_reservation',
+ return_servers_by_reservation)
+ self.stubs.Set(nova.scheduler.api, 'call_zone_method',
+ return_servers_from_child_zones)
+ req = webob.Request.blank('/v1.0/servers/detail?reservation_id=foo')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ i = 0
+ for s in res_dict['servers']:
+ if '_is_precooked' in s:
+ self.assertEqual(s.get('reservation_id'), 'child')
+ else:
+ self.assertEqual(s.get('name'), 'server%d' % i)
+ i += 1
+
def test_get_server_list_v1_1(self):
req = webob.Request.blank('/v1.1/servers')
res = req.get_response(fakes.wsgi_app())
@@ -485,7 +571,8 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for)
self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip',
fake_method)
- self.stubs.Set(nova.api.openstack.servers.Controller,
+ self.stubs.Set(
+ nova.api.openstack.create_instance_helper.CreateInstanceHelper,
"_get_kernel_ramdisk_from_image", kernel_ramdisk_mapping)
self.stubs.Set(nova.compute.api.API, "_find_host", find_host)
@@ -514,6 +601,48 @@ class ServersTest(test.TestCase):
def test_create_instance(self):
self._test_create_instance_helper()
+ def test_create_instance_via_zones(self):
+ """Server generated ReservationID"""
+ self._setup_for_create_instance()
+ FLAGS.allow_admin_api = True
+
+ body = dict(server=dict(
+ name='server_test', imageId=3, flavorId=2,
+ metadata={'hello': 'world', 'open': 'stack'},
+ personality={}))
+ req = webob.Request.blank('/v1.0/zones/boot')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+
+ reservation_id = json.loads(res.body)['reservation_id']
+ self.assertEqual(res.status_int, 200)
+ self.assertNotEqual(reservation_id, "")
+ self.assertNotEqual(reservation_id, None)
+ self.assertTrue(len(reservation_id) > 1)
+
+ def test_create_instance_via_zones_with_resid(self):
+ """User supplied ReservationID"""
+ self._setup_for_create_instance()
+ FLAGS.allow_admin_api = True
+
+ body = dict(server=dict(
+ name='server_test', imageId=3, flavorId=2,
+ metadata={'hello': 'world', 'open': 'stack'},
+ personality={}, reservation_id='myresid'))
+ req = webob.Request.blank('/v1.0/zones/boot')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+
+ reservation_id = json.loads(res.body)['reservation_id']
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(reservation_id, "myresid")
+
def test_create_instance_no_key_pair(self):
fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False)
self._test_create_instance_helper()
@@ -1403,7 +1532,7 @@ class ServersTest(test.TestCase):
class TestServerCreateRequestXMLDeserializer(unittest.TestCase):
def setUp(self):
- self.deserializer = servers.ServerXMLDeserializer()
+ self.deserializer = create_instance_helper.ServerXMLDeserializer()
def test_minimal_request(self):
serial_request = """
@@ -1735,7 +1864,8 @@ class TestServerInstanceCreation(test.TestCase):
compute_api = MockComputeAPI()
self.stubs.Set(nova.compute, 'API', make_stub_method(compute_api))
- self.stubs.Set(nova.api.openstack.servers.Controller,
+ self.stubs.Set(
+ nova.api.openstack.create_instance_helper.CreateInstanceHelper,
'_get_kernel_ramdisk_from_image', make_stub_method((1, 1)))
return compute_api
@@ -1991,6 +2121,6 @@ class TestGetKernelRamdiskFromImage(test.TestCase):
@staticmethod
def _get_k_r(image_meta):
"""Rebinding function to a shorter name for convenience"""
- kernel_id, ramdisk_id = \
- servers.Controller._do_get_kernel_ramdisk_from_image(image_meta)
+ kernel_id, ramdisk_id = create_instance_helper.CreateInstanceHelper. \
+ _do_get_kernel_ramdisk_from_image(image_meta)
return kernel_id, ramdisk_id
diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py
index 9f108f286..4a86755c5 100644
--- a/nova/tests/scheduler/test_scheduler.py
+++ b/nova/tests/scheduler/test_scheduler.py
@@ -1109,10 +1109,4 @@ class CallZoneMethodTest(test.TestCase):
def test_call_zone_method_generates_exception(self):
context = {}
method = 'raises_exception'
- results = api.call_zone_method(context, method)
-
- # FIXME(sirp): for now the _error_trap code is catching errors and
- # converting them to a ("ERROR", "string") tuples. The code (and this
- # test) should eventually handle real exceptions.
- expected = [(1, ('ERROR', 'testing'))]
- self.assertEqual(expected, results)
+ self.assertRaises(Exception, api.call_zone_method, context, method)
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 7931d117d..668c68c64 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -161,9 +161,24 @@ class VMOps(object):
# Create the VM ref and attach the first disk
first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
vdis[0]['vdi_uuid'])
- use_pv_kernel = VMHelper.determine_is_pv(self._session,
- instance.id, first_vdi_ref, disk_image_type,
- instance.os_type)
+
+ vm_mode = instance.vm_mode and instance.vm_mode.lower()
+ if vm_mode == 'pv':
+ use_pv_kernel = True
+ elif vm_mode in ('hv', 'hvm'):
+ use_pv_kernel = False
+ vm_mode = 'hvm' # Normalize
+ else:
+ use_pv_kernel = VMHelper.determine_is_pv(self._session,
+ instance.id, first_vdi_ref, disk_image_type,
+ instance.os_type)
+ vm_mode = use_pv_kernel and 'pv' or 'hvm'
+
+ if instance.vm_mode != vm_mode:
+ # Update database with normalized (or determined) value
+ db.instance_update(context.get_admin_context(),
+ instance['id'], {'vm_mode': vm_mode})
+
vm_ref = VMHelper.create_vm(self._session, instance,
kernel, ramdisk, use_pv_kernel)
VMHelper.create_vbd(session=self._session, vm_ref=vm_ref,
diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration b/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
index 75c653408..ac1c50ad9 100644
--- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
+++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/migration
@@ -44,7 +44,7 @@ def move_vhds_into_sr(session, args):
new_cow_uuid = params['new_cow_uuid']
sr_path = params['sr_path']
- sr_temp_path = "%s/images/" % sr_path
+ sr_temp_path = "%s/tmp/" % sr_path
# Discover the copied VHDs locally, and then set up paths to copy
# them to under the SR
diff --git a/run_tests.py b/run_tests.py
index d5d8acd16..0944bb585 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -56,9 +56,11 @@ To run a single test module:
"""
import gettext
+import heapq
import os
import unittest
import sys
+import time
gettext.install('nova', unicode=1)
@@ -183,9 +185,21 @@ class _NullColorizer(object):
self.stream.write(text)
+def get_elapsed_time_color(elapsed_time):
+ if elapsed_time > 1.0:
+ return 'red'
+ elif elapsed_time > 0.25:
+ return 'yellow'
+ else:
+ return 'green'
+
+
class NovaTestResult(result.TextTestResult):
def __init__(self, *args, **kw):
+ self.show_elapsed = kw.pop('show_elapsed')
result.TextTestResult.__init__(self, *args, **kw)
+ self.num_slow_tests = 5
+ self.slow_tests = [] # this is a fixed-sized heap
self._last_case = None
self.colorizer = None
# NOTE(vish): reset stdout for the terminal check
@@ -200,25 +214,40 @@ class NovaTestResult(result.TextTestResult):
def getDescription(self, test):
return str(test)
- # NOTE(vish): copied from unittest with edit to add color
- def addSuccess(self, test):
- unittest.TestResult.addSuccess(self, test)
+ def _handleElapsedTime(self, test):
+ self.elapsed_time = time.time() - self.start_time
+ item = (self.elapsed_time, test)
+ # Record only the n-slowest tests using heap
+ if len(self.slow_tests) >= self.num_slow_tests:
+ heapq.heappushpop(self.slow_tests, item)
+ else:
+ heapq.heappush(self.slow_tests, item)
+
+ def _writeElapsedTime(self, test):
+ color = get_elapsed_time_color(self.elapsed_time)
+ self.colorizer.write(" %.2f" % self.elapsed_time, color)
+
+ def _writeResult(self, test, long_result, color, short_result, success):
if self.showAll:
- self.colorizer.write("OK", 'green')
+ self.colorizer.write(long_result, color)
+ if self.show_elapsed and success:
+ self._writeElapsedTime(test)
self.stream.writeln()
elif self.dots:
- self.stream.write('.')
+ self.stream.write(short_result)
self.stream.flush()
# NOTE(vish): copied from unittest with edit to add color
+ def addSuccess(self, test):
+ unittest.TestResult.addSuccess(self, test)
+ self._handleElapsedTime(test)
+ self._writeResult(test, 'OK', 'green', '.', True)
+
+ # NOTE(vish): copied from unittest with edit to add color
def addFailure(self, test, err):
unittest.TestResult.addFailure(self, test, err)
- if self.showAll:
- self.colorizer.write("FAIL", 'red')
- self.stream.writeln()
- elif self.dots:
- self.stream.write('F')
- self.stream.flush()
+ self._handleElapsedTime(test)
+ self._writeResult(test, 'FAIL', 'red', 'F', False)
# NOTE(vish): copied from nose with edit to add color
def addError(self, test, err):
@@ -226,6 +255,7 @@ class NovaTestResult(result.TextTestResult):
errorClasses. If the exception is a registered class, the
error will be added to the list for that class, not errors.
"""
+ self._handleElapsedTime(test)
stream = getattr(self, 'stream', None)
ec, ev, tb = err
try:
@@ -252,14 +282,11 @@ class NovaTestResult(result.TextTestResult):
self.errors.append((test, exc_info))
test.passed = False
if stream is not None:
- if self.showAll:
- self.colorizer.write("ERROR", 'red')
- self.stream.writeln()
- elif self.dots:
- stream.write('E')
+ self._writeResult(test, 'ERROR', 'red', 'E', False)
def startTest(self, test):
unittest.TestResult.startTest(self, test)
+ self.start_time = time.time()
current_case = test.test.__class__.__name__
if self.showAll:
@@ -273,21 +300,47 @@ class NovaTestResult(result.TextTestResult):
class NovaTestRunner(core.TextTestRunner):
+ def __init__(self, *args, **kwargs):
+ self.show_elapsed = kwargs.pop('show_elapsed')
+ core.TextTestRunner.__init__(self, *args, **kwargs)
+
def _makeResult(self):
return NovaTestResult(self.stream,
self.descriptions,
self.verbosity,
- self.config)
+ self.config,
+ show_elapsed=self.show_elapsed)
+
+ def _writeSlowTests(self, result_):
+ # Pare out 'fast' tests
+ slow_tests = [item for item in result_.slow_tests
+ if get_elapsed_time_color(item[0]) != 'green']
+ if slow_tests:
+ slow_total_time = sum(item[0] for item in slow_tests)
+ self.stream.writeln("Slowest %i tests took %.2f secs:"
+ % (len(slow_tests), slow_total_time))
+ for elapsed_time, test in sorted(slow_tests, reverse=True):
+ time_str = "%.2f" % elapsed_time
+ self.stream.writeln(" %s %s" % (time_str.ljust(10), test))
+
+ def run(self, test):
+ result_ = core.TextTestRunner.run(self, test)
+ if self.show_elapsed:
+ self._writeSlowTests(result_)
+ return result_
if __name__ == '__main__':
logging.setup()
# If any argument looks like a test name but doesn't have "nova.tests" in
# front of it, automatically add that so we don't have to type as much
+ show_elapsed = True
argv = []
for x in sys.argv:
if x.startswith('test_'):
argv.append('nova.tests.%s' % x)
+ elif x.startswith('--hide-elapsed'):
+ show_elapsed = False
else:
argv.append(x)
@@ -300,5 +353,6 @@ if __name__ == '__main__':
runner = NovaTestRunner(stream=c.stream,
verbosity=c.verbosity,
- config=c)
+ config=c,
+ show_elapsed=show_elapsed)
sys.exit(not core.run(config=c, testRunner=runner, argv=argv))
diff --git a/run_tests.sh b/run_tests.sh
index c7bcd5d67..c3f06f837 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -10,6 +10,7 @@ function usage {
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8"
echo " -h, --help Print this usage message"
+ echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo ""
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
@@ -24,6 +25,7 @@ function process_option {
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
-f|--force) let force=1;;
-p|--pep8) let just_pep8=1;;
+ -*) noseopts="$noseopts $1";;
*) noseargs="$noseargs $1"
esac
}
@@ -34,6 +36,7 @@ always_venv=0
never_venv=0
force=0
noseargs=
+noseopts=
wrapper=""
just_pep8=0
@@ -72,7 +75,7 @@ function run_pep8 {
--exclude=vcsversion.py ${srcfiles}
}
-NOSETESTS="python run_tests.py $noseargs"
+NOSETESTS="python run_tests.py $noseopts $noseargs"
if [ $never_venv -eq 0 ]
then
@@ -107,7 +110,10 @@ fi
run_tests || exit
-# Also run pep8 if no options were provided.
+# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
+# not when we're running tests individually. To handle this, we need to
+# distinguish between options (noseopts), which begin with a '-', and
+# arguments (noseargs).
if [ -z "$noseargs" ]; then
run_pep8
fi
diff --git a/tools/pip-requires b/tools/pip-requires
index e81ef944a..7849dbea9 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -10,7 +10,7 @@ boto==1.9b
carrot==0.10.5
eventlet==0.9.12
lockfile==0.8
-python-novaclient==2.3
+python-novaclient==2.5.3
python-daemon==1.5.5
python-gflags==1.3
redis==2.0.0