summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorChris Behrens <cbehrens@codestud.com>2011-06-29 08:29:35 -0700
committerChris Behrens <cbehrens@codestud.com>2011-06-29 08:29:35 -0700
commit2de973b5d57c8a4a79b94dcbcf6d6299f5c83e3c (patch)
tree9e912e40caa2849932470640e6af8ab32f6aa09b /nova
parent37e86a230720921488ae19fc2ca92667e8be4485 (diff)
parent4a3ef4367db23e8495e9c1dc4a49b5c66a6c763e (diff)
downloadnova-2de973b5d57c8a4a79b94dcbcf6d6299f5c83e3c.tar.gz
nova-2de973b5d57c8a4a79b94dcbcf6d6299f5c83e3c.tar.xz
nova-2de973b5d57c8a4a79b94dcbcf6d6299f5c83e3c.zip
Merged trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/__init__.py5
-rw-r--r--nova/api/openstack/wsgi.py4
-rw-r--r--nova/auth/fakeldap.py24
-rw-r--r--nova/auth/ldapdriver.py39
-rw-r--r--nova/compute/manager.py36
-rw-r--r--nova/db/api.py5
-rw-r--r--nova/db/sqlalchemy/api.py18
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py3
-rw-r--r--nova/exception.py8
-rw-r--r--nova/log.py11
-rw-r--r--nova/notifier/test_notifier.py28
-rw-r--r--nova/service.py175
-rw-r--r--nova/test.py23
-rw-r--r--nova/tests/integrated/integrated_helpers.py14
-rw-r--r--nova/tests/test_auth.py10
-rw-r--r--nova/tests/test_compute.py77
-rw-r--r--nova/tests/test_objectstore.py9
-rw-r--r--nova/tests/test_service.py30
-rw-r--r--nova/tests/test_wsgi.py95
-rw-r--r--nova/utils.py63
-rw-r--r--nova/wsgi.py193
21 files changed, 670 insertions, 200 deletions
diff --git a/nova/__init__.py b/nova/__init__.py
index 256db55a9..884c4a713 100644
--- a/nova/__init__.py
+++ b/nova/__init__.py
@@ -30,3 +30,8 @@
.. moduleauthor:: Manish Singh <yosh@gimp.org>
.. moduleauthor:: Andy Smith <andy@anarkystic.com>
"""
+
+import gettext
+
+
+gettext.install("nova", unicode=1)
diff --git a/nova/api/openstack/wsgi.py b/nova/api/openstack/wsgi.py
index 5d24b4cca..5b6e3cb1d 100644
--- a/nova/api/openstack/wsgi.py
+++ b/nova/api/openstack/wsgi.py
@@ -358,7 +358,7 @@ class Resource(wsgi.Application):
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
- LOG.debug("%(method)s %(url)s" % {"method": request.method,
+ LOG.info("%(method)s %(url)s" % {"method": request.method,
"url": request.url})
try:
@@ -386,7 +386,7 @@ class Resource(wsgi.Application):
msg_dict = dict(url=request.url, e=e)
msg = _("%(url)s returned a fault: %(e)s" % msg_dict)
- LOG.debug(msg)
+ LOG.info(msg)
return response
diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py
index 79afb9109..f1e769278 100644
--- a/nova/auth/fakeldap.py
+++ b/nova/auth/fakeldap.py
@@ -100,6 +100,11 @@ class OBJECT_CLASS_VIOLATION(Exception): # pylint: disable=C0103
pass
+class SERVER_DOWN(Exception): # pylint: disable=C0103
+ """Duplicate exception class from real LDAP module."""
+ pass
+
+
def initialize(_uri):
"""Opens a fake connection with an LDAP server."""
return FakeLDAP()
@@ -202,25 +207,38 @@ def _to_json(unencoded):
return json.dumps(list(unencoded))
+server_fail = False
+
+
class FakeLDAP(object):
"""Fake LDAP connection."""
def simple_bind_s(self, dn, password):
"""This method is ignored, but provided for compatibility."""
+ if server_fail:
+ raise SERVER_DOWN
pass
def unbind_s(self):
"""This method is ignored, but provided for compatibility."""
+ if server_fail:
+ raise SERVER_DOWN
pass
def add_s(self, dn, attr):
"""Add an object with the specified attributes at dn."""
+ if server_fail:
+ raise SERVER_DOWN
+
key = "%s%s" % (self.__prefix, dn)
value_dict = dict([(k, _to_json(v)) for k, v in attr])
Store.instance().hmset(key, value_dict)
def delete_s(self, dn):
"""Remove the ldap object at specified dn."""
+ if server_fail:
+ raise SERVER_DOWN
+
Store.instance().delete("%s%s" % (self.__prefix, dn))
def modify_s(self, dn, attrs):
@@ -232,6 +250,9 @@ class FakeLDAP(object):
([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
"""
+ if server_fail:
+ raise SERVER_DOWN
+
store = Store.instance()
key = "%s%s" % (self.__prefix, dn)
@@ -255,6 +276,9 @@ class FakeLDAP(object):
fields -- fields to return. Returns all fields if not specified
"""
+ if server_fail:
+ raise SERVER_DOWN
+
if scope != SCOPE_BASE and scope != SCOPE_SUBTREE:
raise NotImplementedError(str(scope))
store = Store.instance()
diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py
index e9532473d..bc37d2d87 100644
--- a/nova/auth/ldapdriver.py
+++ b/nova/auth/ldapdriver.py
@@ -101,6 +101,41 @@ def sanitize(fn):
return _wrapped
+class LDAPWrapper(object):
+ def __init__(self, ldap, url, user, password):
+ self.ldap = ldap
+ self.url = url
+ self.user = user
+ self.password = password
+ self.conn = None
+
+ def __wrap_reconnect(f):
+ def inner(self, *args, **kwargs):
+ if self.conn is None:
+ self.connect()
+ return f(self.conn)(*args, **kwargs)
+ else:
+ try:
+ return f(self.conn)(*args, **kwargs)
+ except self.ldap.SERVER_DOWN:
+ self.connect()
+ return f(self.conn)(*args, **kwargs)
+ return inner
+
+ def connect(self):
+ try:
+ self.conn = self.ldap.initialize(self.url)
+ self.conn.simple_bind_s(self.user, self.password)
+ except self.ldap.SERVER_DOWN:
+ self.conn = None
+ raise
+
+ search_s = __wrap_reconnect(lambda conn: conn.search_s)
+ add_s = __wrap_reconnect(lambda conn: conn.add_s)
+ delete_s = __wrap_reconnect(lambda conn: conn.delete_s)
+ modify_s = __wrap_reconnect(lambda conn: conn.modify_s)
+
+
class LdapDriver(object):
"""Ldap Auth driver
@@ -124,8 +159,8 @@ class LdapDriver(object):
LdapDriver.project_objectclass = 'novaProject'
self.__cache = None
if LdapDriver.conn is None:
- LdapDriver.conn = self.ldap.initialize(FLAGS.ldap_url)
- LdapDriver.conn.simple_bind_s(FLAGS.ldap_user_dn,
+ LdapDriver.conn = LDAPWrapper(self.ldap, FLAGS.ldap_url,
+ FLAGS.ldap_user_dn,
FLAGS.ldap_password)
if LdapDriver.mc is None:
LdapDriver.mc = memcache.Client(FLAGS.memcached_servers, debug=0)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 5aed2c677..98e02f5b2 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -49,10 +49,12 @@ from nova import flags
from nova import log as logging
from nova import manager
from nova import network
+from nova import notifier
from nova import rpc
from nova import utils
from nova import volume
from nova.compute import power_state
+from nova.notifier import api as notifier_api
from nova.compute.utils import terminate_volumes
from nova.virt import driver
@@ -343,6 +345,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_launched_at(context, instance_id)
self._update_state(context, instance_id)
+ usage_info = utils.usage_from_instance(instance_ref)
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.create',
+ notifier_api.INFO,
+ usage_info)
except exception.InstanceNotFound:
# FIXME(wwolf): We are just ignoring InstanceNotFound
# exceptions here in case the instance was immediately
@@ -421,9 +428,15 @@ class ComputeManager(manager.SchedulerDependentManager):
def terminate_instance(self, context, instance_id):
"""Terminate an instance on this host."""
self._shutdown_instance(context, instance_id, 'Terminating')
+ instance_ref = self.db.instance_get(context.elevated(), instance_id)
# TODO(ja): should we keep it in a terminated state for a bit?
self.db.instance_destroy(context, instance_id)
+ usage_info = utils.usage_from_instance(instance_ref)
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.delete',
+ notifier_api.INFO,
+ usage_info)
@exception.wrap_exception
@checks_instance_lock
@@ -460,6 +473,12 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_image_ref(context, instance_id, image_ref)
self._update_launched_at(context, instance_id)
self._update_state(context, instance_id)
+ usage_info = utils.usage_from_instance(instance_ref,
+ image_ref=image_ref)
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.rebuild',
+ notifier_api.INFO,
+ usage_info)
@exception.wrap_exception
@checks_instance_lock
@@ -637,6 +656,11 @@ class ComputeManager(manager.SchedulerDependentManager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
self.driver.destroy(instance_ref)
+ usage_info = utils.usage_from_instance(instance_ref)
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.resize.confirm',
+ notifier_api.INFO,
+ usage_info)
@exception.wrap_exception
@checks_instance_lock
@@ -684,6 +708,11 @@ class ComputeManager(manager.SchedulerDependentManager):
self.driver.revert_resize(instance_ref)
self.db.migration_update(context, migration_id,
{'status': 'reverted'})
+ usage_info = utils.usage_from_instance(instance_ref)
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.resize.revert',
+ notifier_api.INFO,
+ usage_info)
@exception.wrap_exception
@checks_instance_lock
@@ -720,6 +749,13 @@ class ComputeManager(manager.SchedulerDependentManager):
'migration_id': migration_ref['id'],
'instance_id': instance_id, },
})
+ usage_info = utils.usage_from_instance(instance_ref,
+ new_instance_type=instance_type['name'],
+ new_instance_type_id=instance_type['id'])
+ notifier_api.notify('compute.%s' % self.host,
+ 'compute.instance.resize.prep',
+ notifier_api.INFO,
+ usage_info)
@exception.wrap_exception
@checks_instance_lock
diff --git a/nova/db/api.py b/nova/db/api.py
index eea9a349e..72e967610 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -442,6 +442,11 @@ def instance_get_all(context):
return IMPL.instance_get_all(context)
+def instance_get_active_by_window(context, begin, end=None):
+ """Get instances active during a certain time window."""
+ return IMPL.instance_get_active_by_window(context, begin, end)
+
+
def instance_get_all_by_user(context, user_id):
"""Get all instances."""
return IMPL.instance_get_all_by_user(context, user_id)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 06bb1501f..6bd16d42e 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -956,6 +956,24 @@ def instance_get_all(context):
@require_admin_context
+def instance_get_active_by_window(context, begin, end=None):
+ """Return instances that were continuously active over the given window"""
+ session = get_session()
+ query = session.query(models.Instance).\
+ options(joinedload_all('fixed_ip.floating_ips')).\
+ options(joinedload('security_groups')).\
+ options(joinedload_all('fixed_ip.network')).\
+ options(joinedload('instance_type')).\
+ filter(models.Instance.launched_at < begin)
+ if end:
+ query = query.filter(or_(models.Instance.terminated_at == None,
+ models.Instance.terminated_at > end))
+ else:
+ query = query.filter(models.Instance.terminated_at == None)
+ return query.all()
+
+
+@require_admin_context
def instance_get_all_by_user(context, user_id):
session = get_session()
return session.query(models.Instance).\
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py
index 5aa30f7a8..cb3c73170 100644
--- a/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py
+++ b/nova/db/sqlalchemy/migrate_repo/versions/027_add_provider_firewall_rules.py
@@ -58,8 +58,7 @@ provider_fw_rules = Table('provider_fw_rules', meta,
Column('to_port', Integer()),
Column('cidr',
String(length=255, convert_unicode=False, assert_unicode=None,
- unicode_error=None, _warn_on_bytestring=False))
- )
+ unicode_error=None, _warn_on_bytestring=False)))
def upgrade(migrate_engine):
diff --git a/nova/exception.py b/nova/exception.py
index 79ae6561c..ef50b144f 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -598,3 +598,11 @@ class MigrationError(NovaException):
class MalformedRequestBody(NovaException):
message = _("Malformed message body: %(reason)s")
+
+
+class PasteConfigNotFound(NotFound):
+ message = _("Could not find paste config at %(path)s")
+
+
+class PasteAppNotFound(NotFound):
+ message = _("Could not load paste app '%(name)s' from %(path)s")
diff --git a/nova/log.py b/nova/log.py
index 6909916a1..f8c0ba68d 100644
--- a/nova/log.py
+++ b/nova/log.py
@@ -314,3 +314,14 @@ logging.setLoggerClass(NovaLogger)
def audit(msg, *args, **kwargs):
"""Shortcut for logging to root log with sevrity 'AUDIT'."""
logging.root.log(AUDIT, msg, *args, **kwargs)
+
+
+class WritableLogger(object):
+ """A thin wrapper that responds to `write` and logs."""
+
+ def __init__(self, logger, level=logging.INFO):
+ self.logger = logger
+ self.level = level
+
+ def write(self, msg):
+ self.logger.log(self.level, msg)
diff --git a/nova/notifier/test_notifier.py b/nova/notifier/test_notifier.py
new file mode 100644
index 000000000..d43f43e48
--- /dev/null
+++ b/nova/notifier/test_notifier.py
@@ -0,0 +1,28 @@
+# 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 json
+
+from nova import flags
+from nova import log as logging
+
+FLAGS = flags.FLAGS
+
+NOTIFICATIONS = []
+
+
+def notify(message):
+ """Test notifier, stores notifications in memory for unittests."""
+ NOTIFICATIONS.append(message)
diff --git a/nova/service.py b/nova/service.py
index 74f9f04d8..00e4f61e5 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -19,10 +19,12 @@
"""Generic Node baseclass for all workers that run on hosts."""
-import greenlet
import inspect
+import multiprocessing
import os
+import greenlet
+
from eventlet import greenthread
from nova import context
@@ -36,6 +38,8 @@ from nova import version
from nova import wsgi
+LOG = logging.getLogger('nova.service')
+
FLAGS = flags.FLAGS
flags.DEFINE_integer('report_interval', 10,
'seconds between nodes reporting state to datastore',
@@ -53,6 +57,63 @@ flags.DEFINE_string('api_paste_config', "api-paste.ini",
'File name for the paste.deploy config for nova-api')
+class Launcher(object):
+ """Launch one or more services and wait for them to complete."""
+
+ def __init__(self):
+ """Initialize the service launcher.
+
+ :returns: None
+
+ """
+ self._services = []
+
+ @staticmethod
+ def run_service(service):
+ """Start and wait for a service to finish.
+
+ :param service: Service to run and wait for.
+ :returns: None
+
+ """
+ service.start()
+ try:
+ service.wait()
+ except KeyboardInterrupt:
+ service.stop()
+
+ def launch_service(self, service):
+ """Load and start the given service.
+
+ :param service: The service you would like to start.
+ :returns: None
+
+ """
+ process = multiprocessing.Process(target=self.run_service,
+ args=(service,))
+ process.start()
+ self._services.append(process)
+
+ def stop(self):
+ """Stop all services which are currently running.
+
+ :returns: None
+
+ """
+ for service in self._services:
+ if service.is_alive():
+ service.terminate()
+
+ def wait(self):
+ """Waits until all services have been stopped, and then returns.
+
+ :returns: None
+
+ """
+ for service in self._services:
+ service.join()
+
+
class Service(object):
"""Base class for workers that run on hosts."""
@@ -232,45 +293,54 @@ class Service(object):
logging.exception(_('model server went away'))
-class WsgiService(object):
- """Base class for WSGI based services.
+class WSGIService(object):
+ """Provides ability to launch API from a 'paste' configuration."""
- For each api you define, you must also define these flags:
- :<api>_listen: The address on which to listen
- :<api>_listen_port: The port on which to listen
+ def __init__(self, name, loader=None):
+ """Initialize, but do not start the WSGI service.
- """
+ :param name: The name of the WSGI service given to the loader.
+ :param loader: Loads the WSGI application using the given name.
+ :returns: None
- def __init__(self, conf, apis):
- self.conf = conf
- self.apis = apis
- self.wsgi_app = None
+ """
+ self.name = name
+ self.loader = loader or wsgi.Loader()
+ self.app = self.loader.load_app(name)
+ self.host = getattr(FLAGS, '%s_listen' % name, "0.0.0.0")
+ self.port = getattr(FLAGS, '%s_listen_port' % name, 0)
+ self.server = wsgi.Server(name,
+ self.app,
+ host=self.host,
+ port=self.port)
def start(self):
- self.wsgi_app = _run_wsgi(self.conf, self.apis)
+ """Start serving this service using loaded configuration.
- def wait(self):
- self.wsgi_app.wait()
+ Also, retrieve updated port number in case '0' was passed in, which
+ indicates a random port should be used.
- 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]
+ :returns: None
+ """
+ self.server.start()
+ self.port = self.server.port
-class ApiService(WsgiService):
- """Class for our nova-api service."""
+ def stop(self):
+ """Stop serving this API.
- @classmethod
- def create(cls, conf=None):
- if not conf:
- conf = wsgi.paste_config_file(FLAGS.api_paste_config)
- if not conf:
- message = (_('No paste configuration found for: %s'),
- FLAGS.api_paste_config)
- raise exception.Error(message)
- api_endpoints = ['ec2', 'osapi']
- service = cls(conf, api_endpoints)
- return service
+ :returns: None
+
+ """
+ self.server.stop()
+
+ def wait(self):
+ """Wait for the service to stop serving this API.
+
+ :returns: None
+
+ """
+ self.server.wait()
def serve(*services):
@@ -302,48 +372,3 @@ def serve(*services):
def wait():
while True:
greenthread.sleep(5)
-
-
-def serve_wsgi(cls, conf=None):
- try:
- service = cls.create(conf)
- except Exception:
- logging.exception('in WsgiService.create()')
- raise
- finally:
- # After we've loaded up all our dynamic bits, check
- # whether we should print help
- flags.DEFINE_flag(flags.HelpFlag())
- flags.DEFINE_flag(flags.HelpshortFlag())
- flags.DEFINE_flag(flags.HelpXMLFlag())
- FLAGS.ParseNewFlags()
-
- service.start()
-
- return service
-
-
-def _run_wsgi(paste_config_file, apis):
- logging.debug(_('Using paste.deploy config at: %s'), paste_config_file)
- apps = []
- for api in apis:
- config = wsgi.load_paste_configuration(paste_config_file, api)
- if config is None:
- logging.debug(_('No paste configuration for app: %s'), api)
- continue
- 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),
- api))
- if len(apps) == 0:
- logging.error(_('No known API applications configured in %s.'),
- paste_config_file)
- return
-
- server = wsgi.Server()
- for app in apps:
- server.start(*app)
- return server
diff --git a/nova/test.py b/nova/test.py
index 4a0a18fe7..ab1eaf5fd 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -38,7 +38,6 @@ from nova import flags
from nova import rpc
from nova import utils
from nova import service
-from nova import wsgi
from nova.virt import fake
@@ -81,7 +80,6 @@ class TestCase(unittest.TestCase):
self.injected = []
self._services = []
self._monkey_patch_attach()
- self._monkey_patch_wsgi()
self._original_flags = FLAGS.FlagValuesDict()
rpc.ConnectionPool = rpc.Pool(max_size=FLAGS.rpc_conn_pool_size)
@@ -107,7 +105,6 @@ class TestCase(unittest.TestCase):
# Reset our monkey-patches
rpc.Consumer.attach_to_eventlet = self.original_attach
- wsgi.Server.start = self.original_start
# Stop any timers
for x in self.injected:
@@ -163,26 +160,6 @@ class TestCase(unittest.TestCase):
_wrapped.func_name = self.original_attach.func_name
rpc.Consumer.attach_to_eventlet = _wrapped
- def _monkey_patch_wsgi(self):
- """Allow us to kill servers spawned by wsgi.Server."""
- self.original_start = wsgi.Server.start
-
- @functools.wraps(self.original_start)
- def _wrapped_start(inner_self, *args, **kwargs):
- original_spawn_n = inner_self.pool.spawn_n
-
- @functools.wraps(original_spawn_n)
- def _wrapped_spawn_n(*args, **kwargs):
- rv = greenthread.spawn(*args, **kwargs)
- self._services.append(rv)
-
- inner_self.pool.spawn_n = _wrapped_spawn_n
- self.original_start(inner_self, *args, **kwargs)
- inner_self.pool.spawn_n = original_spawn_n
-
- _wrapped_start.func_name = self.original_start.func_name
- wsgi.Server.start = _wrapped_start
-
# Useful assertions
def assertDictMatch(self, d1, d2, approx_equal=False, tolerance=0.001):
"""Assert two dicts are equivalent.
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index 522c7cb0e..47bd8c1e4 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -171,16 +171,10 @@ class _IntegratedTestBase(test.TestCase):
self.api = self.user.openstack_api
def _start_api_service(self):
- api_service = service.ApiService.create()
- api_service.start()
-
- if not api_service:
- raise Exception("API Service was None")
-
- self.api_service = api_service
-
- host, port = api_service.get_socket_info('osapi')
- self.auth_url = 'http://%s:%s/v1.1' % (host, port)
+ osapi = service.WSGIService("osapi")
+ osapi.start()
+ self.auth_url = 'http://%s:%s/v1.1' % (osapi.host, osapi.port)
+ LOG.warn(self.auth_url)
def tearDown(self):
self.context.cleanup()
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index 7d00bddfe..71e0d17c9 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -25,6 +25,7 @@ from nova import log as logging
from nova import test
from nova.auth import manager
from nova.api.ec2 import cloud
+from nova.auth import fakeldap
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.auth_unittest')
@@ -369,6 +370,15 @@ class _AuthManagerBaseTestCase(test.TestCase):
class AuthManagerLdapTestCase(_AuthManagerBaseTestCase):
auth_driver = 'nova.auth.ldapdriver.FakeLdapDriver'
+ def test_reconnect_on_server_failure(self):
+ self.manager.get_users()
+ fakeldap.server_fail = True
+ try:
+ self.assertRaises(fakeldap.SERVER_DOWN, self.manager.get_users)
+ finally:
+ fakeldap.server_fail = False
+ self.manager.get_users()
+
class AuthManagerDbTestCase(_AuthManagerBaseTestCase):
auth_driver = 'nova.auth.dbdriver.DbDriver'
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 439508b27..4d42b1fdf 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -37,6 +37,7 @@ from nova import log as logging
from nova import rpc
from nova import test
from nova import utils
+from nova.notifier import test_notifier
LOG = logging.getLogger('nova.tests.compute')
FLAGS = flags.FLAGS
@@ -62,6 +63,7 @@ class ComputeTestCase(test.TestCase):
super(ComputeTestCase, self).setUp()
self.flags(connection_type='fake',
stub_network=True,
+ notification_driver='nova.notifier.test_notifier',
network_manager='nova.network.manager.FlatManager')
self.compute = utils.import_object(FLAGS.compute_manager)
self.compute_api = compute.API()
@@ -69,6 +71,7 @@ class ComputeTestCase(test.TestCase):
self.user = self.manager.create_user('fake', 'fake', 'fake')
self.project = self.manager.create_project('fake', 'fake', 'fake')
self.context = context.RequestContext('fake', 'fake', False)
+ test_notifier.NOTIFICATIONS = []
def fake_show(meh, context, id):
return {'id': 1, 'properties': {'kernel_id': 1, 'ramdisk_id': 1}}
@@ -327,6 +330,50 @@ class ComputeTestCase(test.TestCase):
self.assert_(console)
self.compute.terminate_instance(self.context, instance_id)
+ def test_run_instance_usage_notification(self):
+ """Ensure run instance generates apropriate usage notification"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+ msg = test_notifier.NOTIFICATIONS[0]
+ self.assertEquals(msg['priority'], 'INFO')
+ self.assertEquals(msg['event_type'], 'compute.instance.create')
+ payload = msg['payload']
+ self.assertEquals(payload['tenant_id'], self.project.id)
+ self.assertEquals(payload['user_id'], self.user.id)
+ self.assertEquals(payload['instance_id'], instance_id)
+ self.assertEquals(payload['instance_type'], 'm1.tiny')
+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
+ self.assertTrue('display_name' in payload)
+ self.assertTrue('created_at' in payload)
+ self.assertTrue('launched_at' in payload)
+ self.assertEquals(payload['image_ref'], '1')
+ self.compute.terminate_instance(self.context, instance_id)
+
+ def test_terminate_usage_notification(self):
+ """Ensure terminate_instance generates apropriate usage notification"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ test_notifier.NOTIFICATIONS = []
+ self.compute.terminate_instance(self.context, instance_id)
+
+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+ msg = test_notifier.NOTIFICATIONS[0]
+ self.assertEquals(msg['priority'], 'INFO')
+ self.assertEquals(msg['event_type'], 'compute.instance.delete')
+ payload = msg['payload']
+ self.assertEquals(payload['tenant_id'], self.project.id)
+ self.assertEquals(payload['user_id'], self.user.id)
+ self.assertEquals(payload['instance_id'], instance_id)
+ self.assertEquals(payload['instance_type'], 'm1.tiny')
+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
+ self.assertTrue('display_name' in payload)
+ self.assertTrue('created_at' in payload)
+ self.assertTrue('launched_at' in payload)
+ self.assertEquals(payload['image_ref'], '1')
+
def test_run_instance_existing(self):
"""Ensure failure when running an instance that already exists"""
instance_id = self._create_instance()
@@ -378,6 +425,36 @@ class ComputeTestCase(test.TestCase):
self.compute.terminate_instance(self.context, instance_id)
+ def test_resize_instance_notification(self):
+ """Ensure notifications on instance migrate/resize"""
+ instance_id = self._create_instance()
+ context = self.context.elevated()
+
+ self.compute.run_instance(self.context, instance_id)
+ test_notifier.NOTIFICATIONS = []
+
+ db.instance_update(self.context, instance_id, {'host': 'foo'})
+ self.compute.prep_resize(context, instance_id, 1)
+ migration_ref = db.migration_get_by_instance_and_status(context,
+ instance_id, 'pre-migrating')
+
+ self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+ msg = test_notifier.NOTIFICATIONS[0]
+ self.assertEquals(msg['priority'], 'INFO')
+ self.assertEquals(msg['event_type'], 'compute.instance.resize.prep')
+ payload = msg['payload']
+ self.assertEquals(payload['tenant_id'], self.project.id)
+ self.assertEquals(payload['user_id'], self.user.id)
+ self.assertEquals(payload['instance_id'], instance_id)
+ self.assertEquals(payload['instance_type'], 'm1.tiny')
+ type_id = instance_types.get_instance_type_by_name('m1.tiny')['id']
+ self.assertEquals(str(payload['instance_type_id']), str(type_id))
+ self.assertTrue('display_name' in payload)
+ self.assertTrue('created_at' in payload)
+ self.assertTrue('launched_at' in payload)
+ self.assertEquals(payload['image_ref'], '1')
+ self.compute.terminate_instance(context, instance_id)
+
def test_resize_instance(self):
"""Ensure instance can be migrated/resized"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_objectstore.py b/nova/tests/test_objectstore.py
index c78772f27..39b4e18d7 100644
--- a/nova/tests/test_objectstore.py
+++ b/nova/tests/test_objectstore.py
@@ -70,11 +70,15 @@ class S3APITestCase(test.TestCase):
os.mkdir(FLAGS.buckets_path)
router = s3server.S3Application(FLAGS.buckets_path)
- server = wsgi.Server()
- server.start(router, FLAGS.s3_port, host=FLAGS.s3_host)
+ self.server = wsgi.Server("S3 Objectstore",
+ router,
+ host=FLAGS.s3_host,
+ port=FLAGS.s3_port)
+ self.server.start()
if not boto.config.has_section('Boto'):
boto.config.add_section('Boto')
+
boto.config.set('Boto', 'num_retries', '0')
conn = s3.S3Connection(aws_access_key_id=self.admin_user.access,
aws_secret_access_key=self.admin_user.secret,
@@ -145,4 +149,5 @@ class S3APITestCase(test.TestCase):
"""Tear down auth and test server."""
self.auth_manager.delete_user('admin')
self.auth_manager.delete_project('admin')
+ self.server.stop()
super(S3APITestCase, self).tearDown()
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index d1cc8bd61..f45f76b73 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -30,6 +30,7 @@ from nova import rpc
from nova import test
from nova import service
from nova import manager
+from nova import wsgi
from nova.compute import manager as compute_manager
FLAGS = flags.FLAGS
@@ -349,3 +350,32 @@ class ServiceTestCase(test.TestCase):
serv.stop()
db.service_destroy(ctxt, service_ref['id'])
+
+
+class TestWSGIService(test.TestCase):
+
+ def setUp(self):
+ super(TestWSGIService, self).setUp()
+ self.stubs.Set(wsgi.Loader, "load_app", mox.MockAnything())
+
+ def test_service_random_port(self):
+ test_service = service.WSGIService("test_service")
+ self.assertEquals(0, test_service.port)
+ test_service.start()
+ self.assertNotEqual(0, test_service.port)
+ test_service.stop()
+
+
+class TestLauncher(test.TestCase):
+
+ def setUp(self):
+ super(TestLauncher, self).setUp()
+ self.stubs.Set(wsgi.Loader, "load_app", mox.MockAnything())
+ self.service = service.WSGIService("test_service")
+
+ def test_launch_app(self):
+ self.assertEquals(0, self.service.port)
+ launcher = service.Launcher()
+ launcher.launch_service(self.service)
+ self.assertEquals(0, self.service.port)
+ launcher.stop()
diff --git a/nova/tests/test_wsgi.py b/nova/tests/test_wsgi.py
new file mode 100644
index 000000000..b71e8d418
--- /dev/null
+++ b/nova/tests/test_wsgi.py
@@ -0,0 +1,95 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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.
+
+"""Unit tests for `nova.wsgi`."""
+
+import os.path
+import tempfile
+
+import unittest
+
+import nova.exception
+import nova.test
+import nova.wsgi
+
+
+class TestLoaderNothingExists(unittest.TestCase):
+ """Loader tests where os.path.exists always returns False."""
+
+ def setUp(self):
+ self._os_path_exists = os.path.exists
+ os.path.exists = lambda _: False
+
+ def test_config_not_found(self):
+ self.assertRaises(
+ nova.exception.PasteConfigNotFound,
+ nova.wsgi.Loader,
+ )
+
+ def tearDown(self):
+ os.path.exists = self._os_path_exists
+
+
+class TestLoaderNormalFilesystem(unittest.TestCase):
+ """Loader tests with normal filesystem (unmodified os.path module)."""
+
+ _paste_config = """
+[app:test_app]
+use = egg:Paste#static
+document_root = /tmp
+ """
+
+ def setUp(self):
+ self.config = tempfile.NamedTemporaryFile(mode="w+t")
+ self.config.write(self._paste_config.lstrip())
+ self.config.seek(0)
+ self.config.flush()
+ self.loader = nova.wsgi.Loader(self.config.name)
+
+ def test_config_found(self):
+ self.assertEquals(self.config.name, self.loader.config_path)
+
+ def test_app_not_found(self):
+ self.assertRaises(
+ nova.exception.PasteAppNotFound,
+ self.loader.load_app,
+ "non-existant app",
+ )
+
+ def test_app_found(self):
+ url_parser = self.loader.load_app("test_app")
+ self.assertEquals("/tmp", url_parser.directory)
+
+ def tearDown(self):
+ self.config.close()
+
+
+class TestWSGIServer(unittest.TestCase):
+ """WSGI server tests."""
+
+ def test_no_app(self):
+ server = nova.wsgi.Server("test_app", None)
+ self.assertEquals("test_app", server.name)
+
+ def test_start_random_port(self):
+ server = nova.wsgi.Server("test_random_port", None, host="127.0.0.1")
+ self.assertEqual(0, server.port)
+ server.start()
+ self.assertNotEqual(0, server.port)
+ server.stop()
+ server.wait()
diff --git a/nova/utils.py b/nova/utils.py
index 6d8324e5b..510cdd9f6 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -46,6 +46,7 @@ from eventlet.green import subprocess
from nova import exception
from nova import flags
from nova import log as logging
+from nova import version
LOG = logging.getLogger("nova.utils")
@@ -226,8 +227,10 @@ def novadir():
return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
-def default_flagfile(filename='nova.conf'):
- for arg in sys.argv:
+def default_flagfile(filename='nova.conf', args=None):
+ if args is None:
+ args = sys.argv
+ for arg in args:
if arg.find('flagfile') != -1:
break
else:
@@ -239,8 +242,8 @@ def default_flagfile(filename='nova.conf'):
filename = "./nova.conf"
if not os.path.exists(filename):
filename = '/etc/nova/nova.conf'
- flagfile = ['--flagfile=%s' % filename]
- sys.argv = sys.argv[:1] + flagfile + sys.argv[1:]
+ flagfile = '--flagfile=%s' % filename
+ args.insert(1, flagfile)
def debug(arg):
@@ -279,6 +282,22 @@ EASIER_PASSWORD_SYMBOLS = ('23456789' # Removed: 0, 1
'ABCDEFGHJKLMNPQRSTUVWXYZ') # Removed: I, O
+def usage_from_instance(instance_ref, **kw):
+ usage_info = dict(
+ tenant_id=instance_ref['project_id'],
+ user_id=instance_ref['user_id'],
+ instance_id=instance_ref['id'],
+ instance_type=instance_ref['instance_type']['name'],
+ instance_type_id=instance_ref['instance_type_id'],
+ display_name=instance_ref['display_name'],
+ created_at=str(instance_ref['created_at']),
+ launched_at=str(instance_ref['launched_at']) \
+ if instance_ref['launched_at'] else '',
+ image_ref=instance_ref['image_ref'])
+ usage_info.update(kw)
+ return usage_info
+
+
def generate_password(length=20, symbols=DEFAULT_PASSWORD_SYMBOLS):
"""Generate a random password from the supplied symbols.
@@ -751,3 +770,39 @@ def is_uuid_like(val):
if not isinstance(val, basestring):
return False
return (len(val) == 36) and (val.count('-') == 4)
+
+
+class Bootstrapper(object):
+ """Provides environment bootstrapping capabilities for entry points."""
+
+ @staticmethod
+ def bootstrap_binary(argv):
+ """Initialize the Nova environment using command line arguments."""
+ Bootstrapper.setup_flags(argv)
+ Bootstrapper.setup_logging()
+ Bootstrapper.log_flags()
+
+ @staticmethod
+ def setup_logging():
+ """Initialize logging and log a message indicating the Nova version."""
+ logging.setup()
+ logging.audit(_("Nova Version (%s)") %
+ version.version_string_with_vcs())
+
+ @staticmethod
+ def setup_flags(input_flags):
+ """Initialize flags, load flag file, and print help if needed."""
+ default_flagfile(args=input_flags)
+ FLAGS(input_flags or [])
+ flags.DEFINE_flag(flags.HelpFlag())
+ flags.DEFINE_flag(flags.HelpshortFlag())
+ flags.DEFINE_flag(flags.HelpXMLFlag())
+ FLAGS.ParseNewFlags()
+
+ @staticmethod
+ def log_flags():
+ """Log the list of all active flags being used."""
+ logging.audit(_("Currently active flags:"))
+ for key in FLAGS:
+ value = FLAGS.get(key, None)
+ logging.audit(_("%(key)s : %(value)s" % locals()))
diff --git a/nova/wsgi.py b/nova/wsgi.py
index 33ba852bc..23d29079f 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -21,16 +21,16 @@
import os
import sys
+
from xml.dom import minidom
import eventlet
import eventlet.wsgi
-eventlet.patcher.monkey_patch(all=False, socket=True, time=True)
-import routes
+import greenlet
import routes.middleware
-import webob
import webob.dec
import webob.exc
+
from paste import deploy
from nova import exception
@@ -39,49 +39,86 @@ from nova import log as logging
from nova import utils
+eventlet.patcher.monkey_patch(socket=True, time=True)
+
+
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.wsgi')
-class WritableLogger(object):
- """A thin wrapper that responds to `write` and logs."""
+class Server(object):
+ """Server class to manage a WSGI server, serving a WSGI application."""
- def __init__(self, logger, level=logging.DEBUG):
- self.logger = logger
- self.level = level
+ default_pool_size = 1000
- def write(self, msg):
- self.logger.log(self.level, msg)
+ def __init__(self, name, app, host=None, port=None, pool_size=None):
+ """Initialize, but do not start, a WSGI server.
+ :param name: Pretty name for logging.
+ :param app: The WSGI application to serve.
+ :param host: IP address to serve the application.
+ :param port: Port number to server the application.
+ :param pool_size: Maximum number of eventlets to spawn concurrently.
+ :returns: None
-class Server(object):
- """Server class to manage multiple WSGI sockets and applications."""
+ """
+ self.name = name
+ self.app = app
+ self.host = host or "0.0.0.0"
+ self.port = port or 0
+ self._server = None
+ self._socket = None
+ self._pool = eventlet.GreenPool(pool_size or self.default_pool_size)
+ self._logger = logging.getLogger("eventlet.wsgi.server")
+ self._wsgi_logger = logging.WritableLogger(self._logger)
+
+ def _start(self):
+ """Run the blocking eventlet WSGI server.
+
+ :returns: None
+
+ """
+ eventlet.wsgi.server(self._socket,
+ self.app,
+ custom_pool=self._pool,
+ log=self._wsgi_logger)
+
+ def start(self, backlog=128):
+ """Start serving a WSGI application.
+
+ :param backlog: Maximum number of queued connections.
+ :returns: None
+
+ """
+ self._socket = eventlet.listen((self.host, self.port), backlog=backlog)
+ self._server = eventlet.spawn(self._start)
+ (self.host, self.port) = self._socket.getsockname()
+ LOG.info(_("Started %(name)s on %(host)s:%(port)s") % self.__dict__)
+
+ def stop(self):
+ """Stop this server.
+
+ This is not a very nice action, as currently the method by which a
+ server is stopped is by killing it's eventlet.
- def __init__(self, threads=1000):
- self.pool = eventlet.GreenPool(threads)
- self.socket_info = {}
+ :returns: None
- 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()
+ """
+ LOG.info(_("Stopping WSGI server."))
+ self._server.kill()
def wait(self):
- """Wait until all servers have completed running."""
- try:
- self.pool.waitall()
- except KeyboardInterrupt:
- pass
+ """Block, until the server has stopped.
+
+ Waits on the server's eventlet to finish, then returns.
- def _run(self, application, socket):
- """Start a WSGI server in a new green thread."""
- logger = logging.getLogger('eventlet.wsgi.server')
- eventlet.wsgi.server(socket, application, custom_pool=self.pool,
- log=WritableLogger(logger))
+ :returns: None
+
+ """
+ try:
+ self._server.wait()
+ except greenlet.GreenletExit:
+ LOG.info(_("WSGI server has stopped."))
class Request(webob.Request):
@@ -309,55 +346,51 @@ class Router(object):
return app
-def paste_config_file(basename):
- """Find the best location in the system for a paste config file.
+class Loader(object):
+ """Used to load WSGI applications from paste configurations."""
+
+ def __init__(self, config_path=None):
+ """Initialize the loader, and attempt to find the config.
- Search Order
- ------------
+ :param config_path: Full or relative path to the paste config.
+ :returns: None
- The search for a paste config file honors `FLAGS.state_path`, which in a
- version checked out from bzr will be the `nova` directory in the top level
- of the checkout, and in an installation for a package for your distribution
- will likely point to someplace like /etc/nova.
+ """
+ config_path = config_path or FLAGS.api_paste_config
+ self.config_path = self._find_config(config_path)
- This method tries to load places likely to be used in development or
- experimentation before falling back to the system-wide configuration
- in `/etc/nova/`.
+ def _find_config(self, config_path):
+ """Find the paste configuration file using the given hint.
- * Current working directory
- * the `etc` directory under state_path, because when working on a checkout
- from bzr this will point to the default
- * top level of FLAGS.state_path, for distributions
- * /etc/nova, which may not be diffrerent from state_path on your distro
+ :param config_path: Full or relative path to the paste config.
+ :returns: Full path of the paste config, if it exists.
+ :raises: `nova.exception.PasteConfigNotFound`
- """
- configfiles = [basename,
- os.path.join(FLAGS.state_path, 'etc', 'nova', basename),
- os.path.join(FLAGS.state_path, 'etc', basename),
- os.path.join(FLAGS.state_path, basename),
- '/etc/nova/%s' % basename]
- for configfile in configfiles:
- if os.path.exists(configfile):
- return configfile
-
-
-def load_paste_configuration(filename, appname):
- """Returns a paste configuration dict, or None."""
- filename = os.path.abspath(filename)
- config = None
- try:
- config = deploy.appconfig('config:%s' % filename, name=appname)
- except LookupError:
- pass
- return config
-
-
-def load_paste_app(filename, appname):
- """Builds a wsgi app from a paste config, None if app not configured."""
- filename = os.path.abspath(filename)
- app = None
- try:
- app = deploy.loadapp('config:%s' % filename, name=appname)
- except LookupError:
- pass
- return app
+ """
+ possible_locations = [
+ config_path,
+ os.path.join(FLAGS.state_path, "etc", "nova", config_path),
+ os.path.join(FLAGS.state_path, "etc", config_path),
+ os.path.join(FLAGS.state_path, config_path),
+ "/etc/nova/%s" % config_path,
+ ]
+
+ for path in possible_locations:
+ if os.path.exists(path):
+ return os.path.abspath(path)
+
+ raise exception.PasteConfigNotFound(path=os.path.abspath(config_path))
+
+ def load_app(self, name):
+ """Return the paste URLMap wrapped WSGI application.
+
+ :param name: Name of the application to load.
+ :returns: Paste URLMap object wrapping the requested application.
+ :raises: `nova.exception.PasteAppNotFound`
+
+ """
+ try:
+ return deploy.loadapp("config:%s" % self.config_path, name=name)
+ except LookupError as err:
+ LOG.error(err)
+ raise exception.PasteAppNotFound(name=name, path=self.config_path)