summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorTrey Morris <trey.morris@rackspace.com>2011-05-24 16:21:47 -0500
committerTrey Morris <trey.morris@rackspace.com>2011-05-24 16:21:47 -0500
commit2b0af0ccf792dd2f0420fd8eaa6bc8be8a8671bf (patch)
tree56d5d0933b98ee61f9b7efad15f91975910bdca7 /nova
parentd3c6f77f287f8078606ca7fc99a8121cadb76fd4 (diff)
parent781ed70a4035fe507e2e1e83dcd5262c81792b5a (diff)
downloadnova-2b0af0ccf792dd2f0420fd8eaa6bc8be8a8671bf.tar.gz
nova-2b0af0ccf792dd2f0420fd8eaa6bc8be8a8671bf.tar.xz
nova-2b0af0ccf792dd2f0420fd8eaa6bc8be8a8671bf.zip
merge trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/__init__.py4
-rw-r--r--nova/api/ec2/cloud.py23
-rw-r--r--nova/api/openstack/__init__.py3
-rw-r--r--nova/api/openstack/extensions.py84
-rw-r--r--nova/api/openstack/zones.py43
-rw-r--r--nova/compute/api.py7
-rw-r--r--nova/crypto.py45
-rw-r--r--nova/exception.py4
-rw-r--r--nova/flags.py7
-rw-r--r--nova/network/linux_net.py3
-rw-r--r--nova/scheduler/api.py45
-rw-r--r--nova/scheduler/zone_aware_scheduler.py119
-rw-r--r--nova/service.py10
-rw-r--r--nova/tests/api/openstack/extensions/foxinsocks.py26
-rw-r--r--nova/tests/api/openstack/test_extensions.py45
-rw-r--r--nova/tests/api/openstack/test_zones.py42
-rw-r--r--nova/tests/fake_flags.py28
-rw-r--r--nova/tests/integrated/integrated_helpers.py13
-rw-r--r--nova/tests/public_key/dummy.fingerprint1
-rw-r--r--nova/tests/public_key/dummy.pub1
-rw-r--r--nova/tests/real_flags.py26
-rw-r--r--nova/tests/test_api.py23
-rw-r--r--nova/tests/test_cloud.py30
-rw-r--r--nova/tests/test_crypto.py48
-rw-r--r--nova/tests/test_flags.py14
-rw-r--r--nova/tests/test_scheduler.py61
-rw-r--r--nova/tests/test_zone_aware_scheduler.py119
-rw-r--r--nova/virt/disk.py50
-rw-r--r--nova/virt/images.py69
-rw-r--r--nova/virt/xenapi/vm_utils.py8
-rw-r--r--nova/virt/xenapi/vmops.py10
-rw-r--r--nova/virt/xenapi/volume_utils.py43
-rw-r--r--nova/wsgi.py5
33 files changed, 803 insertions, 256 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index cd59340bd..c13993dd3 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -338,6 +338,10 @@ class Executor(wsgi.Application):
else:
return self._error(req, context, type(ex).__name__,
unicode(ex))
+ except exception.KeyPairExists as ex:
+ LOG.debug(_('KeyPairExists raised: %s'), unicode(ex),
+ context=context)
+ return self._error(req, context, type(ex).__name__, unicode(ex))
except Exception as ex:
extra = {'environment': req.environ}
LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index de34a2874..09f5fa989 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,6 +27,8 @@ import datetime
import IPy
import os
import urllib
+import tempfile
+import shutil
from nova import compute
from nova import context
@@ -316,6 +318,27 @@ class CloudController(object):
'keyMaterial': data['private_key']}
# TODO(vish): when context is no longer an object, pass it here
+ def import_public_key(self, context, key_name, public_key,
+ fingerprint=None):
+ LOG.audit(_("Import key %s"), key_name, context=context)
+ key = {}
+ key['user_id'] = context.user_id
+ key['name'] = key_name
+ key['public_key'] = public_key
+ if fingerprint is None:
+ tmpdir = tempfile.mkdtemp()
+ pubfile = os.path.join(tmpdir, 'temp.pub')
+ fh = open(pubfile, 'w')
+ fh.write(public_key)
+ fh.close()
+ (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f',
+ '%s' % (pubfile))
+ fingerprint = out.split(' ')[1]
+ shutil.rmtree(tmpdir)
+ key['fingerprint'] = fingerprint
+ db.key_pair_create(context, key)
+ return True
+
def delete_key_pair(self, context, key_name, **kwargs):
LOG.audit(_("Delete key pair %s"), key_name, context=context)
try:
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 348b70d5b..5b7f080ad 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -98,7 +98,8 @@ class APIRouter(wsgi.Router):
server_members['inject_network_info'] = 'POST'
mapper.resource("zone", "zones", controller=zones.Controller(),
- collection={'detail': 'GET', 'info': 'GET'}),
+ collection={'detail': 'GET', 'info': 'GET',
+ 'select': 'GET'})
mapper.resource("user", "users", controller=users.Controller(),
collection={'detail': 'GET'})
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index 7ea7afef6..8e77b25fb 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -105,15 +105,14 @@ class ExtensionDescriptor(object):
actions = []
return actions
- def get_response_extensions(self):
- """List of extensions.ResponseExtension extension objects.
+ def get_request_extensions(self):
+ """List of extensions.RequestException extension objects.
- Response extensions are used to insert information into existing
- response data.
+ Request extensions are used to handle custom request data.
"""
- response_exts = []
- return response_exts
+ request_exts = []
+ return request_exts
class ActionExtensionController(common.OpenstackController):
@@ -137,7 +136,7 @@ class ActionExtensionController(common.OpenstackController):
return res
-class ResponseExtensionController(common.OpenstackController):
+class RequestExtensionController(common.OpenstackController):
def __init__(self, application):
self.application = application
@@ -148,20 +147,9 @@ class ResponseExtensionController(common.OpenstackController):
def process(self, req, *args, **kwargs):
res = req.get_response(self.application)
- content_type = req.best_match_content_type()
- # currently response handlers are un-ordered
+ # currently request handlers are un-ordered
for handler in self.handlers:
- res = handler(res)
- try:
- body = res.body
- headers = res.headers
- except AttributeError:
- default_xmlns = None
- body = self._serialize(res, content_type, default_xmlns)
- headers = {"Content-Type": content_type}
- res = webob.Response()
- res.body = body
- res.headers = headers
+ res = handler(req, res)
return res
@@ -226,24 +214,24 @@ class ExtensionMiddleware(wsgi.Middleware):
return action_controllers
- def _response_ext_controllers(self, application, ext_mgr, mapper):
- """Returns a dict of ResponseExtensionController-s by collection."""
- response_ext_controllers = {}
- for resp_ext in ext_mgr.get_response_extensions():
- if not resp_ext.key in response_ext_controllers.keys():
- controller = ResponseExtensionController(application)
- mapper.connect(resp_ext.url_route + '.:(format)',
+ def _request_ext_controllers(self, application, ext_mgr, mapper):
+ """Returns a dict of RequestExtensionController-s by collection."""
+ request_ext_controllers = {}
+ for req_ext in ext_mgr.get_request_extensions():
+ if not req_ext.key in request_ext_controllers.keys():
+ controller = RequestExtensionController(application)
+ mapper.connect(req_ext.url_route + '.:(format)',
action='process',
controller=controller,
- conditions=resp_ext.conditions)
+ conditions=req_ext.conditions)
- mapper.connect(resp_ext.url_route,
+ mapper.connect(req_ext.url_route,
action='process',
controller=controller,
- conditions=resp_ext.conditions)
- response_ext_controllers[resp_ext.key] = controller
+ conditions=req_ext.conditions)
+ request_ext_controllers[req_ext.key] = controller
- return response_ext_controllers
+ return request_ext_controllers
def __init__(self, application, ext_mgr=None):
@@ -271,13 +259,13 @@ class ExtensionMiddleware(wsgi.Middleware):
controller = action_controllers[action.collection]
controller.add_action(action.action_name, action.handler)
- # extended responses
- resp_controllers = self._response_ext_controllers(application, ext_mgr,
+ # extended requests
+ req_controllers = self._request_ext_controllers(application, ext_mgr,
mapper)
- for response_ext in ext_mgr.get_response_extensions():
- LOG.debug(_('Extended response: %s'), response_ext.key)
- controller = resp_controllers[response_ext.key]
- controller.add_handler(response_ext.handler)
+ for request_ext in ext_mgr.get_request_extensions():
+ LOG.debug(_('Extended request: %s'), request_ext.key)
+ controller = req_controllers[request_ext.key]
+ controller.add_handler(request_ext.handler)
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
mapper)
@@ -347,17 +335,17 @@ class ExtensionManager(object):
pass
return actions
- def get_response_extensions(self):
- """Returns a list of ResponseExtension objects."""
- response_exts = []
+ def get_request_extensions(self):
+ """Returns a list of RequestExtension objects."""
+ request_exts = []
for alias, ext in self.extensions.iteritems():
try:
- response_exts.extend(ext.get_response_extensions())
+ request_exts.extend(ext.get_request_extensions())
except AttributeError:
- # NOTE(dprince): Extension aren't required to have response
+ # NOTE(dprince): Extension aren't required to have request
# extensions
pass
- return response_exts
+ return request_exts
def _check_extension(self, extension):
"""Checks for required methods in extension objects."""
@@ -421,9 +409,13 @@ class ExtensionManager(object):
self.extensions[alias] = ext
-class ResponseExtension(object):
- """Add data to responses from core nova OpenStack API controllers."""
+class RequestExtension(object):
+ """Extend requests and responses of core nova OpenStack API controllers.
+ Provide a way to add data to responses and handle custom request data
+ that is sent to core nova OpenStack API controllers.
+
+ """
def __init__(self, method, url_route, handler):
self.url_route = url_route
self.handler = handler
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 227ffecdc..af73d8f6d 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -13,7 +13,12 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
+import urlparse
+
+from nova import crypto
from nova import db
+from nova import exception
from nova import flags
from nova import log as logging
from nova.api.openstack import common
@@ -21,6 +26,12 @@ from nova.scheduler import api
FLAGS = flags.FLAGS
+flags.DEFINE_string('build_plan_encryption_key',
+ None,
+ '128bit (hex) encryption key for scheduler build plans.')
+
+
+LOG = logging.getLogger('nova.api.openstack.zones')
def _filter_keys(item, keys):
@@ -97,3 +108,35 @@ class Controller(common.OpenstackController):
zone_id = int(id)
zone = api.zone_update(context, zone_id, env["zone"])
return dict(zone=_scrub_zone(zone))
+
+ def select(self, req):
+ """Returns a weighted list of costs to create instances
+ of desired capabilities."""
+ ctx = req.environ['nova.context']
+ qs = req.environ['QUERY_STRING']
+ param_dict = urlparse.parse_qs(qs)
+ param_dict.pop("fresh", None)
+ # parse_qs returns a dict where the values are lists,
+ # since query strings can have multiple values for the
+ # same key. We need to convert that to single values.
+ for key in param_dict:
+ param_dict[key] = param_dict[key][0]
+ build_plan = api.select(ctx, specs=param_dict)
+ cooked = self._scrub_build_plan(build_plan)
+ return {"weights": cooked}
+
+ def _scrub_build_plan(self, build_plan):
+ """Remove all the confidential data and return a sanitized
+ version of the build plan. Include an encrypted full version
+ of the weighting entry so we can get back to it later."""
+ if not FLAGS.build_plan_encryption_key:
+ raise exception.FlagNotSet(flag='build_plan_encryption_key')
+
+ encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key)
+ cooked = []
+ for entry in build_plan:
+ json_entry = json.dumps(entry)
+ cipher_text = encryptor(json_entry)
+ cooked.append(dict(weight=entry['weight'],
+ blob=cipher_text))
+ return cooked
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 2dc083e9e..29650e782 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -233,11 +233,18 @@ class API(base.Base):
uid = context.user_id
LOG.debug(_("Casting to scheduler for %(pid)s/%(uid)s's"
" instance %(instance_id)s") % locals())
+
+ # NOTE(sandy): For now we're just going to pass in the
+ # instance_type record to the scheduler. In a later phase
+ # we'll be ripping this whole for-loop out and deferring the
+ # creation of the Instance record. At that point all this will
+ # change.
rpc.cast(context,
FLAGS.scheduler_topic,
{"method": "run_instance",
"args": {"topic": FLAGS.compute_topic,
"instance_id": instance_id,
+ "instance_type": instance_type,
"availability_zone": availability_zone,
"injected_files": injected_files}})
diff --git a/nova/crypto.py b/nova/crypto.py
index 14b9cbef6..bdc32482a 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -332,6 +332,51 @@ def mkcacert(subject='nova', years=1):
return cert, pk, pkey
+def _build_cipher(key, iv, encode=True):
+ """Make a 128bit AES CBC encode/decode Cipher object.
+ Padding is handled internally."""
+ operation = 1 if encode else 0
+ return M2Crypto.EVP.Cipher(alg='aes_128_cbc', key=key, iv=iv, op=operation)
+
+
+def encryptor(key, iv=None):
+ """Simple symmetric key encryption."""
+ key = base64.b64decode(key)
+ if iv is None:
+ iv = '\0' * 16
+ else:
+ iv = base64.b64decode(iv)
+
+ def encrypt(data):
+ cipher = _build_cipher(key, iv, encode=True)
+ v = cipher.update(data)
+ v = v + cipher.final()
+ del cipher
+ v = base64.b64encode(v)
+ return v
+
+ return encrypt
+
+
+def decryptor(key, iv=None):
+ """Simple symmetric key decryption."""
+ key = base64.b64decode(key)
+ if iv is None:
+ iv = '\0' * 16
+ else:
+ iv = base64.b64decode(iv)
+
+ def decrypt(data):
+ data = base64.b64decode(data)
+ cipher = _build_cipher(key, iv, encode=False)
+ v = cipher.update(data)
+ v = v + cipher.final()
+ del cipher
+ return v
+
+ return decrypt
+
+
# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
#
# Permission is hereby granted, free of charge, to any person obtaining a
diff --git a/nova/exception.py b/nova/exception.py
index 020243220..010ec8166 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -259,6 +259,10 @@ class NotFound(NovaException):
super(NotFound, self).__init__(**kwargs)
+class FlagNotSet(NotFound):
+ message = _("Required flag %(flag)s not set.")
+
+
class InstanceNotFound(NotFound):
message = _("Instance %(instance_id)s could not be found.")
diff --git a/nova/flags.py b/nova/flags.py
index 32cb6efa8..9eaac5596 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -110,7 +110,7 @@ class FlagValues(gflags.FlagValues):
return name in self.__dict__['__dirty']
def ClearDirty(self):
- self.__dict__['__is_dirty'] = []
+ self.__dict__['__dirty'] = []
def WasAlreadyParsed(self):
return self.__dict__['__was_already_parsed']
@@ -119,11 +119,12 @@ class FlagValues(gflags.FlagValues):
if '__stored_argv' not in self.__dict__:
return
new_flags = FlagValues(self)
- for k in self.__dict__['__dirty']:
+ for k in self.FlagDict().iterkeys():
new_flags[k] = gflags.FlagValues.__getitem__(self, k)
+ new_flags.Reset()
new_flags(self.__dict__['__stored_argv'])
- for k in self.__dict__['__dirty']:
+ for k in new_flags.FlagDict().iterkeys():
setattr(self, k, getattr(new_flags, k))
self.ClearDirty()
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 51bd6f2b2..d5ea49ecb 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -27,6 +27,7 @@ from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
+from IPy import IP
LOG = logging.getLogger("nova.linux_net")
@@ -449,6 +450,7 @@ def ensure_vlan_bridge(vlan_num, bridge, bridge_interface, net_attrs=None):
ensure_bridge(bridge, interface, net_attrs)
+@utils.synchronized('ensure_vlan', external=True)
def ensure_vlan(vlan_num, bridge_interface):
"""Create a vlan unless it already exists"""
interface = 'vlan%s' % vlan_num
@@ -698,6 +700,7 @@ def _dnsmasq_cmd(net):
'--listen-address=%s' % net['gateway'],
'--except-interface=lo',
'--dhcp-range=%s,static,120s' % net['dhcp_start'],
+ '--dhcp-lease-max=%s' % IP(net['cidr']).len(),
'--dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'),
'--dhcp-script=%s' % FLAGS.dhcpbridge,
'--leasefile-ro']
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 816ae5513..55f8e0a6d 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -81,6 +81,12 @@ def get_zone_capabilities(context):
return _call_scheduler('get_zone_capabilities', context=context)
+def select(context, specs=None):
+ """Returns a list of hosts."""
+ return _call_scheduler('select', context=context,
+ params={"specs": specs})
+
+
def update_service_capabilities(context, service_name, host, capabilities):
"""Send an update to all the scheduler services informing them
of the capabilities of this service."""
@@ -105,6 +111,45 @@ def _process(func, zone):
return func(nova, zone)
+def call_zone_method(context, method, errors_to_ignore=None, *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
+ errors_to_ignore = [errors_to_ignore]
+
+ pool = greenpool.GreenPool()
+ results = []
+ for zone in db.zone_get_all(context):
+ try:
+ nova = novaclient.OpenStack(zone.username, zone.password,
+ zone.api_url)
+ nova.authenticate()
+ except novaclient.exceptions.BadRequest, e:
+ url = zone.api_url
+ LOG.warn(_("Failed request to zone; URL=%(url)s: %(e)s")
+ % locals())
+ #TODO (dabo) - add logic for failure counts per zone,
+ # with escalation after a given number of failures.
+ continue
+ zone_method = getattr(nova.zones, method)
+
+ def _error_trap(*args, **kwargs):
+ try:
+ return zone_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
+
+ res = pool.spawn(_error_trap, *args, **kwargs)
+ results.append((zone, res))
+ pool.waitall()
+ return [(zone.id, res.wait()) for zone, res in results]
+
+
def child_zone_helper(zone_list, func):
"""Fire off a command to each zone in the list.
The return is [novaclient return objects] from each child zone.
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
new file mode 100644
index 000000000..b3d230bd2
--- /dev/null
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -0,0 +1,119 @@
+# Copyright (c) 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.
+
+"""
+The Zone Aware Scheduler is a base class Scheduler for creating instances
+across zones. There are two expansion points to this class for:
+1. Assigning Weights to hosts for requested instances
+2. Filtering Hosts based on required instance capabilities
+"""
+
+import operator
+
+from nova import log as logging
+from nova.scheduler import api
+from nova.scheduler import driver
+
+LOG = logging.getLogger('nova.scheduler.zone_aware_scheduler')
+
+
+class ZoneAwareScheduler(driver.Scheduler):
+ """Base class for creating Zone Aware Schedulers."""
+
+ def _call_zone_method(self, context, method, specs):
+ """Call novaclient zone method. Broken out for testing."""
+ return api.call_zone_method(context, method, specs=specs)
+
+ def schedule_run_instance(self, context, topic='compute', specs={},
+ *args, **kwargs):
+ """This method is called from nova.compute.api to provision
+ an instance. However we need to look at the parameters being
+ passed in to see if this is a request to:
+ 1. Create a Build Plan and then provision, or
+ 2. Use the Build Plan information in the request parameters
+ to simply create the instance (either in this zone or
+ a child zone)."""
+
+ if 'blob' in specs:
+ return self.provision_instance(context, topic, specs)
+
+ # Create build plan and provision ...
+ build_plan = self.select(context, specs)
+ for item in build_plan:
+ self.provision_instance(context, topic, item)
+
+ def provision_instance(context, topic, item):
+ """Create the requested instance in this Zone or a child zone."""
+ pass
+
+ def select(self, context, *args, **kwargs):
+ """Select returns a list of weights and zone/host information
+ corresponding to the best hosts to service the request. Any
+ child zone information has been encrypted so as not to reveal
+ anything about the children."""
+ return self._schedule(context, "compute", *args, **kwargs)
+
+ def schedule(self, context, topic, *args, **kwargs):
+ """The schedule() contract requires we return the one
+ best-suited host for this request.
+ """
+ res = self._schedule(context, topic, *args, **kwargs)
+ # TODO(sirp): should this be a host object rather than a weight-dict?
+ if not res:
+ raise driver.NoValidHost(_('No hosts were available'))
+ return res[0]
+
+ def _schedule(self, context, topic, *args, **kwargs):
+ """Returns a list of hosts that meet the required specs,
+ ordered by their fitness.
+ """
+
+ #TODO(sandy): extract these from args.
+ num_instances = 1
+ specs = {}
+
+ # Filter local hosts based on requirements ...
+ host_list = self.filter_hosts(num_instances, specs)
+
+ # then weigh the selected hosts.
+ # weighted = [{weight=weight, name=hostname}, ...]
+ weighted = self.weigh_hosts(num_instances, specs, host_list)
+
+ # Next, tack on the best weights from the child zones ...
+ child_results = self._call_zone_method(context, "select",
+ specs=specs)
+ for child_zone, result in child_results:
+ for weighting in result:
+ # Remember the child_zone so we can get back to
+ # it later if needed. This implicitly builds a zone
+ # path structure.
+ host_dict = {
+ "weight": weighting["weight"],
+ "child_zone": child_zone,
+ "child_blob": weighting["blob"]}
+ weighted.append(host_dict)
+
+ weighted.sort(key=operator.itemgetter('weight'))
+ return weighted
+
+ def filter_hosts(self, num, specs):
+ """Derived classes must override this method and return
+ a list of hosts in [(hostname, capability_dict)] format."""
+ raise NotImplemented()
+
+ def weigh_hosts(self, num, specs, hosts):
+ """Derived classes must override this method and return
+ a lists of hosts in [{weight, hostname}] format."""
+ raise NotImplemented()
diff --git a/nova/service.py b/nova/service.py
index 2532b9df2..ab1238c3b 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -240,6 +240,10 @@ class WsgiService(object):
def wait(self):
self.wsgi_app.wait()
+ def get_socket_info(self, api_name):
+ """Returns the (host, port) that an API was started on."""
+ return self.wsgi_app.socket_info[api_name]
+
class ApiService(WsgiService):
"""Class for our nova-api service."""
@@ -318,8 +322,10 @@ def _run_wsgi(paste_config_file, apis):
logging.debug(_('App Config: %(api)s\n%(config)r') % locals())
logging.info(_('Running %s API'), api)
app = wsgi.load_paste_app(paste_config_file, api)
- apps.append((app, getattr(FLAGS, '%s_listen_port' % api),
- getattr(FLAGS, '%s_listen' % api)))
+ apps.append((app,
+ getattr(FLAGS, '%s_listen_port' % api),
+ getattr(FLAGS, '%s_listen' % api),
+ api))
if len(apps) == 0:
logging.error(_('No known API applications configured in %s.'),
paste_config_file)
diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py
index 0860b51ac..dbdd0928a 100644
--- a/nova/tests/api/openstack/extensions/foxinsocks.py
+++ b/nova/tests/api/openstack/extensions/foxinsocks.py
@@ -63,31 +63,33 @@ class Foxinsocks(object):
self._delete_tweedle))
return actions
- def get_response_extensions(self):
- response_exts = []
+ def get_request_extensions(self):
+ request_exts = []
- def _goose_handler(res):
+ def _goose_handler(req, res):
#NOTE: This only handles JSON responses.
# You can use content type header to test for XML.
data = json.loads(res.body)
- data['flavor']['googoose'] = "Gooey goo for chewy chewing!"
- return data
+ data['flavor']['googoose'] = req.GET.get('chewing')
+ res.body = json.dumps(data)
+ return res
- resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)',
+ req_ext1 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
_goose_handler)
- response_exts.append(resp_ext)
+ request_exts.append(req_ext1)
- def _bands_handler(res):
+ def _bands_handler(req, res):
#NOTE: This only handles JSON responses.
# You can use content type header to test for XML.
data = json.loads(res.body)
data['big_bands'] = 'Pig Bands!'
- return data
+ res.body = json.dumps(data)
+ return res
- resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)',
+ req_ext2 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
_bands_handler)
- response_exts.append(resp_ext2)
- return response_exts
+ request_exts.append(req_ext2)
+ return request_exts
def _add_tweedle(self, input_dict, req, id):
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 481d34ed1..544298602 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -45,10 +45,10 @@ class StubController(nova.wsgi.Controller):
class StubExtensionManager(object):
- def __init__(self, resource_ext=None, action_ext=None, response_ext=None):
+ def __init__(self, resource_ext=None, action_ext=None, request_ext=None):
self.resource_ext = resource_ext
self.action_ext = action_ext
- self.response_ext = response_ext
+ self.request_ext = request_ext
def get_name(self):
return "Tweedle Beetle Extension"
@@ -71,11 +71,11 @@ class StubExtensionManager(object):
action_exts.append(self.action_ext)
return action_exts
- def get_response_extensions(self):
- response_exts = []
- if self.response_ext:
- response_exts.append(self.response_ext)
- return response_exts
+ def get_request_extensions(self):
+ request_extensions = []
+ if self.request_ext:
+ request_extensions.append(self.request_ext)
+ return request_extensions
class ExtensionControllerTest(unittest.TestCase):
@@ -183,10 +183,10 @@ class ActionExtensionTest(unittest.TestCase):
self.assertEqual(404, response.status_int)
-class ResponseExtensionTest(unittest.TestCase):
+class RequestExtensionTest(unittest.TestCase):
def setUp(self):
- super(ResponseExtensionTest, self).setUp()
+ super(RequestExtensionTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
fakes.FakeAuthManager.reset_fake_data()
fakes.FakeAuthDatabase.data = {}
@@ -195,42 +195,39 @@ class ResponseExtensionTest(unittest.TestCase):
def tearDown(self):
self.stubs.UnsetAll()
- super(ResponseExtensionTest, self).tearDown()
+ super(RequestExtensionTest, self).tearDown()
def test_get_resources_with_stub_mgr(self):
- test_resp = "Gooey goo for chewy chewing!"
-
- def _resp_handler(res):
+ def _req_handler(req, res):
# only handle JSON responses
data = json.loads(res.body)
- data['flavor']['googoose'] = test_resp
- return data
+ data['flavor']['googoose'] = req.GET.get('chewing')
+ res.body = json.dumps(data)
+ return res
- resp_ext = extensions.ResponseExtension('GET',
+ req_ext = extensions.RequestExtension('GET',
'/v1.1/flavors/:(id)',
- _resp_handler)
+ _req_handler)
- manager = StubExtensionManager(None, None, resp_ext)
+ manager = StubExtensionManager(None, None, req_ext)
app = fakes.wsgi_app()
ext_midware = extensions.ExtensionMiddleware(app, manager)
- request = webob.Request.blank("/v1.1/flavors/1")
+ request = webob.Request.blank("/v1.1/flavors/1?chewing=bluegoo")
request.environ['api.version'] = '1.1'
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
response_data = json.loads(response.body)
- self.assertEqual(test_resp, response_data['flavor']['googoose'])
+ self.assertEqual('bluegoo', response_data['flavor']['googoose'])
def test_get_resources_with_mgr(self):
- test_resp = "Gooey goo for chewy chewing!"
-
app = fakes.wsgi_app()
ext_midware = extensions.ExtensionMiddleware(app)
- request = webob.Request.blank("/v1.1/flavors/1")
+ request = webob.Request.blank("/v1.1/flavors/1?chewing=newblue")
request.environ['api.version'] = '1.1'
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
response_data = json.loads(response.body)
- self.assertEqual(test_resp, response_data['flavor']['googoose'])
+ self.assertEqual('newblue', response_data['flavor']['googoose'])
self.assertEqual("Pig Bands!", response_data['big_bands'])
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 5d5799b59..fa2e05033 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -20,6 +20,8 @@ import json
import nova.db
from nova import context
+from nova import crypto
+from nova import exception
from nova import flags
from nova import test
from nova.api.openstack import zones
@@ -79,6 +81,18 @@ def zone_capabilities(method, context):
return dict()
+GLOBAL_BUILD_PLAN = [
+ dict(name='host1', weight=10, ip='10.0.0.1', zone='zone1'),
+ dict(name='host2', weight=9, ip='10.0.0.2', zone='zone2'),
+ dict(name='host3', weight=8, ip='10.0.0.3', zone='zone3'),
+ dict(name='host4', weight=7, ip='10.0.0.4', zone='zone4'),
+ ]
+
+
+def zone_select(context, specs):
+ return GLOBAL_BUILD_PLAN
+
+
class ZonesTest(test.TestCase):
def setUp(self):
super(ZonesTest, self).setUp()
@@ -190,3 +204,31 @@ class ZonesTest(test.TestCase):
self.assertEqual(res_dict['zone']['name'], 'darksecret')
self.assertEqual(res_dict['zone']['cap1'], 'a;b')
self.assertEqual(res_dict['zone']['cap2'], 'c;d')
+
+ def test_zone_select(self):
+ FLAGS.build_plan_encryption_key = 'c286696d887c9aa0611bbb3e2025a45a'
+ self.stubs.Set(api, 'select', zone_select)
+
+ req = webob.Request.blank('/v1.0/zones/select')
+
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+
+ self.assertTrue('weights' in res_dict)
+
+ for item in res_dict['weights']:
+ blob = item['blob']
+ decrypt = crypto.decryptor(FLAGS.build_plan_encryption_key)
+ secret_item = json.loads(decrypt(blob))
+ found = False
+ for original_item in GLOBAL_BUILD_PLAN:
+ if original_item['name'] != secret_item['name']:
+ continue
+ found = True
+ for key in ('weight', 'ip', 'zone'):
+ self.assertEqual(secret_item[key], original_item[key])
+
+ self.assertTrue(found)
+ self.assertEqual(len(item), 2)
+ self.assertTrue('weight' in item)
diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py
index 5d7ca98b5..ecefc464a 100644
--- a/nova/tests/fake_flags.py
+++ b/nova/tests/fake_flags.py
@@ -21,24 +21,24 @@ from nova import flags
FLAGS = flags.FLAGS
flags.DECLARE('volume_driver', 'nova.volume.manager')
-FLAGS.volume_driver = 'nova.volume.driver.FakeISCSIDriver'
-FLAGS.connection_type = 'fake'
-FLAGS.fake_rabbit = True
+FLAGS['volume_driver'].SetDefault('nova.volume.driver.FakeISCSIDriver')
+FLAGS['connection_type'].SetDefault('fake')
+FLAGS['fake_rabbit'].SetDefault(True)
flags.DECLARE('auth_driver', 'nova.auth.manager')
-FLAGS.auth_driver = 'nova.auth.dbdriver.DbDriver'
+FLAGS['auth_driver'].SetDefault('nova.auth.dbdriver.DbDriver')
flags.DECLARE('network_size', 'nova.network.manager')
flags.DECLARE('num_networks', 'nova.network.manager')
flags.DECLARE('fake_network', 'nova.network.manager')
-FLAGS.network_size = 8
-FLAGS.num_networks = 2
-FLAGS.fake_network = True
-FLAGS.image_service = 'nova.image.local.LocalImageService'
+FLAGS['network_size'].SetDefault(8)
+FLAGS['num_networks'].SetDefault(2)
+FLAGS['fake_network'].SetDefault(True)
+FLAGS['image_service'].SetDefault('nova.image.local.LocalImageService')
flags.DECLARE('num_shelves', 'nova.volume.driver')
flags.DECLARE('blades_per_shelf', 'nova.volume.driver')
flags.DECLARE('iscsi_num_targets', 'nova.volume.driver')
-FLAGS.num_shelves = 2
-FLAGS.blades_per_shelf = 4
-FLAGS.iscsi_num_targets = 8
-FLAGS.verbose = True
-FLAGS.sqlite_db = "tests.sqlite"
-FLAGS.use_ipv6 = True
+FLAGS['num_shelves'].SetDefault(2)
+FLAGS['blades_per_shelf'].SetDefault(4)
+FLAGS['iscsi_num_targets'].SetDefault(8)
+FLAGS['verbose'].SetDefault(True)
+FLAGS['sqlite_db'].SetDefault("tests.sqlite")
+FLAGS['use_ipv6'].SetDefault(True)
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index 2e5d67017..bc98921f0 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -160,7 +160,7 @@ class _IntegratedTestBase(test.TestCase):
#self.start_service('network')
self.start_service('scheduler')
- self.auth_url = self._start_api_service()
+ self._start_api_service()
self.context = IntegratedUnitTestContext(self.auth_url)
@@ -174,8 +174,10 @@ class _IntegratedTestBase(test.TestCase):
if not api_service:
raise Exception("API Service was None")
- auth_url = 'http://localhost:8774/v1.1'
- return auth_url
+ self.api_service = api_service
+
+ host, port = api_service.get_socket_info('osapi')
+ self.auth_url = 'http://%s:%s/v1.1' % (host, port)
def tearDown(self):
self.context.cleanup()
@@ -184,6 +186,11 @@ class _IntegratedTestBase(test.TestCase):
def _get_flags(self):
"""An opportunity to setup flags, before the services are started."""
f = {}
+
+ # Auto-assign ports to allow concurrent tests
+ f['ec2_listen_port'] = 0
+ f['osapi_listen_port'] = 0
+
f['image_service'] = 'nova.image.fake.FakeImageService'
f['fake_network'] = True
return f
diff --git a/nova/tests/public_key/dummy.fingerprint b/nova/tests/public_key/dummy.fingerprint
new file mode 100644
index 000000000..715bca27a
--- /dev/null
+++ b/nova/tests/public_key/dummy.fingerprint
@@ -0,0 +1 @@
+1c:87:d1:d9:32:fd:62:3c:78:2b:c0:ad:c0:15:88:df
diff --git a/nova/tests/public_key/dummy.pub b/nova/tests/public_key/dummy.pub
new file mode 100644
index 000000000..d4cf2bc0d
--- /dev/null
+++ b/nova/tests/public_key/dummy.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAMGJlY9XEIm2X234pdO5yFWMp2JuOQx8U0E815IVXhmKxYCBK9ZakgZOIQmPbXoGYyV+mziDPp6HJ0wKYLQxkwLEFr51fAZjWQvRss0SinURRuLkockDfGFtD4pYJthekr/rlqMKlBSDUSpGq8jUWW60UJ18FGooFpxR7ESqQRx/AAAAFQC96LRglaUeeP+E8U/yblEJocuiWwAAAIA3XiMR8Skiz/0aBm5K50SeQznQuMJTyzt9S9uaz5QZWiFu69hOyGSFGw8fqgxEkXFJIuHobQQpGYQubLW0NdaYRqyE/Vud3JUJUb8Texld6dz8vGemyB5d1YvtSeHIo8/BGv2msOqR3u5AZTaGCBD9DhpSGOKHEdNjTtvpPd8S8gAAAIBociGZ5jf09iHLVENhyXujJbxfGRPsyNTyARJfCOGl0oFV6hEzcQyw8U/ePwjgvjc2UizMWLl8tsb2FXKHRdc2v+ND3Us+XqKQ33X3ADP4FZ/+Oj213gMyhCmvFTP0u5FmHog9My4CB7YcIWRuUR42WlhQ2IfPvKwUoTk3R+T6Og== www-data@mk
diff --git a/nova/tests/real_flags.py b/nova/tests/real_flags.py
deleted file mode 100644
index 71da04992..000000000
--- a/nova/tests/real_flags.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# 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.
-
-from nova import flags
-
-FLAGS = flags.FLAGS
-
-FLAGS.connection_type = 'libvirt'
-FLAGS.fake_rabbit = False
-FLAGS.fake_network = False
-FLAGS.verbose = False
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index 97f401b87..7c0331eff 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -224,6 +224,29 @@ class ApiEc2TestCase(test.TestCase):
self.manager.delete_project(project)
self.manager.delete_user(user)
+ def test_create_duplicate_key_pair(self):
+ """Test that, after successfully generating a keypair,
+ requesting a second keypair with the same name fails sanely"""
+ self.expect_http()
+ self.mox.ReplayAll()
+ keyname = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
+ for x in range(random.randint(4, 8)))
+ user = self.manager.create_user('fake', 'fake', 'fake')
+ project = self.manager.create_project('fake', 'fake', 'fake')
+ # NOTE(vish): create depends on pool, so call helper directly
+ self.ec2.create_key_pair('test')
+
+ try:
+ self.ec2.create_key_pair('test')
+ except EC2ResponseError, e:
+ if e.code == 'KeyPairExists':
+ pass
+ else:
+ self.fail("Unexpected EC2ResponseError: %s "
+ "(expected KeyPairExists)" % e.code)
+ else:
+ self.fail('Exception not raised.')
+
def test_get_all_security_groups(self):
"""Test that we can retrieve security groups"""
self.expect_http()
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index c8559615a..54c0454de 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -354,6 +354,36 @@ class CloudTestCase(test.TestCase):
self.assertTrue(filter(lambda k: k['keyName'] == 'test1', keys))
self.assertTrue(filter(lambda k: k['keyName'] == 'test2', keys))
+ def test_import_public_key(self):
+ # test when user provides all values
+ result1 = self.cloud.import_public_key(self.context,
+ 'testimportkey1',
+ 'mytestpubkey',
+ 'mytestfprint')
+ self.assertTrue(result1)
+ keydata = db.key_pair_get(self.context,
+ self.context.user.id,
+ 'testimportkey1')
+ self.assertEqual('mytestpubkey', keydata['public_key'])
+ self.assertEqual('mytestfprint', keydata['fingerprint'])
+ # test when user omits fingerprint
+ pubkey_path = os.path.join(os.path.dirname(__file__), 'public_key')
+ f = open(pubkey_path + '/dummy.pub', 'r')
+ dummypub = f.readline().rstrip()
+ f.close
+ f = open(pubkey_path + '/dummy.fingerprint', 'r')
+ dummyfprint = f.readline().rstrip()
+ f.close
+ result2 = self.cloud.import_public_key(self.context,
+ 'testimportkey2',
+ dummypub)
+ self.assertTrue(result2)
+ keydata = db.key_pair_get(self.context,
+ self.context.user.id,
+ 'testimportkey2')
+ self.assertEqual(dummypub, keydata['public_key'])
+ self.assertEqual(dummyfprint, keydata['fingerprint'])
+
def test_delete_key_pair(self):
self._create_key('test')
self.cloud.delete_key_pair(self.context, 'test')
diff --git a/nova/tests/test_crypto.py b/nova/tests/test_crypto.py
new file mode 100644
index 000000000..945d78794
--- /dev/null
+++ b/nova/tests/test_crypto.py
@@ -0,0 +1,48 @@
+# 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.
+"""
+Tests for Crypto module.
+"""
+
+from nova import crypto
+from nova import test
+
+
+class SymmetricKeyTestCase(test.TestCase):
+ """Test case for Encrypt/Decrypt"""
+ def test_encrypt_decrypt(self):
+ key = 'c286696d887c9aa0611bbb3e2025a45a'
+ plain_text = "The quick brown fox jumped over the lazy dog."
+
+ # No IV supplied (all 0's)
+ encrypt = crypto.encryptor(key)
+ cipher_text = encrypt(plain_text)
+ self.assertNotEquals(plain_text, cipher_text)
+
+ decrypt = crypto.decryptor(key)
+ plain = decrypt(cipher_text)
+
+ self.assertEquals(plain_text, plain)
+
+ # IV supplied ...
+ iv = '562e17996d093d28ddb3ba695a2e6f58'
+ encrypt = crypto.encryptor(key, iv)
+ cipher_text = encrypt(plain_text)
+ self.assertNotEquals(plain_text, cipher_text)
+
+ decrypt = crypto.decryptor(key, iv)
+ plain = decrypt(cipher_text)
+
+ self.assertEquals(plain_text, plain)
diff --git a/nova/tests/test_flags.py b/nova/tests/test_flags.py
index 707300fcf..05319d91f 100644
--- a/nova/tests/test_flags.py
+++ b/nova/tests/test_flags.py
@@ -91,6 +91,20 @@ class FlagsTestCase(test.TestCase):
self.assert_('runtime_answer' in self.global_FLAGS)
self.assertEqual(self.global_FLAGS.runtime_answer, 60)
+ def test_long_vs_short_flags(self):
+ flags.DEFINE_string('duplicate_answer_long', 'val', 'desc',
+ flag_values=self.global_FLAGS)
+ argv = ['flags_test', '--duplicate_answer=60', 'extra_arg']
+ args = self.global_FLAGS(argv)
+
+ self.assert_('duplicate_answer' not in self.global_FLAGS)
+ self.assert_(self.global_FLAGS.duplicate_answer_long, 60)
+
+ flags.DEFINE_integer('duplicate_answer', 60, 'desc',
+ flag_values=self.global_FLAGS)
+ self.assertEqual(self.global_FLAGS.duplicate_answer, 60)
+ self.assertEqual(self.global_FLAGS.duplicate_answer_long, 'val')
+
def test_flag_leak_left(self):
self.assertEqual(FLAGS.flags_unittest, 'foo')
FLAGS.flags_unittest = 'bar'
diff --git a/nova/tests/test_scheduler.py b/nova/tests/test_scheduler.py
index 968ef9d6c..54b3f80fb 100644
--- a/nova/tests/test_scheduler.py
+++ b/nova/tests/test_scheduler.py
@@ -912,7 +912,8 @@ class SimpleDriverTestCase(test.TestCase):
class FakeZone(object):
- def __init__(self, api_url, username, password):
+ def __init__(self, id, api_url, username, password):
+ self.id = id
self.api_url = api_url
self.username = username
self.password = password
@@ -920,7 +921,7 @@ class FakeZone(object):
def zone_get_all(context):
return [
- FakeZone('http://example.com', 'bob', 'xxx'),
+ FakeZone(1, 'http://example.com', 'bob', 'xxx'),
]
@@ -1037,7 +1038,7 @@ class FakeNovaClient(object):
class DynamicNovaClientTest(test.TestCase):
def test_issue_novaclient_command_found(self):
- zone = FakeZone('http://example.com', 'bob', 'xxx')
+ zone = FakeZone(1, 'http://example.com', 'bob', 'xxx')
self.assertEquals(api._issue_novaclient_command(
FakeNovaClient(FakeServerCollection()),
zone, "servers", "get", 100).a, 10)
@@ -1051,7 +1052,7 @@ class DynamicNovaClientTest(test.TestCase):
zone, "servers", "pause", 100), None)
def test_issue_novaclient_command_not_found(self):
- zone = FakeZone('http://example.com', 'bob', 'xxx')
+ zone = FakeZone(1, 'http://example.com', 'bob', 'xxx')
self.assertEquals(api._issue_novaclient_command(
FakeNovaClient(FakeEmptyServerCollection()),
zone, "servers", "get", 100), None)
@@ -1063,3 +1064,55 @@ class DynamicNovaClientTest(test.TestCase):
self.assertEquals(api._issue_novaclient_command(
FakeNovaClient(FakeEmptyServerCollection()),
zone, "servers", "any", "name"), None)
+
+
+class FakeZonesProxy(object):
+ def do_something(*args, **kwargs):
+ return 42
+
+ def raises_exception(*args, **kwargs):
+ raise Exception('testing')
+
+
+class FakeNovaClientOpenStack(object):
+ def __init__(self, *args, **kwargs):
+ self.zones = FakeZonesProxy()
+
+ def authenticate(self):
+ pass
+
+
+class CallZoneMethodTest(test.TestCase):
+ def setUp(self):
+ super(CallZoneMethodTest, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ self.stubs.Set(db, 'zone_get_all', zone_get_all)
+ self.stubs.Set(novaclient, 'OpenStack', FakeNovaClientOpenStack)
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+ super(CallZoneMethodTest, self).tearDown()
+
+ def test_call_zone_method(self):
+ context = {}
+ method = 'do_something'
+ results = api.call_zone_method(context, method)
+ expected = [(1, 42)]
+ self.assertEqual(expected, results)
+
+ def test_call_zone_method_not_present(self):
+ context = {}
+ method = 'not_present'
+ self.assertRaises(AttributeError, api.call_zone_method,
+ context, method)
+
+ 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)
diff --git a/nova/tests/test_zone_aware_scheduler.py b/nova/tests/test_zone_aware_scheduler.py
new file mode 100644
index 000000000..fdcde34c9
--- /dev/null
+++ b/nova/tests/test_zone_aware_scheduler.py
@@ -0,0 +1,119 @@
+# 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.
+"""
+Tests For Zone Aware Scheduler.
+"""
+
+from nova import test
+from nova.scheduler import driver
+from nova.scheduler import zone_aware_scheduler
+from nova.scheduler import zone_manager
+
+
+class FakeZoneAwareScheduler(zone_aware_scheduler.ZoneAwareScheduler):
+ def filter_hosts(self, num, specs):
+ # NOTE(sirp): this is returning [(hostname, services)]
+ return self.zone_manager.service_states.items()
+
+ def weigh_hosts(self, num, specs, hosts):
+ fake_weight = 99
+ weighted = []
+ for hostname, caps in hosts:
+ weighted.append(dict(weight=fake_weight, name=hostname))
+ return weighted
+
+
+class FakeZoneManager(zone_manager.ZoneManager):
+ def __init__(self):
+ self.service_states = {
+ 'host1': {
+ 'compute': {'ram': 1000}
+ },
+ 'host2': {
+ 'compute': {'ram': 2000}
+ },
+ 'host3': {
+ 'compute': {'ram': 3000}
+ }
+ }
+
+
+class FakeEmptyZoneManager(zone_manager.ZoneManager):
+ def __init__(self):
+ self.service_states = {}
+
+
+def fake_empty_call_zone_method(context, method, specs):
+ return []
+
+
+def fake_call_zone_method(context, method, specs):
+ return [
+ ('zone1', [
+ dict(weight=1, blob='AAAAAAA'),
+ dict(weight=111, blob='BBBBBBB'),
+ dict(weight=112, blob='CCCCCCC'),
+ dict(weight=113, blob='DDDDDDD'),
+ ]),
+ ('zone2', [
+ dict(weight=120, blob='EEEEEEE'),
+ dict(weight=2, blob='FFFFFFF'),
+ dict(weight=122, blob='GGGGGGG'),
+ dict(weight=123, blob='HHHHHHH'),
+ ]),
+ ('zone3', [
+ dict(weight=130, blob='IIIIIII'),
+ dict(weight=131, blob='JJJJJJJ'),
+ dict(weight=132, blob='KKKKKKK'),
+ dict(weight=3, blob='LLLLLLL'),
+ ]),
+ ]
+
+
+class ZoneAwareSchedulerTestCase(test.TestCase):
+ """Test case for Zone Aware Scheduler."""
+
+ def test_zone_aware_scheduler(self):
+ """
+ Create a nested set of FakeZones, ensure that a select call returns the
+ appropriate build plan.
+ """
+ sched = FakeZoneAwareScheduler()
+ self.stubs.Set(sched, '_call_zone_method', fake_call_zone_method)
+
+ zm = FakeZoneManager()
+ sched.set_zone_manager(zm)
+
+ fake_context = {}
+ build_plan = sched.select(fake_context, {})
+
+ self.assertEqual(15, len(build_plan))
+
+ hostnames = [plan_item['name']
+ for plan_item in build_plan if 'name' in plan_item]
+ self.assertEqual(3, len(hostnames))
+
+ def test_empty_zone_aware_scheduler(self):
+ """
+ Ensure empty hosts & child_zones result in NoValidHosts exception.
+ """
+ sched = FakeZoneAwareScheduler()
+ self.stubs.Set(sched, '_call_zone_method', fake_empty_call_zone_method)
+
+ zm = FakeEmptyZoneManager()
+ sched.set_zone_manager(zm)
+
+ fake_context = {}
+ self.assertRaises(driver.NoValidHost, sched.schedule, fake_context, {})
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
index ddea1a1f7..f8aea1f34 100644
--- a/nova/virt/disk.py
+++ b/nova/virt/disk.py
@@ -81,34 +81,36 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
else:
mapped_device = device
- # We can only loopback mount raw images. If the device isn't there,
- # it's normally because it's a .vmdk or a .vdi etc
- if not os.path.exists(mapped_device):
- raise exception.Error('Mapped device was not found (we can'
- ' only inject raw disk images): %s' %
- mapped_device)
-
- # Configure ext2fs so that it doesn't auto-check every N boots
- out, err = utils.execute('sudo', 'tune2fs',
- '-c', 0, '-i', 0, mapped_device)
-
- tmpdir = tempfile.mkdtemp()
try:
- # mount loopback to dir
- out, err = utils.execute(
- 'sudo', 'mount', mapped_device, tmpdir)
- if err:
- raise exception.Error(_('Failed to mount filesystem: %s')
- % err)
-
+ # We can only loopback mount raw images. If the device isn't there,
+ # it's normally because it's a .vmdk or a .vdi etc
+ if not os.path.exists(mapped_device):
+ raise exception.Error('Mapped device was not found (we can'
+ ' only inject raw disk images): %s' %
+ mapped_device)
+
+ # Configure ext2fs so that it doesn't auto-check every N boots
+ out, err = utils.execute('sudo', 'tune2fs',
+ '-c', 0, '-i', 0, mapped_device)
+
+ tmpdir = tempfile.mkdtemp()
try:
- inject_data_into_fs(tmpdir, key, net, utils.execute)
+ # mount loopback to dir
+ out, err = utils.execute(
+ 'sudo', 'mount', mapped_device, tmpdir)
+ if err:
+ raise exception.Error(_('Failed to mount filesystem: %s')
+ % err)
+
+ try:
+ inject_data_into_fs(tmpdir, key, net, utils.execute)
+ finally:
+ # unmount device
+ utils.execute('sudo', 'umount', mapped_device)
finally:
- # unmount device
- utils.execute('sudo', 'umount', mapped_device)
+ # remove temporary directory
+ utils.execute('rmdir', tmpdir)
finally:
- # remove temporary directory
- utils.execute('rmdir', tmpdir)
if not partition is None:
# remove partitions
utils.execute('sudo', 'kpartx', '-d', device)
diff --git a/nova/virt/images.py b/nova/virt/images.py
index 2e3f2ee4d..02c898fda 100644
--- a/nova/virt/images.py
+++ b/nova/virt/images.py
@@ -21,19 +21,10 @@
Handling of VM disk images.
"""
-import os.path
-import shutil
-import sys
-import time
-import urllib2
-import urlparse
-
from nova import context
from nova import flags
from nova import log as logging
from nova import utils
-from nova.auth import manager
-from nova.auth import signer
FLAGS = flags.FLAGS
@@ -52,66 +43,6 @@ def fetch(image_id, path, _user, _project):
return metadata
-# NOTE(vish): The methods below should be unnecessary, but I'm leaving
-# them in case the glance client does not work on windows.
-def _fetch_image_no_curl(url, path, headers):
- request = urllib2.Request(url)
- for (k, v) in headers.iteritems():
- request.add_header(k, v)
-
- def urlretrieve(urlfile, fpath):
- chunk = 1 * 1024 * 1024
- f = open(fpath, "wb")
- while 1:
- data = urlfile.read(chunk)
- if not data:
- break
- f.write(data)
-
- urlopened = urllib2.urlopen(request)
- urlretrieve(urlopened, path)
- LOG.debug(_("Finished retreving %(url)s -- placed in %(path)s") % locals())
-
-
-def _fetch_s3_image(image, path, user, project):
- url = image_url(image)
-
- # This should probably move somewhere else, like e.g. a download_as
- # method on User objects and at the same time get rewritten to use
- # a web client.
- headers = {}
- headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
-
- (_, _, url_path, _, _, _) = urlparse.urlparse(url)
- access = manager.AuthManager().get_access_key(user, project)
- signature = signer.Signer(user.secret.encode()).s3_authorization(headers,
- 'GET',
- url_path)
- headers['Authorization'] = 'AWS %s:%s' % (access, signature)
-
- if sys.platform.startswith('win'):
- return _fetch_image_no_curl(url, path, headers)
- else:
- cmd = ['/usr/bin/curl', '--fail', '--silent', url]
- for (k, v) in headers.iteritems():
- cmd += ['-H', '\'%s: %s\'' % (k, v)]
-
- cmd += ['-o', path]
- return utils.execute(*cmd)
-
-
-def _fetch_local_image(image, path, user, project):
- source = _image_path(os.path.join(image, 'image'))
- if sys.platform.startswith('win'):
- return shutil.copy(source, path)
- else:
- return utils.execute('cp', source, path)
-
-
-def _image_path(path):
- return os.path.join(FLAGS.images_path, path)
-
-
# TODO(vish): xenapi should use the glance client code directly instead
# of retrieving the image using this method.
def image_url(image):
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index c8f342aa8..9f6cd608c 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -48,6 +48,8 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('default_os_type', 'linux', 'Default OS type')
flags.DEFINE_integer('block_device_creation_timeout', 10,
'time to wait for a block device to be created')
+flags.DEFINE_integer('max_kernel_ramdisk_size', 16 * 1024 * 1024,
+ 'maximum size in bytes of kernel or ramdisk images')
XENAPI_POWER_STATE = {
'Halted': power_state.SHUTDOWN,
@@ -444,6 +446,12 @@ class VMHelper(HelperBase):
if image_type == ImageType.DISK:
# Make room for MBR.
vdi_size += MBR_SIZE_BYTES
+ elif image_type == ImageType.KERNEL_RAMDISK and \
+ vdi_size > FLAGS.max_kernel_ramdisk_size:
+ max_size = FLAGS.max_kernel_ramdisk_size
+ raise exception.Error(
+ _("Kernel/Ramdisk image is too large: %(vdi_size)d bytes, "
+ "max %(max_size)d bytes") % locals())
name_label = get_name_label_for_image(image)
vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False)
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 13d7d215b..0074444f8 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -25,7 +25,6 @@ import M2Crypto
import os
import pickle
import subprocess
-import tempfile
import uuid
from nova import context
@@ -1163,18 +1162,17 @@ class SimpleDH(object):
return mpi
def _run_ssl(self, text, which):
- base_cmd = ('cat %(tmpfile)s | openssl enc -aes-128-cbc '
- '-a -pass pass:%(shared)s -nosalt %(dec_flag)s')
+ base_cmd = ('openssl enc -aes-128-cbc -a -pass pass:%(shared)s '
+ '-nosalt %(dec_flag)s')
if which.lower()[0] == 'd':
dec_flag = ' -d'
else:
dec_flag = ''
- fd, tmpfile = tempfile.mkstemp()
- os.close(fd)
- file(tmpfile, 'w').write(text)
shared = self._shared
cmd = base_cmd % locals()
proc = _runproc(cmd)
+ proc.stdin.write(text)
+ proc.stdin.close()
proc.wait()
err = proc.stderr.read()
if err:
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
index 72284ac02..7821a4f7e 100644
--- a/nova/virt/xenapi/volume_utils.py
+++ b/nova/virt/xenapi/volume_utils.py
@@ -204,14 +204,17 @@ def _get_volume_id(path_or_id):
if isinstance(path_or_id, int):
return path_or_id
# n must contain at least the volume_id
- # /vol- is for remote volumes
- # -vol- is for local volumes
+ # :volume- is for remote volumes
+ # -volume- is for local volumes
# see compute/manager->setup_compute_volume
- volume_id = path_or_id[path_or_id.find('/vol-') + 1:]
+ volume_id = path_or_id[path_or_id.find(':volume-') + 1:]
if volume_id == path_or_id:
- volume_id = path_or_id[path_or_id.find('-vol-') + 1:]
- volume_id = volume_id.replace('--', '-')
- return volume_id
+ volume_id = path_or_id[path_or_id.find('-volume--') + 1:]
+ volume_id = volume_id.replace('volume--', '')
+ else:
+ volume_id = volume_id.replace('volume-', '')
+ volume_id = volume_id[0:volume_id.find('-')]
+ return int(volume_id)
def _get_target_host(iscsi_string):
@@ -244,25 +247,23 @@ def _get_target(volume_id):
Gets iscsi name and portal from volume name and host.
For this method to work the following are needed:
1) volume_ref['host'] must resolve to something rather than loopback
- 2) ietd must bind only to the address as resolved above
- If any of the two conditions are not met, fall back on Flags.
"""
- volume_ref = db.volume_get_by_ec2_id(context.get_admin_context(),
- volume_id)
+ volume_ref = db.volume_get(context.get_admin_context(),
+ volume_id)
result = (None, None)
try:
- (r, _e) = utils.execute("sudo iscsiadm -m discovery -t "
- "sendtargets -p %s" %
- volume_ref['host'])
+ (r, _e) = utils.execute('sudo', 'iscsiadm',
+ '-m', 'discovery',
+ '-t', 'sendtargets',
+ '-p', volume_ref['host'])
except exception.ProcessExecutionError, exc:
LOG.exception(exc)
else:
- targets = r.splitlines()
- if len(_e) == 0 and len(targets) == 1:
- for target in targets:
- if volume_id in target:
- (location, _sep, iscsi_name) = target.partition(" ")
- break
- iscsi_portal = location.split(",")[0]
- result = (iscsi_name, iscsi_portal)
+ volume_name = "volume-%08x" % volume_id
+ for target in r.splitlines():
+ if FLAGS.iscsi_ip_prefix in target and volume_name in target:
+ (location, _sep, iscsi_name) = target.partition(" ")
+ break
+ iscsi_portal = location.split(",")[0]
+ result = (iscsi_name, iscsi_portal)
return result
diff --git a/nova/wsgi.py b/nova/wsgi.py
index e60a8820d..ea9bb963d 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -59,13 +59,16 @@ class Server(object):
def __init__(self, threads=1000):
self.pool = eventlet.GreenPool(threads)
+ self.socket_info = {}
- def start(self, application, port, host='0.0.0.0', backlog=128):
+ def start(self, application, port, host='0.0.0.0', key=None, backlog=128):
"""Run a WSGI server with the given application."""
arg0 = sys.argv[0]
logging.audit(_('Starting %(arg0)s on %(host)s:%(port)s') % locals())
socket = eventlet.listen((host, port), backlog=backlog)
self.pool.spawn_n(self._run, application, socket)
+ if key:
+ self.socket_info[key] = socket.getsockname()
def wait(self):
"""Wait until all servers have completed running."""