summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIlya Alekseyev <ialekseev@griddynamics.com>2010-12-23 22:58:20 +0300
committerIlya Alekseyev <ialekseev@griddynamics.com>2010-12-23 22:58:20 +0300
commit57ead438d06dd5c6e98e971670f397bed5d7e29c (patch)
tree6a94815ea5d66930e4638769e463b7ec60c9cdc2
parentd88817a360676173ac31566e13201d56f1e2b0b0 (diff)
parent75e2cbec9eb5132a49446f1b6d563d5f43d007de (diff)
downloadnova-57ead438d06dd5c6e98e971670f397bed5d7e29c.tar.gz
nova-57ead438d06dd5c6e98e971670f397bed5d7e29c.tar.xz
nova-57ead438d06dd5c6e98e971670f397bed5d7e29c.zip
Merge with trunk
-rw-r--r--.mailmap7
-rw-r--r--Authors2
-rwxr-xr-xbin/nova-combined3
-rwxr-xr-xbin/nova-dhcpbridge1
-rw-r--r--nova/api/__init__.py1
-rw-r--r--nova/api/ec2/__init__.py75
-rw-r--r--nova/api/ec2/cloud.py41
-rw-r--r--nova/api/ec2/metadatarequesthandler.py11
-rw-r--r--nova/api/openstack/__init__.py124
-rw-r--r--nova/api/openstack/auth.py59
-rw-r--r--nova/api/openstack/common.py36
-rw-r--r--nova/api/openstack/flavors.py3
-rw-r--r--nova/api/openstack/images.py7
-rw-r--r--nova/api/openstack/ratelimiting/__init__.py96
-rw-r--r--nova/api/openstack/servers.py3
-rw-r--r--nova/api/openstack/sharedipgroups.py20
-rw-r--r--nova/auth/fakeldap.py3
-rw-r--r--nova/auth/ldapdriver.py161
-rw-r--r--nova/auth/nova_openldap.schema46
-rw-r--r--nova/auth/nova_sun.schema13
-rwxr-xr-xnova/auth/opendj.sh1
-rwxr-xr-xnova/auth/slap.sh3
-rw-r--r--nova/compute/api.py62
-rw-r--r--nova/compute/manager.py75
-rw-r--r--nova/crypto.py3
-rw-r--r--nova/db/sqlalchemy/api.py8
-rw-r--r--nova/fakememcache.py59
-rw-r--r--nova/fakerabbit.py136
-rw-r--r--nova/network/linux_net.py102
-rw-r--r--nova/network/manager.py116
-rw-r--r--nova/rpc.py2
-rw-r--r--nova/test.py3
-rw-r--r--nova/tests/api/__init__.py81
-rw-r--r--nova/tests/api/openstack/__init__.py13
-rw-r--r--nova/tests/api/openstack/fakes.py22
-rw-r--r--nova/tests/api/openstack/test_auth.py4
-rw-r--r--nova/tests/api/test.py81
-rw-r--r--nova/tests/api_integration.py54
-rw-r--r--nova/tests/db/__init__.py20
-rw-r--r--nova/tests/db/fakes.py75
-rw-r--r--nova/tests/test_access.py (renamed from nova/tests/access_unittest.py)0
-rw-r--r--nova/tests/test_api.py (renamed from nova/tests/api_unittest.py)0
-rw-r--r--nova/tests/test_auth.py (renamed from nova/tests/auth_unittest.py)0
-rw-r--r--nova/tests/test_cloud.py (renamed from nova/tests/cloud_unittest.py)30
-rw-r--r--nova/tests/test_compute.py (renamed from nova/tests/compute_unittest.py)1
-rw-r--r--nova/tests/test_flags.py (renamed from nova/tests/flags_unittest.py)0
-rw-r--r--nova/tests/test_middleware.py86
-rw-r--r--nova/tests/test_misc.py (renamed from nova/tests/misc_unittest.py)8
-rw-r--r--nova/tests/test_network.py (renamed from nova/tests/network_unittest.py)8
-rw-r--r--nova/tests/test_quota.py (renamed from nova/tests/quota_unittest.py)0
-rw-r--r--nova/tests/test_rpc.py (renamed from nova/tests/rpc_unittest.py)29
-rw-r--r--nova/tests/test_scheduler.py (renamed from nova/tests/scheduler_unittest.py)3
-rw-r--r--nova/tests/test_service.py (renamed from nova/tests/service_unittest.py)14
-rw-r--r--nova/tests/test_twistd.py (renamed from nova/tests/twistd_unittest.py)0
-rw-r--r--nova/tests/test_virt.py (renamed from nova/tests/virt_unittest.py)37
-rw-r--r--nova/tests/test_volume.py (renamed from nova/tests/volume_unittest.py)0
-rw-r--r--nova/tests/test_xenapi.py219
-rw-r--r--nova/tests/xenapi/__init__.py20
-rw-r--r--nova/tests/xenapi/stubs.py94
-rw-r--r--nova/utils.py41
-rw-r--r--nova/virt/fake.py6
-rw-r--r--nova/virt/libvirt.xml.template3
-rw-r--r--nova/virt/libvirt_conn.py83
-rw-r--r--nova/virt/xenapi/__init__.py15
-rw-r--r--nova/virt/xenapi/fake.py388
-rw-r--r--nova/virt/xenapi/network_utils.py15
-rw-r--r--nova/virt/xenapi/vm_utils.py104
-rw-r--r--nova/virt/xenapi/vmops.py64
-rw-r--r--nova/virt/xenapi/volume_utils.py268
-rw-r--r--nova/virt/xenapi/volumeops.py101
-rw-r--r--nova/virt/xenapi_conn.py97
-rw-r--r--run_tests.py125
-rwxr-xr-xrun_tests.sh12
-rw-r--r--setup.py1
74 files changed, 2561 insertions, 943 deletions
diff --git a/.mailmap b/.mailmap
index 8041e2341..9ab7db743 100644
--- a/.mailmap
+++ b/.mailmap
@@ -24,7 +24,6 @@
<todd@ansolabs.com> <todd@rubidine.com>
<vishvananda@gmail.com> <vishvananda@yahoo.com>
<vishvananda@gmail.com> <root@mirror.nasanebula.net>
-# These are from people who failed to set a proper committer
-. <root@tonbuntu>
-. <laner@controller>
-. <root@ubuntu>
+<vishvananda@gmail.com> <root@ubuntu>
+<sleepsonthefloor@gmail.com> <root@tonbuntu>
+<rlane@wikimedia.org> <laner@controller>
diff --git a/Authors b/Authors
index 0b048becb..a8acfc719 100644
--- a/Authors
+++ b/Authors
@@ -11,6 +11,7 @@ Eldar Nugaev <enugaev@griddynamics.com>
Eric Day <eday@oddments.org>
Ewan Mellor <ewan.mellor@citrix.com>
Hisaki Ohara <hisaki.ohara@intel.com>
+Ilya Alekseyev <ialekseev@griddynamics.com>
Jay Pipes <jaypipes@gmail.com>
Jesse Andrews <anotherjesse@gmail.com>
Joe Heck <heckj@mac.com>
@@ -24,6 +25,7 @@ Michael Gundlach <michael.gundlach@rackspace.com>
Monty Taylor <mordred@inaugust.com>
Paul Voccio <paul@openstack.org>
Rick Clark <rick@openstack.org>
+Ryan Lane <rlane@wikimedia.org>
Ryan Lucio <rlucio@internap.com>
Sandy Walsh <sandy.walsh@rackspace.com>
Soren Hansen <soren.hansen@rackspace.com>
diff --git a/bin/nova-combined b/bin/nova-combined
index c6a04f7e9..53322f1a0 100755
--- a/bin/nova-combined
+++ b/bin/nova-combined
@@ -22,6 +22,7 @@
import eventlet
eventlet.monkey_patch()
+import gettext
import os
import sys
@@ -33,6 +34,8 @@ possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
+gettext.install('nova', unicode=1)
+
from nova import api
from nova import flags
from nova import service
diff --git a/bin/nova-dhcpbridge b/bin/nova-dhcpbridge
index 81b9b6dd3..828aba3d1 100755
--- a/bin/nova-dhcpbridge
+++ b/bin/nova-dhcpbridge
@@ -110,7 +110,6 @@ def main():
FLAGS.num_networks = 5
path = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..',
- '_trial_temp',
'nova.sqlite'))
FLAGS.sql_connection = 'sqlite:///%s' % path
action = argv[1]
diff --git a/nova/api/__init__.py b/nova/api/__init__.py
index 803470570..26fed847b 100644
--- a/nova/api/__init__.py
+++ b/nova/api/__init__.py
@@ -24,6 +24,7 @@ Root WSGI middleware for all API controllers.
:ec2api_subdomain: subdomain running the EC2 API (default: ec2)
"""
+import logging
import routes
import webob.dec
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index dd87d1f71..51d33bcc6 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -26,8 +26,8 @@ import webob
import webob.dec
import webob.exc
-from nova import exception
from nova import context
+from nova import exception
from nova import flags
from nova import wsgi
from nova.api.ec2 import apirequest
@@ -37,16 +37,82 @@ from nova.auth import manager
FLAGS = flags.FLAGS
+flags.DEFINE_boolean('use_forwarded_for', False,
+ 'Treat X-Forwarded-For as the canonical remote address. '
+ 'Only enable this if you have a sanitizing proxy.')
+flags.DEFINE_boolean('use_lockout', False,
+ 'Whether or not to use lockout middleware.')
+flags.DEFINE_integer('lockout_attempts', 5,
+ 'Number of failed auths before lockout.')
+flags.DEFINE_integer('lockout_minutes', 15,
+ 'Number of minutes to lockout if triggered.')
+flags.DEFINE_integer('lockout_window', 15,
+ 'Number of minutes for lockout window.')
+flags.DEFINE_list('lockout_memcached_servers', None,
+ 'Memcached servers or None for in process cache.')
+
+
_log = logging.getLogger("api")
_log.setLevel(logging.DEBUG)
class API(wsgi.Middleware):
-
"""Routing for all EC2 API requests."""
def __init__(self):
self.application = Authenticate(Router(Authorizer(Executor())))
+ if FLAGS.use_lockout:
+ self.application = Lockout(self.application)
+
+
+class Lockout(wsgi.Middleware):
+ """Lockout for x minutes on y failed auths in a z minute period.
+
+ x = lockout_timeout flag
+ y = lockout_window flag
+ z = lockout_attempts flag
+
+ Uses memcached if lockout_memcached_servers flag is set, otherwise it
+ uses a very simple in-proccess cache. Due to the simplicity of
+ the implementation, the timeout window is started with the first
+ failed request, so it will block if there are x failed logins within
+ that period.
+
+ There is a possible race condition where simultaneous requests could
+ sneak in before the lockout hits, but this is extremely rare and would
+ only result in a couple of extra failed attempts."""
+
+ def __init__(self, application):
+ """middleware can use fake for testing."""
+ if FLAGS.lockout_memcached_servers:
+ import memcache
+ else:
+ from nova import fakememcache as memcache
+ self.mc = memcache.Client(FLAGS.lockout_memcached_servers,
+ debug=0)
+ super(Lockout, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ access_key = str(req.params['AWSAccessKeyId'])
+ failures_key = "authfailures-%s" % access_key
+ failures = int(self.mc.get(failures_key) or 0)
+ if failures >= FLAGS.lockout_attempts:
+ detail = "Too many failed authentications."
+ raise webob.exc.HTTPForbidden(detail=detail)
+ res = req.get_response(self.application)
+ if res.status_int == 403:
+ failures = self.mc.incr(failures_key)
+ if failures is None:
+ # NOTE(vish): To use incr, failures has to be a string.
+ self.mc.set(failures_key, '1', time=FLAGS.lockout_window * 60)
+ elif failures >= FLAGS.lockout_attempts:
+ _log.warn('Access key %s has had %d failed authentications'
+ ' and will be locked out for %d minutes.' %
+ (access_key, failures, FLAGS.lockout_minutes))
+ self.mc.set(failures_key, str(failures),
+ time=FLAGS.lockout_minutes * 60)
+ return res
class Authenticate(wsgi.Middleware):
@@ -81,9 +147,12 @@ class Authenticate(wsgi.Middleware):
raise webob.exc.HTTPForbidden()
# Authenticated!
+ remote_address = req.remote_addr
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
ctxt = context.RequestContext(user=user,
project=project,
- remote_address=req.remote_addr)
+ remote_address=remote_address)
req.environ['ec2.context'] = ctxt
return self.application
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index e1a21f122..e09261f00 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,7 +27,6 @@ import datetime
import logging
import re
import os
-import time
from nova import context
import IPy
@@ -699,19 +698,24 @@ class CloudController(object):
context.project_id)
raise quota.QuotaError(_("Address quota exceeded. You cannot "
"allocate any more addresses"))
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we allocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
public_ip = rpc.call(context,
- network_topic,
+ FLAGS.network_topic,
{"method": "allocate_floating_ip",
"args": {"project_id": context.project_id}})
return {'addressSet': [{'publicIp': public_ip}]}
def release_address(self, context, public_ip, **kwargs):
- # NOTE(vish): Should we make sure this works?
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we deallocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
rpc.cast(context,
- network_topic,
+ FLAGS.network_topic,
{"method": "deallocate_floating_ip",
"args": {"floating_address": floating_ip_ref['address']}})
return {'releaseResponse': ["Address released."]}
@@ -722,7 +726,10 @@ class CloudController(object):
fixed_address = db.instance_get_fixed_address(context,
instance_ref['id'])
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): Perhaps we should just pass this on to compute and
+ # let compute communicate with network.
+ network_topic = self.compute_api.get_network_topic(context,
+ internal_id)
rpc.cast(context,
network_topic,
{"method": "associate_floating_ip",
@@ -732,24 +739,18 @@ class CloudController(object):
def disassociate_address(self, context, public_ip, **kwargs):
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): Get the topic from the host name of the network of
+ # the associated fixed ip.
+ if not floating_ip_ref.get('fixed_ip'):
+ raise exception.ApiError('Address is not associated.')
+ host = floating_ip_ref['fixed_ip']['network']['host']
+ topic = db.queue_get_for(context, FLAGS.network_topic, host)
rpc.cast(context,
- network_topic,
+ topic,
{"method": "disassociate_floating_ip",
"args": {"floating_address": floating_ip_ref['address']}})
return {'disassociateResponse': ["Address disassociated."]}
- def _get_network_topic(self, context):
- """Retrieves the network host for a project"""
- network_ref = self.network_manager.get_network(context)
- host = network_ref['host']
- if not host:
- host = rpc.call(context,
- FLAGS.network_topic,
- {"method": "set_network_host",
- "args": {"network_id": network_ref['id']}})
- return db.queue_get_for(context, FLAGS.network_topic, host)
-
def run_instances(self, context, **kwargs):
max_count = int(kwargs.get('max_count', 1))
instances = self.compute_api.create_instances(context,
diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index 0e9e686ff..f832863a9 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -23,9 +23,13 @@ import logging
import webob.dec
import webob.exc
+from nova import flags
from nova.api.ec2 import cloud
+FLAGS = flags.FLAGS
+
+
class MetadataRequestHandler(object):
"""Serve metadata from the EC2 API."""
@@ -63,10 +67,13 @@ class MetadataRequestHandler(object):
@webob.dec.wsgify
def __call__(self, req):
cc = cloud.CloudController()
- meta_data = cc.get_metadata(req.remote_addr)
+ remote_address = req.remote_addr
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
+ meta_data = cc.get_metadata(remote_address)
if meta_data is None:
logging.error(_('Failed to get metadata for ip: %s') %
- req.remote_addr)
+ remote_address)
raise webob.exc.HTTPNotFound()
data = self.lookup(req.path_info, meta_data)
if data is None:
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index de95ee548..c49399f28 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -45,10 +45,14 @@ from nova.auth import manager
FLAGS = flags.FLAGS
-flags.DEFINE_string('nova_api_auth',
- 'nova.api.openstack.auth.BasicApiAuthManager',
+flags.DEFINE_string('os_api_auth',
+ 'nova.api.openstack.auth.AuthMiddleware',
'The auth mechanism to use for the OpenStack API implemenation')
+flags.DEFINE_string('os_api_ratelimiting',
+ 'nova.api.openstack.ratelimiting.RateLimitingMiddleware',
+ 'Default ratelimiting implementation for the Openstack API')
+
flags.DEFINE_bool('allow_admin_api',
False,
'When True, this API service will accept admin operations.')
@@ -58,7 +62,10 @@ class API(wsgi.Middleware):
"""WSGI entry point for all OpenStack API requests."""
def __init__(self):
- app = AuthMiddleware(RateLimitingMiddleware(APIRouter()))
+ auth_middleware = utils.import_class(FLAGS.os_api_auth)
+ ratelimiting_middleware = \
+ utils.import_class(FLAGS.os_api_ratelimiting)
+ app = auth_middleware(ratelimiting_middleware(APIRouter()))
super(API, self).__init__(app)
@webob.dec.wsgify
@@ -67,101 +74,11 @@ class API(wsgi.Middleware):
return req.get_response(self.application)
except Exception as ex:
logging.warn(_("Caught error: %s") % str(ex))
- logging.debug(traceback.format_exc())
+ logging.error(traceback.format_exc())
exc = webob.exc.HTTPInternalServerError(explanation=str(ex))
return faults.Fault(exc)
-class AuthMiddleware(wsgi.Middleware):
- """Authorize the openstack API request or return an HTTP Forbidden."""
-
- def __init__(self, application):
- self.auth_driver = utils.import_class(FLAGS.nova_api_auth)()
- super(AuthMiddleware, self).__init__(application)
-
- @webob.dec.wsgify
- def __call__(self, req):
- if 'X-Auth-Token' not in req.headers:
- return self.auth_driver.authenticate(req)
-
- user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"])
-
- if not user:
- return faults.Fault(webob.exc.HTTPUnauthorized())
-
- req.environ['nova.context'] = context.RequestContext(user, user)
- return self.application
-
-
-class RateLimitingMiddleware(wsgi.Middleware):
- """Rate limit incoming requests according to the OpenStack rate limits."""
-
- def __init__(self, application, service_host=None):
- """Create a rate limiting middleware that wraps the given application.
-
- By default, rate counters are stored in memory. If service_host is
- specified, the middleware instead relies on the ratelimiting.WSGIApp
- at the given host+port to keep rate counters.
- """
- super(RateLimitingMiddleware, self).__init__(application)
- if not service_host:
- #TODO(gundlach): These limits were based on limitations of Cloud
- #Servers. We should revisit them in Nova.
- self.limiter = ratelimiting.Limiter(limits={
- 'DELETE': (100, ratelimiting.PER_MINUTE),
- 'PUT': (10, ratelimiting.PER_MINUTE),
- 'POST': (10, ratelimiting.PER_MINUTE),
- 'POST servers': (50, ratelimiting.PER_DAY),
- 'GET changes-since': (3, ratelimiting.PER_MINUTE),
- })
- else:
- self.limiter = ratelimiting.WSGIAppProxy(service_host)
-
- @webob.dec.wsgify
- def __call__(self, req):
- """Rate limit the request.
-
- If the request should be rate limited, return a 413 status with a
- Retry-After header giving the time when the request would succeed.
- """
- action_name = self.get_action_name(req)
- if not action_name:
- # Not rate limited
- return self.application
- delay = self.get_delay(action_name,
- req.environ['nova.context'].user_id)
- if delay:
- # TODO(gundlach): Get the retry-after format correct.
- exc = webob.exc.HTTPRequestEntityTooLarge(
- explanation=_('Too many requests.'),
- headers={'Retry-After': time.time() + delay})
- raise faults.Fault(exc)
- return self.application
-
- def get_delay(self, action_name, username):
- """Return the delay for the given action and username, or None if
- the action would not be rate limited.
- """
- if action_name == 'POST servers':
- # "POST servers" is a POST, so it counts against "POST" too.
- # Attempt the "POST" first, lest we are rate limited by "POST" but
- # use up a precious "POST servers" call.
- delay = self.limiter.perform("POST", username=username)
- if delay:
- return delay
- return self.limiter.perform(action_name, username=username)
-
- def get_action_name(self, req):
- """Return the action name for this request."""
- if req.method == 'GET' and 'changes-since' in req.GET:
- return 'GET changes-since'
- if req.method == 'POST' and req.path_info.startswith('/servers'):
- return 'POST servers'
- if req.method in ['PUT', 'POST', 'DELETE']:
- return req.method
- return None
-
-
class APIRouter(wsgi.Router):
"""
Routes requests on the OpenStack API to the appropriate controller
@@ -194,22 +111,3 @@ class APIRouter(wsgi.Router):
controller=sharedipgroups.Controller())
super(APIRouter, self).__init__(mapper)
-
-
-def limited(items, req):
- """Return a slice of items according to requested offset and limit.
-
- items - a sliceable
- req - wobob.Request possibly containing offset and limit GET variables.
- offset is where to start in the list, and limit is the maximum number
- of items to return.
-
- If limit is not specified, 0, or > 1000, defaults to 1000.
- """
- offset = int(req.GET.get('offset', 0))
- limit = int(req.GET.get('limit', 0))
- if not limit:
- limit = 1000
- limit = min(1000, limit)
- range_end = offset + limit
- return items[offset:range_end]
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index fcda97ab1..e24e58fd3 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -1,3 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.import datetime
+
import datetime
import hashlib
import json
@@ -7,29 +24,45 @@ import webob.exc
import webob.dec
from nova import auth
+from nova import context
from nova import db
from nova import flags
from nova import manager
from nova import utils
+from nova import wsgi
from nova.api.openstack import faults
FLAGS = flags.FLAGS
-class Context(object):
- pass
-
-
-class BasicApiAuthManager(object):
- """ Implements a somewhat rudimentary version of OpenStack Auth"""
+class AuthMiddleware(wsgi.Middleware):
+ """Authorize the openstack API request or return an HTTP Forbidden."""
- def __init__(self, db_driver=None):
+ def __init__(self, application, db_driver=None):
if not db_driver:
db_driver = FLAGS.db_driver
self.db = utils.import_object(db_driver)
self.auth = auth.manager.AuthManager()
- self.context = Context()
- super(BasicApiAuthManager, self).__init__()
+ super(AuthMiddleware, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ if not self.has_authentication(req):
+ return self.authenticate(req)
+
+ user = self.get_user_by_authentication(req)
+
+ if not user:
+ return faults.Fault(webob.exc.HTTPUnauthorized())
+
+ req.environ['nova.context'] = context.RequestContext(user, user)
+ return self.application
+
+ def has_authentication(self, req):
+ return 'X-Auth-Token' in req.headers
+
+ def get_user_by_authentication(self, req):
+ return self.authorize_token(req.headers["X-Auth-Token"])
def authenticate(self, req):
# Unless the request is explicitly made against /<version>/ don't
@@ -68,11 +101,12 @@ class BasicApiAuthManager(object):
This method will also remove the token if the timestamp is older than
2 days ago.
"""
- token = self.db.auth_get_token(self.context, token_hash)
+ ctxt = context.get_admin_context()
+ token = self.db.auth_get_token(ctxt, token_hash)
if token:
delta = datetime.datetime.now() - token.created_at
if delta.days >= 2:
- self.db.auth_destroy_token(self.context, token)
+ self.db.auth_destroy_token(ctxt, token)
else:
return self.auth.get_user(token.user_id)
return None
@@ -84,6 +118,7 @@ class BasicApiAuthManager(object):
key - string API key
req - webob.Request object
"""
+ ctxt = context.get_admin_context()
user = self.auth.get_user_from_access_key(key)
if user and user.name == username:
token_hash = hashlib.sha1('%s%s%f' % (username, key,
@@ -95,6 +130,6 @@ class BasicApiAuthManager(object):
token_dict['server_management_url'] = req.url
token_dict['storage_url'] = ''
token_dict['user_id'] = user.id
- token = self.db.auth_create_token(self.context, token_dict)
+ token = self.db.auth_create_token(ctxt, token_dict)
return token, user
return None, None
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
new file mode 100644
index 000000000..ac0572c96
--- /dev/null
+++ b/nova/api/openstack/common.py
@@ -0,0 +1,36 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+def limited(items, req):
+ """Return a slice of items according to requested offset and limit.
+
+ items - a sliceable
+ req - wobob.Request possibly containing offset and limit GET variables.
+ offset is where to start in the list, and limit is the maximum number
+ of items to return.
+
+ If limit is not specified, 0, or > 1000, defaults to 1000.
+ """
+
+ offset = int(req.GET.get('offset', 0))
+ limit = int(req.GET.get('limit', 0))
+ if not limit:
+ limit = 1000
+ limit = min(1000, limit)
+ range_end = offset + limit
+ return items[offset:range_end]
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index f23f74fd1..f620d4107 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -18,6 +18,7 @@
from webob import exc
from nova.api.openstack import faults
+from nova.api.openstack import common
from nova.compute import instance_types
from nova import wsgi
import nova.api.openstack
@@ -39,7 +40,7 @@ class Controller(wsgi.Controller):
def detail(self, req):
"""Return all flavors in detail."""
items = [self.show(req, id)['flavor'] for id in self._all_ids()]
- items = nova.api.openstack.limited(items, req)
+ items = common.limited(items, req)
return dict(flavors=items)
def show(self, req, id):
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 4a0a8e6f1..d3312aba8 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -22,6 +22,8 @@ from nova import utils
from nova import wsgi
import nova.api.openstack
import nova.image.service
+
+from nova.api.openstack import common
from nova.api.openstack import faults
@@ -48,11 +50,12 @@ class Controller(wsgi.Controller):
"""Return all public images in detail."""
try:
images = self._service.detail(req.environ['nova.context'])
- images = nova.api.openstack.limited(images, req)
+ images = common.limited(images, req)
except NotImplementedError:
# Emulate detail() using repeated calls to show()
+ ctxt = req.environ['nova.context']
images = self._service.index(ctxt)
- images = nova.api.openstack.limited(images, req)
+ images = common.limited(images, req)
images = [self._service.show(ctxt, i['id']) for i in images]
return dict(images=images)
diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py
index 918caf055..91a8b2e55 100644
--- a/nova/api/openstack/ratelimiting/__init__.py
+++ b/nova/api/openstack/ratelimiting/__init__.py
@@ -1,3 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.import datetime
+
"""Rate limiting of arbitrary actions."""
import httplib
@@ -6,6 +23,8 @@ import urllib
import webob.dec
import webob.exc
+from nova import wsgi
+from nova.api.openstack import faults
# Convenience constants for the limits dictionary passed to Limiter().
PER_SECOND = 1
@@ -14,6 +33,83 @@ PER_HOUR = 60 * 60
PER_DAY = 60 * 60 * 24
+class RateLimitingMiddleware(wsgi.Middleware):
+ """Rate limit incoming requests according to the OpenStack rate limits."""
+
+ def __init__(self, application, service_host=None):
+ """Create a rate limiting middleware that wraps the given application.
+
+ By default, rate counters are stored in memory. If service_host is
+ specified, the middleware instead relies on the ratelimiting.WSGIApp
+ at the given host+port to keep rate counters.
+ """
+ if not service_host:
+ #TODO(gundlach): These limits were based on limitations of Cloud
+ #Servers. We should revisit them in Nova.
+ self.limiter = Limiter(limits={
+ 'DELETE': (100, PER_MINUTE),
+ 'PUT': (10, PER_MINUTE),
+ 'POST': (10, PER_MINUTE),
+ 'POST servers': (50, PER_DAY),
+ 'GET changes-since': (3, PER_MINUTE),
+ })
+ else:
+ self.limiter = WSGIAppProxy(service_host)
+ super(RateLimitingMiddleware, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Rate limit the request.
+
+ If the request should be rate limited, return a 413 status with a
+ Retry-After header giving the time when the request would succeed.
+ """
+ return self.limited_request(req, self.application)
+
+ def limited_request(self, req, application):
+ """Rate limit the request.
+
+ If the request should be rate limited, return a 413 status with a
+ Retry-After header giving the time when the request would succeed.
+ """
+ action_name = self.get_action_name(req)
+ if not action_name:
+ # Not rate limited
+ return application
+ delay = self.get_delay(action_name,
+ req.environ['nova.context'].user_id)
+ if delay:
+ # TODO(gundlach): Get the retry-after format correct.
+ exc = webob.exc.HTTPRequestEntityTooLarge(
+ explanation=('Too many requests.'),
+ headers={'Retry-After': time.time() + delay})
+ raise faults.Fault(exc)
+ return application
+
+ def get_delay(self, action_name, username):
+ """Return the delay for the given action and username, or None if
+ the action would not be rate limited.
+ """
+ if action_name == 'POST servers':
+ # "POST servers" is a POST, so it counts against "POST" too.
+ # Attempt the "POST" first, lest we are rate limited by "POST" but
+ # use up a precious "POST servers" call.
+ delay = self.limiter.perform("POST", username=username)
+ if delay:
+ return delay
+ return self.limiter.perform(action_name, username=username)
+
+ def get_action_name(self, req):
+ """Return the action name for this request."""
+ if req.method == 'GET' and 'changes-since' in req.GET:
+ return 'GET changes-since'
+ if req.method == 'POST' and req.path_info.startswith('/servers'):
+ return 'POST servers'
+ if req.method in ['PUT', 'POST', 'DELETE']:
+ return req.method
+ return None
+
+
class Limiter(object):
"""Class providing rate limiting of arbitrary actions."""
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 5c3322f7c..8d60e2cab 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -22,6 +22,7 @@ from webob import exc
from nova import exception
from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
from nova.auth import manager as auth_manager
from nova.compute import api as compute_api
@@ -98,7 +99,7 @@ class Controller(wsgi.Controller):
"""
instance_list = self.compute_api.get_instances(
req.environ['nova.context'])
- limited_list = nova.api.openstack.limited(instance_list, req)
+ limited_list = common.limited(instance_list, req)
res = [entity_maker(inst)['server'] for inst in limited_list]
return _entity_list(res)
diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py
index e805ca9f7..75d02905c 100644
--- a/nova/api/openstack/sharedipgroups.py
+++ b/nova/api/openstack/sharedipgroups.py
@@ -19,4 +19,22 @@ from nova import wsgi
class Controller(wsgi.Controller):
- pass
+ """ The Shared IP Groups Controller for the Openstack API """
+
+ def index(self, req):
+ raise NotImplementedError
+
+ def show(self, req, id):
+ raise NotImplementedError
+
+ def update(self, req, id):
+ raise NotImplementedError
+
+ def delete(self, req, id):
+ raise NotImplementedError
+
+ def detail(self, req):
+ raise NotImplementedError
+
+ def create(self, req):
+ raise NotImplementedError
diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py
index 33cd03430..4466051f0 100644
--- a/nova/auth/fakeldap.py
+++ b/nova/auth/fakeldap.py
@@ -150,6 +150,9 @@ def _match(key, value, attrs):
"""Match a given key and value against an attribute list."""
if key not in attrs:
return False
+ # This is a wild card search. Implemented as all or nothing for now.
+ if value == "*":
+ return True
if key != "objectclass":
return value in attrs[key]
# it is an objectclass check, so check subclasses
diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py
index e289ea5a2..7616ff112 100644
--- a/nova/auth/ldapdriver.py
+++ b/nova/auth/ldapdriver.py
@@ -32,11 +32,16 @@ from nova import flags
FLAGS = flags.FLAGS
+flags.DEFINE_integer('ldap_schema_version', 2,
+ 'Current version of the LDAP schema')
flags.DEFINE_string('ldap_url', 'ldap://localhost',
'Point this at your ldap server')
flags.DEFINE_string('ldap_password', 'changeme', 'LDAP password')
flags.DEFINE_string('ldap_user_dn', 'cn=Manager,dc=example,dc=com',
'DN of admin user')
+flags.DEFINE_string('ldap_user_id_attribute', 'uid', 'Attribute to use as id')
+flags.DEFINE_string('ldap_user_name_attribute', 'cn',
+ 'Attribute to use as name')
flags.DEFINE_string('ldap_user_unit', 'Users', 'OID for Users')
flags.DEFINE_string('ldap_user_subtree', 'ou=Users,dc=example,dc=com',
'OU for Users')
@@ -73,10 +78,20 @@ class LdapDriver(object):
Defines enter and exit and therefore supports the with/as syntax.
"""
+ project_pattern = '(owner=*)'
+ isadmin_attribute = 'isNovaAdmin'
+ project_attribute = 'owner'
+ project_objectclass = 'groupOfNames'
+
def __init__(self):
"""Imports the LDAP module"""
self.ldap = __import__('ldap')
self.conn = None
+ if FLAGS.ldap_schema_version == 1:
+ LdapDriver.project_pattern = '(objectclass=novaProject)'
+ LdapDriver.isadmin_attribute = 'isAdmin'
+ LdapDriver.project_attribute = 'projectManager'
+ LdapDriver.project_objectclass = 'novaProject'
def __enter__(self):
"""Creates the connection to LDAP"""
@@ -104,13 +119,13 @@ class LdapDriver(object):
"""Retrieve project by id"""
dn = 'cn=%s,%s' % (pid,
FLAGS.ldap_project_subtree)
- attr = self.__find_object(dn, '(objectclass=novaProject)')
+ attr = self.__find_object(dn, LdapDriver.project_pattern)
return self.__to_project(attr)
def get_users(self):
"""Retrieve list of users"""
attrs = self.__find_objects(FLAGS.ldap_user_subtree,
- '(objectclass=novaUser)')
+ '(objectclass=novaUser)')
users = []
for attr in attrs:
user = self.__to_user(attr)
@@ -120,7 +135,7 @@ class LdapDriver(object):
def get_projects(self, uid=None):
"""Retrieve list of projects"""
- pattern = '(objectclass=novaProject)'
+ pattern = LdapDriver.project_pattern
if uid:
pattern = "(&%s(member=%s))" % (pattern, self.__uid_to_dn(uid))
attrs = self.__find_objects(FLAGS.ldap_project_subtree,
@@ -139,23 +154,25 @@ class LdapDriver(object):
# Malformed entries are useless, replace attributes found.
attr = []
if 'secretKey' in user.keys():
- attr.append((self.ldap.MOD_REPLACE, 'secretKey', \
- [secret_key]))
+ attr.append((self.ldap.MOD_REPLACE, 'secretKey',
+ [secret_key]))
else:
- attr.append((self.ldap.MOD_ADD, 'secretKey', \
- [secret_key]))
+ attr.append((self.ldap.MOD_ADD, 'secretKey',
+ [secret_key]))
if 'accessKey' in user.keys():
- attr.append((self.ldap.MOD_REPLACE, 'accessKey', \
- [access_key]))
+ attr.append((self.ldap.MOD_REPLACE, 'accessKey',
+ [access_key]))
else:
- attr.append((self.ldap.MOD_ADD, 'accessKey', \
- [access_key]))
- if 'isAdmin' in user.keys():
- attr.append((self.ldap.MOD_REPLACE, 'isAdmin', \
- [str(is_admin).upper()]))
+ attr.append((self.ldap.MOD_ADD, 'accessKey',
+ [access_key]))
+ if LdapDriver.isadmin_attribute in user.keys():
+ attr.append((self.ldap.MOD_REPLACE,
+ LdapDriver.isadmin_attribute,
+ [str(is_admin).upper()]))
else:
- attr.append((self.ldap.MOD_ADD, 'isAdmin', \
- [str(is_admin).upper()]))
+ attr.append((self.ldap.MOD_ADD,
+ LdapDriver.isadmin_attribute,
+ [str(is_admin).upper()]))
self.conn.modify_s(self.__uid_to_dn(name), attr)
return self.get_user(name)
else:
@@ -168,12 +185,12 @@ class LdapDriver(object):
'inetOrgPerson',
'novaUser']),
('ou', [FLAGS.ldap_user_unit]),
- ('uid', [name]),
+ (FLAGS.ldap_user_id_attribute, [name]),
('sn', [name]),
- ('cn', [name]),
+ (FLAGS.ldap_user_name_attribute, [name]),
('secretKey', [secret_key]),
('accessKey', [access_key]),
- ('isAdmin', [str(is_admin).upper()]),
+ (LdapDriver.isadmin_attribute, [str(is_admin).upper()]),
]
self.conn.add_s(self.__uid_to_dn(name), attr)
return self.__to_user(dict(attr))
@@ -204,10 +221,10 @@ class LdapDriver(object):
if not manager_dn in members:
members.append(manager_dn)
attr = [
- ('objectclass', ['novaProject']),
+ ('objectclass', [LdapDriver.project_objectclass]),
('cn', [name]),
('description', [description]),
- ('projectManager', [manager_dn]),
+ (LdapDriver.project_attribute, [manager_dn]),
('member', members)]
self.conn.add_s('cn=%s,%s' % (name, FLAGS.ldap_project_subtree), attr)
return self.__to_project(dict(attr))
@@ -223,7 +240,8 @@ class LdapDriver(object):
"manager %s doesn't exist")
% manager_uid)
manager_dn = self.__uid_to_dn(manager_uid)
- attr.append((self.ldap.MOD_REPLACE, 'projectManager', manager_dn))
+ attr.append((self.ldap.MOD_REPLACE, LdapDriver.project_attribute,
+ manager_dn))
if description:
attr.append((self.ldap.MOD_REPLACE, 'description', description))
self.conn.modify_s('cn=%s,%s' % (project_id,
@@ -283,10 +301,9 @@ class LdapDriver(object):
return roles
else:
project_dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree)
- roles = self.__find_objects(project_dn,
- '(&(&(objectclass=groupOfNames)'
- '(!(objectclass=novaProject)))'
- '(member=%s))' % self.__uid_to_dn(uid))
+ query = ('(&(&(objectclass=groupOfNames)(!%s))(member=%s))' %
+ (LdapDriver.project_pattern, self.__uid_to_dn(uid)))
+ roles = self.__find_objects(project_dn, query)
return [role['cn'][0] for role in roles]
def delete_user(self, uid):
@@ -300,14 +317,15 @@ class LdapDriver(object):
# Retrieve user by name
user = self.__get_ldap_user(uid)
if 'secretKey' in user.keys():
- attr.append((self.ldap.MOD_DELETE, 'secretKey', \
- user['secretKey']))
+ attr.append((self.ldap.MOD_DELETE, 'secretKey',
+ user['secretKey']))
if 'accessKey' in user.keys():
- attr.append((self.ldap.MOD_DELETE, 'accessKey', \
- user['accessKey']))
- if 'isAdmin' in user.keys():
- attr.append((self.ldap.MOD_DELETE, 'isAdmin', \
- user['isAdmin']))
+ attr.append((self.ldap.MOD_DELETE, 'accessKey',
+ user['accessKey']))
+ if LdapDriver.isadmin_attribute in user.keys():
+ attr.append((self.ldap.MOD_DELETE,
+ LdapDriver.isadmin_attribute,
+ user[LdapDriver.isadmin_attribute]))
self.conn.modify_s(self.__uid_to_dn(uid), attr)
else:
# Delete entry
@@ -329,7 +347,8 @@ class LdapDriver(object):
if secret_key:
attr.append((self.ldap.MOD_REPLACE, 'secretKey', secret_key))
if admin is not None:
- attr.append((self.ldap.MOD_REPLACE, 'isAdmin', str(admin).upper()))
+ attr.append((self.ldap.MOD_REPLACE, LdapDriver.isadmin_attribute,
+ str(admin).upper()))
self.conn.modify_s(self.__uid_to_dn(uid), attr)
def __user_exists(self, uid):
@@ -347,7 +366,7 @@ class LdapDriver(object):
def __get_ldap_user(self, uid):
"""Retrieve LDAP user entry by id"""
attr = self.__find_object(self.__uid_to_dn(uid),
- '(objectclass=novaUser)')
+ '(objectclass=novaUser)')
return attr
def __find_object(self, dn, query=None, scope=None):
@@ -383,19 +402,21 @@ class LdapDriver(object):
def __find_role_dns(self, tree):
"""Find dns of role objects in given tree"""
- return self.__find_dns(tree,
- '(&(objectclass=groupOfNames)(!(objectclass=novaProject)))')
+ query = ('(&(objectclass=groupOfNames)(!%s))' %
+ LdapDriver.project_pattern)
+ return self.__find_dns(tree, query)
def __find_group_dns_with_member(self, tree, uid):
"""Find dns of group objects in a given tree that contain member"""
- dns = self.__find_dns(tree,
- '(&(objectclass=groupOfNames)(member=%s))' %
- self.__uid_to_dn(uid))
+ query = ('(&(objectclass=groupOfNames)(member=%s))' %
+ self.__uid_to_dn(uid))
+ dns = self.__find_dns(tree, query)
return dns
def __group_exists(self, dn):
"""Check if group exists"""
- return self.__find_object(dn, '(objectclass=groupOfNames)') is not None
+ query = '(objectclass=groupOfNames)'
+ return self.__find_object(dn, query) is not None
@staticmethod
def __role_to_dn(role, project_id=None):
@@ -417,9 +438,9 @@ class LdapDriver(object):
if member_uids is not None:
for member_uid in member_uids:
if not self.__user_exists(member_uid):
- raise exception.NotFound(_("Group can't be created "
- "because user %s doesn't exist")
- % member_uid)
+ raise exception.NotFound("Group can't be created "
+ "because user %s doesn't exist" %
+ member_uid)
members.append(self.__uid_to_dn(member_uid))
dn = self.__uid_to_dn(uid)
if not dn in members:
@@ -434,9 +455,8 @@ class LdapDriver(object):
def __is_in_group(self, uid, group_dn):
"""Check if user is in group"""
if not self.__user_exists(uid):
- raise exception.NotFound(_("User %s can't be searched in group "
- "because the user doesn't exist")
- % uid)
+ raise exception.NotFound("User %s can't be searched in group "
+ "because the user doesn't exist" % uid)
if not self.__group_exists(group_dn):
return False
res = self.__find_object(group_dn,
@@ -447,12 +467,11 @@ class LdapDriver(object):
def __add_to_group(self, uid, group_dn):
"""Add user to group"""
if not self.__user_exists(uid):
- raise exception.NotFound(_("User %s can't be added to the group "
- "because the user doesn't exist")
- % uid)
+ raise exception.NotFound("User %s can't be added to the group "
+ "because the user doesn't exist" % uid)
if not self.__group_exists(group_dn):
- raise exception.NotFound(_("The group at dn %s doesn't exist")
- % group_dn)
+ raise exception.NotFound("The group at dn %s doesn't exist" %
+ group_dn)
if self.__is_in_group(uid, group_dn):
raise exception.Duplicate(_("User %s is already a member of "
"the group %s") % (uid, group_dn))
@@ -462,18 +481,17 @@ class LdapDriver(object):
def __remove_from_group(self, uid, group_dn):
"""Remove user from group"""
if not self.__group_exists(group_dn):
- raise exception.NotFound(_("The group at dn %s doesn't exist")
- % group_dn)
+ raise exception.NotFound("The group at dn %s doesn't exist" %
+ group_dn)
if not self.__user_exists(uid):
- raise exception.NotFound(_("User %s can't be removed from the "
- "group because the user doesn't exist")
- % uid)
+ raise exception.NotFound("User %s can't be removed from the "
+ "group because the user doesn't exist" %
+ uid)
if not self.__is_in_group(uid, group_dn):
- raise exception.NotFound(_("User %s is not a member of the group")
- % uid)
+ raise exception.NotFound("User %s is not a member of the group" %
+ uid)
# NOTE(vish): remove user from group and any sub_groups
- sub_dns = self.__find_group_dns_with_member(
- group_dn, uid)
+ sub_dns = self.__find_group_dns_with_member(group_dn, uid)
for sub_dn in sub_dns:
self.__safe_remove_from_group(uid, sub_dn)
@@ -491,9 +509,8 @@ class LdapDriver(object):
def __remove_from_all(self, uid):
"""Remove user from all roles and projects"""
if not self.__user_exists(uid):
- raise exception.NotFound(_("User %s can't be removed from all "
- "because the user doesn't exist")
- % uid)
+ raise exception.NotFound("User %s can't be removed from all "
+ "because the user doesn't exist" % uid)
role_dns = self.__find_group_dns_with_member(
FLAGS.role_project_subtree, uid)
for role_dn in role_dns:
@@ -521,13 +538,13 @@ class LdapDriver(object):
if attr is None:
return None
if ('accessKey' in attr.keys() and 'secretKey' in attr.keys() \
- and 'isAdmin' in attr.keys()):
+ and LdapDriver.isadmin_attribute in attr.keys()):
return {
- 'id': attr['uid'][0],
- 'name': attr['cn'][0],
+ 'id': attr[FLAGS.ldap_user_id_attribute][0],
+ 'name': attr[FLAGS.ldap_user_name_attribute][0],
'access': attr['accessKey'][0],
'secret': attr['secretKey'][0],
- 'admin': (attr['isAdmin'][0] == 'TRUE')}
+ 'admin': (attr[LdapDriver.isadmin_attribute][0] == 'TRUE')}
else:
return None
@@ -539,7 +556,8 @@ class LdapDriver(object):
return {
'id': attr['cn'][0],
'name': attr['cn'][0],
- 'project_manager_id': self.__dn_to_uid(attr['projectManager'][0]),
+ 'project_manager_id':
+ self.__dn_to_uid(attr[LdapDriver.project_attribute][0]),
'description': attr.get('description', [None])[0],
'member_ids': [self.__dn_to_uid(x) for x in member_dns]}
@@ -549,9 +567,10 @@ class LdapDriver(object):
return dn.split(',')[0].split('=')[1]
@staticmethod
- def __uid_to_dn(dn):
+ def __uid_to_dn(uid):
"""Convert uid to dn"""
- return 'uid=%s,%s' % (dn, FLAGS.ldap_user_subtree)
+ return (FLAGS.ldap_user_id_attribute + '=%s,%s'
+ % (uid, FLAGS.ldap_user_subtree))
class FakeLdapDriver(LdapDriver):
diff --git a/nova/auth/nova_openldap.schema b/nova/auth/nova_openldap.schema
index 4047361de..539a5c42d 100644
--- a/nova/auth/nova_openldap.schema
+++ b/nova/auth/nova_openldap.schema
@@ -1,7 +1,9 @@
#
# Person object for Nova
# inetorgperson with extra attributes
-# Author: Vishvananda Ishaya <vishvananda@yahoo.com>
+# Schema version: 2
+# Authors: Vishvananda Ishaya <vishvananda@gmail.com>
+# Ryan Lane <rlane@wikimedia.org>
#
#
@@ -31,54 +33,18 @@ attributetype (
)
attributetype (
- novaAttrs:3
- NAME 'keyFingerprint'
- DESC 'Fingerprint of private key'
- EQUALITY caseIgnoreMatch
- SUBSTR caseIgnoreSubstringsMatch
- SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
- SINGLE-VALUE
- )
-
-attributetype (
novaAttrs:4
- NAME 'isAdmin'
- DESC 'Is user an administrator?'
+ NAME 'isNovaAdmin'
+ DESC 'Is user an nova administrator?'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE
)
-attributetype (
- novaAttrs:5
- NAME 'projectManager'
- DESC 'Project Managers of a project'
- SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
- )
-
objectClass (
novaOCs:1
NAME 'novaUser'
DESC 'access and secret keys'
AUXILIARY
- MUST ( uid )
- MAY ( accessKey $ secretKey $ isAdmin )
- )
-
-objectClass (
- novaOCs:2
- NAME 'novaKeyPair'
- DESC 'Key pair for User'
- SUP top
- STRUCTURAL
- MUST ( cn $ sshPublicKey $ keyFingerprint )
- )
-
-objectClass (
- novaOCs:3
- NAME 'novaProject'
- DESC 'Container for project'
- SUP groupOfNames
- STRUCTURAL
- MUST ( cn $ projectManager )
+ MAY ( accessKey $ secretKey $ isNovaAdmin )
)
diff --git a/nova/auth/nova_sun.schema b/nova/auth/nova_sun.schema
index e925e05e4..4a6a78839 100644
--- a/nova/auth/nova_sun.schema
+++ b/nova/auth/nova_sun.schema
@@ -1,16 +1,13 @@
#
# Person object for Nova
# inetorgperson with extra attributes
-# Author: Vishvananda Ishaya <vishvananda@yahoo.com>
-# Modified for strict RFC 4512 compatibility by: Ryan Lane <ryan@ryandlane.com>
+# Schema version: 2
+# Authors: Vishvananda Ishaya <vishvananda@gmail.com>
+# Ryan Lane <rlane@wikimedia.org>
#
# using internet experimental oid arc as per BP64 3.1
dn: cn=schema
attributeTypes: ( 1.3.6.1.3.1.666.666.3.1 NAME 'accessKey' DESC 'Key for accessing data' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
attributeTypes: ( 1.3.6.1.3.1.666.666.3.2 NAME 'secretKey' DESC 'Secret key' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
-attributeTypes: ( 1.3.6.1.3.1.666.666.3.3 NAME 'keyFingerprint' DESC 'Fingerprint of private key' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE)
-attributeTypes: ( 1.3.6.1.3.1.666.666.3.4 NAME 'isAdmin' DESC 'Is user an administrator?' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
-attributeTypes: ( 1.3.6.1.3.1.666.666.3.5 NAME 'projectManager' DESC 'Project Managers of a project' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
-objectClasses: ( 1.3.6.1.3.1.666.666.4.1 NAME 'novaUser' DESC 'access and secret keys' SUP top AUXILIARY MUST ( uid ) MAY ( accessKey $ secretKey $ isAdmin ) )
-objectClasses: ( 1.3.6.1.3.1.666.666.4.2 NAME 'novaKeyPair' DESC 'Key pair for User' SUP top STRUCTURAL MUST ( cn $ sshPublicKey $ keyFingerprint ) )
-objectClasses: ( 1.3.6.1.3.1.666.666.4.3 NAME 'novaProject' DESC 'Container for project' SUP groupOfNames STRUCTURAL MUST ( cn $ projectManager ) )
+attributeTypes: ( 1.3.6.1.3.1.666.666.3.4 NAME 'isNovaAdmin' DESC 'Is user a nova administrator?' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+objectClasses: ( 1.3.6.1.3.1.666.666.4.1 NAME 'novaUser' DESC 'access and secret keys' SUP top AUXILIARY MAY ( accessKey $ secretKey $ isNovaAdmin ) )
diff --git a/nova/auth/opendj.sh b/nova/auth/opendj.sh
index 8052c077d..1a280e5a8 100755
--- a/nova/auth/opendj.sh
+++ b/nova/auth/opendj.sh
@@ -32,7 +32,6 @@ abspath=`dirname "$(cd "${0%/*}" 2>/dev/null; echo "$PWD"/"${0##*/}")"`
schemapath='/var/opendj/instance/config/schema'
cp $abspath/openssh-lpk_sun.schema $schemapath/97-openssh-lpk_sun.ldif
cp $abspath/nova_sun.schema $schemapath/98-nova_sun.ldif
-chown opendj:opendj $schemapath/97-openssh-lpk_sun.ldif
chown opendj:opendj $schemapath/98-nova_sun.ldif
cat >/etc/ldap/ldap.conf <<LDAP_CONF_EOF
diff --git a/nova/auth/slap.sh b/nova/auth/slap.sh
index 797675d2e..95c61dafd 100755
--- a/nova/auth/slap.sh
+++ b/nova/auth/slap.sh
@@ -22,7 +22,7 @@ apt-get install -y slapd ldap-utils python-ldap
abspath=`dirname "$(cd "${0%/*}" 2>/dev/null; echo "$PWD"/"${0##*/}")"`
cp $abspath/openssh-lpk_openldap.schema /etc/ldap/schema/openssh-lpk_openldap.schema
-cp $abspath/nova_openldap.schema /etc/ldap/schema/nova_openldap.schema
+cp $abspath/nova_openldap.schema /etc/ldap/schema/nova.schema
mv /etc/ldap/slapd.conf /etc/ldap/slapd.conf.orig
cat >/etc/ldap/slapd.conf <<SLAPD_CONF_EOF
@@ -33,7 +33,6 @@ cat >/etc/ldap/slapd.conf <<SLAPD_CONF_EOF
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
-include /etc/ldap/schema/openssh-lpk_openldap.schema
include /etc/ldap/schema/nova.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 75434176e..4953fe559 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -53,6 +53,23 @@ class ComputeAPI(base.Base):
self.image_service = image_service
super(ComputeAPI, self).__init__(**kwargs)
+ def get_network_topic(self, context, instance_id):
+ try:
+ instance = self.db.instance_get_by_internal_id(context,
+ instance_id)
+ except exception.NotFound as e:
+ logging.warning("Instance %d was not found in get_network_topic",
+ instance_id)
+ raise e
+
+ host = instance['host']
+ if not host:
+ raise exception.Error("Instance %d has no host" % instance_id)
+ topic = self.db.queue_get_for(context, FLAGS.compute_topic, host)
+ return rpc.call(context,
+ topic,
+ {"method": "get_network_topic", "args": {'fake': 1}})
+
def create_instances(self, context, instance_type, image_id, min_count=1,
max_count=1, kernel_id=None, ramdisk_id=None,
display_name='', description='', key_name=None,
@@ -152,18 +169,6 @@ class ComputeAPI(base.Base):
instance = self.update_instance(context, instance_id, **updates)
instances.append(instance)
- # TODO(vish): This probably should be done in the scheduler
- # or in compute as a call. The network should be
- # allocated after the host is assigned and setup
- # can happen at the same time.
- address = self.network_manager.allocate_fixed_ip(context,
- instance_id,
- is_vpn)
- rpc.cast(elevated,
- self._get_network_topic(context),
- {"method": "setup_fixed_ip",
- "args": {"address": address}})
-
logging.debug(_("Casting to scheduler for %s/%s's instance %s"),
context.project_id, context.user_id, instance_id)
rpc.cast(context,
@@ -226,28 +231,6 @@ class ComputeAPI(base.Base):
state=0,
terminated_at=datetime.datetime.utcnow())
- # FIXME(ja): where should network deallocate occur?
- address = self.db.instance_get_floating_address(context,
- instance['id'])
- if address:
- logging.debug(_("Disassociating address %s") % address)
- # NOTE(vish): Right now we don't really care if the ip is
- # disassociated. We may need to worry about
- # checking this later. Perhaps in the scheduler?
- rpc.cast(context,
- self._get_network_topic(context),
- {"method": "disassociate_floating_ip",
- "args": {"floating_address": address}})
-
- address = self.db.instance_get_fixed_address(context, instance['id'])
- if address:
- logging.debug(_("Deallocating address %s") % address)
- # NOTE(vish): Currently, nothing needs to be done on the
- # network node until release. If this changes,
- # we will need to cast here.
- self.network_manager.deallocate_fixed_ip(context.elevated(),
- address)
-
host = instance['host']
if host:
rpc.cast(context,
@@ -317,14 +300,3 @@ class ComputeAPI(base.Base):
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "unrescue_instance",
"args": {"instance_id": instance['id']}})
-
- def _get_network_topic(self, context):
- """Retrieves the network host for a project"""
- network_ref = self.network_manager.get_network(context)
- host = network_ref['host']
- if not host:
- host = rpc.call(context,
- FLAGS.network_topic,
- {"method": "set_network_host",
- "args": {"network_id": network_ref['id']}})
- return self.db.queue_get_for(context, FLAGS.network_topic, host)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 135663ab5..ff8202cca 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -40,6 +40,7 @@ import logging
from nova import exception
from nova import flags
from nova import manager
+from nova import rpc
from nova import utils
from nova.compute import power_state
@@ -48,6 +49,8 @@ flags.DEFINE_string('instances_path', '$state_path/instances',
'where instances are stored on disk')
flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection',
'Driver to use for controlling virtualization')
+flags.DEFINE_string('stub_network', False,
+ 'Stub network related code')
class ComputeManager(manager.Manager):
@@ -65,6 +68,12 @@ class ComputeManager(manager.Manager):
self.volume_manager = utils.import_object(FLAGS.volume_manager)
super(ComputeManager, self).__init__(*args, **kwargs)
+ def init_host(self):
+ """Do any initialization that needs to be run if this is a
+ standalone service.
+ """
+ self.driver.init_host()
+
def _update_state(self, context, instance_id):
"""Update the state of an instance from the driver info."""
# FIXME(ja): include other fields from state?
@@ -76,6 +85,20 @@ class ComputeManager(manager.Manager):
state = power_state.NOSTATE
self.db.instance_set_state(context, instance_id, state)
+ def get_network_topic(self, context, **_kwargs):
+ """Retrieves the network host for a project on this host"""
+ # TODO(vish): This method should be memoized. This will make
+ # the call to get_network_host cheaper, so that
+ # it can pas messages instead of checking the db
+ # locally.
+ if FLAGS.stub_network:
+ host = FLAGS.network_host
+ else:
+ host = self.network_manager.get_network_host(context)
+ return self.db.queue_get_for(context,
+ FLAGS.network_topic,
+ host)
+
@exception.wrap_exception
def refresh_security_group(self, context, security_group_id, **_kwargs):
"""This call passes stright through to the virtualization driver."""
@@ -89,11 +112,30 @@ class ComputeManager(manager.Manager):
if instance_ref['name'] in self.driver.list_instances():
raise exception.Error(_("Instance has already been created"))
logging.debug(_("instance %s: starting..."), instance_id)
- self.network_manager.setup_compute_network(context, instance_id)
self.db.instance_update(context,
instance_id,
{'host': self.host})
+ self.db.instance_set_state(context,
+ instance_id,
+ power_state.NOSTATE,
+ 'networking')
+
+ is_vpn = instance_ref['image_id'] == FLAGS.vpn_image_id
+ # NOTE(vish): This could be a cast because we don't do anything
+ # with the address currently, but I'm leaving it as
+ # a call to ensure that network setup completes. We
+ # will eventually also need to save the address here.
+ if not FLAGS.stub_network:
+ address = rpc.call(context,
+ self.get_network_topic(context),
+ {"method": "allocate_fixed_ip",
+ "args": {"instance_id": instance_id,
+ "vpn": is_vpn}})
+
+ self.network_manager.setup_compute_network(context,
+ instance_id)
+
# TODO(vish) check to make sure the availability zone matches
self.db.instance_set_state(context,
instance_id,
@@ -119,9 +161,34 @@ class ComputeManager(manager.Manager):
def terminate_instance(self, context, instance_id):
"""Terminate an instance on this machine."""
context = context.elevated()
- logging.debug(_("instance %s: terminating"), instance_id)
instance_ref = self.db.instance_get(context, instance_id)
+
+ if not FLAGS.stub_network:
+ address = self.db.instance_get_floating_address(context,
+ instance_ref['id'])
+ if address:
+ logging.debug(_("Disassociating address %s") % address)
+ # NOTE(vish): Right now we don't really care if the ip is
+ # disassociated. We may need to worry about
+ # checking this later.
+ rpc.cast(context,
+ self.get_network_topic(context),
+ {"method": "disassociate_floating_ip",
+ "args": {"floating_address": address}})
+
+ address = self.db.instance_get_fixed_address(context,
+ instance_ref['id'])
+ if address:
+ logging.debug(_("Deallocating address %s") % address)
+ # NOTE(vish): Currently, nothing needs to be done on the
+ # network node until release. If this changes,
+ # we will need to cast here.
+ self.network_manager.deallocate_fixed_ip(context.elevated(),
+ address)
+
+ logging.debug(_("instance %s: terminating"), instance_id)
+
volumes = instance_ref.get('volumes', []) or []
for volume in volumes:
self.detach_volume(context, instance_id, volume['id'])
@@ -138,8 +205,8 @@ class ComputeManager(manager.Manager):
def reboot_instance(self, context, instance_id):
"""Reboot an instance on this server."""
context = context.elevated()
- instance_ref = self.db.instance_get(context, instance_id)
self._update_state(context, instance_id)
+ instance_ref = self.db.instance_get(context, instance_id)
if instance_ref['state'] != power_state.RUNNING:
logging.warn(_('trying to reboot a non-running '
@@ -153,6 +220,7 @@ class ComputeManager(manager.Manager):
instance_id,
power_state.NOSTATE,
'rebooting')
+ self.network_manager.setup_compute_network(context, instance_id)
self.driver.reboot(instance_ref)
self._update_state(context, instance_id)
@@ -168,6 +236,7 @@ class ComputeManager(manager.Manager):
instance_id,
power_state.NOSTATE,
'rescuing')
+ self.network_manager.setup_compute_network(context, instance_id)
self.driver.rescue(instance_ref)
self._update_state(context, instance_id)
diff --git a/nova/crypto.py b/nova/crypto.py
index e4133ac85..b8405552d 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -22,6 +22,7 @@ Includes root and intermediate CAs, SSH key_pairs and x509 certificates.
"""
import base64
+import gettext
import hashlib
import logging
import os
@@ -33,6 +34,8 @@ import utils
import M2Crypto
+gettext.install('nova', unicode=1)
+
from nova import context
from nova import db
from nova import flags
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 5ba458241..7e945e4cb 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -463,6 +463,7 @@ def floating_ip_get_by_address(context, address, session=None):
session = get_session()
result = session.query(models.FloatingIp).\
+ options(joinedload_all('fixed_ip.network')).\
filter_by(address=address).\
filter_by(deleted=can_read_deleted(context)).\
first()
@@ -659,13 +660,17 @@ def instance_get(context, instance_id, session=None):
if is_admin_context(context):
result = session.query(models.Instance).\
+ options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload('volumes')).\
filter_by(id=instance_id).\
filter_by(deleted=can_read_deleted(context)).\
first()
elif is_user_context(context):
result = session.query(models.Instance).\
+ options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
+ options(joinedload('volumes')).\
filter_by(project_id=context.project_id).\
filter_by(id=instance_id).\
filter_by(deleted=False).\
@@ -1170,11 +1175,13 @@ def iscsi_target_create_safe(context, values):
###################
+@require_admin_context
def auth_destroy_token(_context, token):
session = get_session()
session.delete(token)
+@require_admin_context
def auth_get_token(_context, token_hash):
session = get_session()
tk = session.query(models.AuthToken).\
@@ -1185,6 +1192,7 @@ def auth_get_token(_context, token_hash):
return tk
+@require_admin_context
def auth_create_token(_context, token):
tk = models.AuthToken()
tk.update(token)
diff --git a/nova/fakememcache.py b/nova/fakememcache.py
new file mode 100644
index 000000000..67f46dbdc
--- /dev/null
+++ b/nova/fakememcache.py
@@ -0,0 +1,59 @@
+# 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.
+
+"""Super simple fake memcache client."""
+
+import utils
+
+
+class Client(object):
+ """Replicates a tiny subset of memcached client interface."""
+
+ def __init__(self, *args, **kwargs):
+ """Ignores the passed in args"""
+ self.cache = {}
+
+ def get(self, key):
+ """Retrieves the value for a key or None."""
+ (timeout, value) = self.cache.get(key, (0, None))
+ if timeout == 0 or utils.utcnow_ts() < timeout:
+ return value
+ return None
+
+ def set(self, key, value, time=0, min_compress_len=0):
+ """Sets the value for a key."""
+ timeout = 0
+ if time != 0:
+ timeout = utils.utcnow_ts() + time
+ self.cache[key] = (timeout, value)
+ return True
+
+ def add(self, key, value, time=0, min_compress_len=0):
+ """Sets the value for a key if it doesn't exist."""
+ if not self.get(key) is None:
+ return False
+ return self.set(key, value, time, min_compress_len)
+
+ def incr(self, key, delta=1):
+ """Increments the value for a key."""
+ value = self.get(key)
+ if value is None:
+ return None
+ new_value = int(value) + delta
+ self.cache[key] = (self.cache[key][0], str(new_value))
+ return new_value
diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py
index 41e686cff..79d8b894d 100644
--- a/nova/fakerabbit.py
+++ b/nova/fakerabbit.py
@@ -25,6 +25,10 @@ from carrot.backends import base
from eventlet import greenthread
+EXCHANGES = {}
+QUEUES = {}
+
+
class Message(base.BaseMessage):
pass
@@ -68,81 +72,63 @@ class Queue(object):
return self._queue.get()
-class Backend(object):
- """ Singleton backend for testing """
- class __impl(base.BaseBackend):
- def __init__(self, *args, **kwargs):
- #super(__impl, self).__init__(*args, **kwargs)
- self._exchanges = {}
- self._queues = {}
-
- def _reset_all(self):
- self._exchanges = {}
- self._queues = {}
-
- def queue_declare(self, queue, **kwargs):
- if queue not in self._queues:
- logging.debug(_('Declaring queue %s'), queue)
- self._queues[queue] = Queue(queue)
-
- def exchange_declare(self, exchange, type, *args, **kwargs):
- if exchange not in self._exchanges:
- logging.debug(_('Declaring exchange %s'), exchange)
- self._exchanges[exchange] = Exchange(exchange, type)
-
- def queue_bind(self, queue, exchange, routing_key, **kwargs):
- logging.debug(_('Binding %s to %s with key %s'),
- queue, exchange, routing_key)
- self._exchanges[exchange].bind(self._queues[queue].push,
- routing_key)
-
- def declare_consumer(self, queue, callback, *args, **kwargs):
- self.current_queue = queue
- self.current_callback = callback
-
- def consume(self, *args, **kwargs):
- while True:
- item = self.get(self.current_queue)
- if item:
- self.current_callback(item)
- raise StopIteration()
- greenthread.sleep(0)
-
- def get(self, queue, no_ack=False):
- if not queue in self._queues or not self._queues[queue].size():
- return None
- (message_data, content_type, content_encoding) = \
- self._queues[queue].pop()
- message = Message(backend=self, body=message_data,
- content_type=content_type,
- content_encoding=content_encoding)
- message.result = True
- logging.debug(_('Getting from %s: %s'), queue, message)
- return message
-
- def prepare_message(self, message_data, delivery_mode,
- content_type, content_encoding, **kwargs):
- """Prepare message for sending."""
- return (message_data, content_type, content_encoding)
-
- def publish(self, message, exchange, routing_key, **kwargs):
- if exchange in self._exchanges:
- self._exchanges[exchange].publish(
- message, routing_key=routing_key)
-
- __instance = None
-
- def __init__(self, *args, **kwargs):
- if Backend.__instance is None:
- Backend.__instance = Backend.__impl(*args, **kwargs)
- self.__dict__['_Backend__instance'] = Backend.__instance
-
- def __getattr__(self, attr):
- return getattr(self.__instance, attr)
-
- def __setattr__(self, attr, value):
- return setattr(self.__instance, attr, value)
+class Backend(base.BaseBackend):
+ def queue_declare(self, queue, **kwargs):
+ global QUEUES
+ if queue not in QUEUES:
+ logging.debug(_('Declaring queue %s'), queue)
+ QUEUES[queue] = Queue(queue)
+
+ def exchange_declare(self, exchange, type, *args, **kwargs):
+ global EXCHANGES
+ if exchange not in EXCHANGES:
+ logging.debug(_('Declaring exchange %s'), exchange)
+ EXCHANGES[exchange] = Exchange(exchange, type)
+
+ def queue_bind(self, queue, exchange, routing_key, **kwargs):
+ global EXCHANGES
+ global QUEUES
+ logging.debug(_('Binding %s to %s with key %s'),
+ queue, exchange, routing_key)
+ EXCHANGES[exchange].bind(QUEUES[queue].push, routing_key)
+
+ def declare_consumer(self, queue, callback, *args, **kwargs):
+ self.current_queue = queue
+ self.current_callback = callback
+
+ def consume(self, limit=None):
+ while True:
+ item = self.get(self.current_queue)
+ if item:
+ self.current_callback(item)
+ raise StopIteration()
+ greenthread.sleep(0)
+
+ def get(self, queue, no_ack=False):
+ global QUEUES
+ if not queue in QUEUES or not QUEUES[queue].size():
+ return None
+ (message_data, content_type, content_encoding) = QUEUES[queue].pop()
+ message = Message(backend=self, body=message_data,
+ content_type=content_type,
+ content_encoding=content_encoding)
+ message.result = True
+ logging.debug(_('Getting from %s: %s'), queue, message)
+ return message
+
+ def prepare_message(self, message_data, delivery_mode,
+ content_type, content_encoding, **kwargs):
+ """Prepare message for sending."""
+ return (message_data, content_type, content_encoding)
+
+ def publish(self, message, exchange, routing_key, **kwargs):
+ global EXCHANGES
+ if exchange in EXCHANGES:
+ EXCHANGES[exchange].publish(message, routing_key=routing_key)
def reset_all():
- Backend()._reset_all()
+ global EXCHANGES
+ global QUEUES
+ EXCHANGES = {}
+ QUEUES = {}
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 11f61d764..931a89554 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -19,7 +19,6 @@ Implements vlans, bridges, and iptables rules using linux utilities.
import logging
import os
-import signal
# TODO(ja): does the definition of network_path belong here?
@@ -50,8 +49,10 @@ flags.DEFINE_string('routing_source_ip', utils.get_my_ip(),
'Public IP of network host')
flags.DEFINE_bool('use_nova_chains', False,
'use the nova_ routing chains instead of default')
-
-DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)]
+flags.DEFINE_string('dns_server', None,
+ 'if set, uses specific dns server for dnsmasq')
+flags.DEFINE_string('dmz_cidr', '10.128.0.0/24',
+ 'dmz range that should be accepted')
def metadata_forward():
@@ -63,22 +64,71 @@ def metadata_forward():
def init_host():
"""Basic networking setup goes here"""
+
+ if FLAGS.use_nova_chains:
+ _execute("sudo iptables -N nova_input", check_exit_code=False)
+ _execute("sudo iptables -D %s -j nova_input" % FLAGS.input_chain,
+ check_exit_code=False)
+ _execute("sudo iptables -A %s -j nova_input" % FLAGS.input_chain)
+
+ _execute("sudo iptables -N nova_forward", check_exit_code=False)
+ _execute("sudo iptables -D FORWARD -j nova_forward",
+ check_exit_code=False)
+ _execute("sudo iptables -A FORWARD -j nova_forward")
+
+ _execute("sudo iptables -N nova_output", check_exit_code=False)
+ _execute("sudo iptables -D OUTPUT -j nova_output",
+ check_exit_code=False)
+ _execute("sudo iptables -A OUTPUT -j nova_output")
+
+ _execute("sudo iptables -t nat -N nova_prerouting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -D PREROUTING -j nova_prerouting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -A PREROUTING -j nova_prerouting")
+
+ _execute("sudo iptables -t nat -N nova_postrouting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -D POSTROUTING -j nova_postrouting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -A POSTROUTING -j nova_postrouting")
+
+ _execute("sudo iptables -t nat -N nova_snatting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -D POSTROUTING -j nova_snatting",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -A POSTROUTING -j nova_snatting")
+
+ _execute("sudo iptables -t nat -N nova_output", check_exit_code=False)
+ _execute("sudo iptables -t nat -D OUTPUT -j nova_output",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -A OUTPUT -j nova_output")
+ else:
+ # NOTE(vish): This makes it easy to ensure snatting rules always
+ # come after the accept rules in the postrouting chain
+ _execute("sudo iptables -t nat -N SNATTING",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -D POSTROUTING -j SNATTING",
+ check_exit_code=False)
+ _execute("sudo iptables -t nat -A POSTROUTING -j SNATTING")
+
# NOTE(devcamcar): Cloud public SNAT entries and the default
# SNAT rule for outbound traffic.
- _confirm_rule("POSTROUTING", "-t nat -s %s "
+ _confirm_rule("SNATTING", "-t nat -s %s "
"-j SNAT --to-source %s"
- % (FLAGS.fixed_range, FLAGS.routing_source_ip))
+ % (FLAGS.fixed_range, FLAGS.routing_source_ip), append=True)
- _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" %
- FLAGS.fixed_range)
+ _confirm_rule("POSTROUTING", "-t nat -s %s -d %s -j ACCEPT" %
+ (FLAGS.fixed_range, FLAGS.dmz_cidr))
_confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" %
{'range': FLAGS.fixed_range})
-def bind_floating_ip(floating_ip):
+def bind_floating_ip(floating_ip, check_exit_code=True):
"""Bind ip to public interface"""
_execute("sudo ip addr add %s dev %s" % (floating_ip,
- FLAGS.public_interface))
+ FLAGS.public_interface),
+ check_exit_code=check_exit_code)
def unbind_floating_ip(floating_ip):
@@ -100,27 +150,16 @@ def ensure_floating_forward(floating_ip, fixed_ip):
"""Ensure floating ip forwarding rule"""
_confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
- _confirm_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
+ _confirm_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
- # TODO(joshua): Get these from the secgroup datastore entries
- _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
- % (fixed_ip))
- for (protocol, port) in DEFAULT_PORTS:
- _confirm_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT"
- % (fixed_ip, protocol, port))
def remove_floating_forward(floating_ip, fixed_ip):
"""Remove forwarding for floating ip"""
_remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s"
% (floating_ip, fixed_ip))
- _remove_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s"
+ _remove_rule("SNATTING", "-t nat -s %s -j SNAT --to %s"
% (fixed_ip, floating_ip))
- _remove_rule("FORWARD", "-d %s -p icmp -j ACCEPT"
- % (fixed_ip))
- for (protocol, port) in DEFAULT_PORTS:
- _remove_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT"
- % (fixed_ip, protocol, port))
def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
@@ -158,6 +197,15 @@ def ensure_bridge(bridge, interface, net_attrs=None):
net_attrs['netmask']))
else:
_execute("sudo ifconfig %s up" % bridge)
+ if FLAGS.use_nova_chains:
+ (out, err) = _execute("sudo iptables -N nova_forward",
+ check_exit_code=False)
+ if err != 'iptables: Chain already exists.\n':
+ # NOTE(vish): chain didn't exist link chain
+ _execute("sudo iptables -D FORWARD -j nova_forward",
+ check_exit_code=False)
+ _execute("sudo iptables -A FORWARD -j nova_forward")
+
_confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge)
_confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge)
@@ -234,13 +282,17 @@ def _device_exists(device):
return not err
-def _confirm_rule(chain, cmd):
+def _confirm_rule(chain, cmd, append=False):
"""Delete and re-add iptables rule"""
if FLAGS.use_nova_chains:
chain = "nova_%s" % chain.lower()
+ if append:
+ loc = "-A"
+ else:
+ loc = "-I"
_execute("sudo iptables --delete %s %s" % (chain, cmd),
check_exit_code=False)
- _execute("sudo iptables -I %s %s" % (chain, cmd))
+ _execute("sudo iptables %s %s %s" % (loc, chain, cmd))
def _remove_rule(chain, cmd):
@@ -263,6 +315,8 @@ def _dnsmasq_cmd(net):
' --dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'),
' --dhcp-script=%s' % FLAGS.dhcpbridge,
' --leasefile-ro']
+ if FLAGS.dns_server:
+ cmd.append(' -h -R --server=%s' % FLAGS.dns_server)
return ''.join(cmd)
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 8c300e305..16aa8f895 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -47,6 +47,7 @@ topologies. All of the network commands are issued to a subclass of
import datetime
import logging
import math
+import socket
import IPy
@@ -56,6 +57,7 @@ from nova import exception
from nova import flags
from nova import manager
from nova import utils
+from nova import rpc
FLAGS = flags.FLAGS
@@ -87,6 +89,10 @@ flags.DEFINE_bool('update_dhcp_on_disassociate', False,
'Whether to update dhcp when fixed_ip is disassociated')
flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600,
'Seconds after which a deallocated ip is disassociated')
+flags.DEFINE_string('network_host', socket.gethostname(),
+ 'Network host to use for ip allocation in flat modes')
+flags.DEFINE_bool('fake_call', False,
+ 'If True, skip using the queue and make local calls')
class AddressAlreadyAllocated(exception.Error):
@@ -112,6 +118,16 @@ class NetworkManager(manager.Manager):
ctxt = context.get_admin_context()
for network in self.db.host_get_networks(ctxt, self.host):
self._on_set_network_host(ctxt, network['id'])
+ floating_ips = self.db.floating_ip_get_all_by_host(ctxt,
+ self.host)
+ for floating_ip in floating_ips:
+ if floating_ip.get('fixed_ip', None):
+ fixed_address = floating_ip['fixed_ip']['address']
+ # NOTE(vish): The False here is because we ignore the case
+ # that the ip is already bound.
+ self.driver.bind_floating_ip(floating_ip['address'], False)
+ self.driver.ensure_floating_forward(floating_ip['address'],
+ fixed_address)
def set_network_host(self, context, network_id):
"""Safely sets the host of the network."""
@@ -213,8 +229,8 @@ class NetworkManager(manager.Manager):
network_ref = self.db.fixed_ip_get_network(context, address)
self.driver.update_dhcp(context, network_ref['id'])
- def get_network(self, context):
- """Get the network for the current context."""
+ def get_network_host(self, context):
+ """Get the network host for the current context."""
raise NotImplementedError()
def create_networks(self, context, num_networks, network_size,
@@ -302,10 +318,6 @@ class FlatManager(NetworkManager):
"""Network is created manually."""
pass
- def setup_fixed_ip(self, context, address):
- """Currently no setup."""
- pass
-
def create_networks(self, context, cidr, num_networks, network_size,
*args, **kwargs):
"""Create networks based on parameters."""
@@ -326,14 +338,25 @@ class FlatManager(NetworkManager):
if network_ref:
self._create_fixed_ips(context, network_ref['id'])
- def get_network(self, context):
- """Get the network for the current context."""
- # NOTE(vish): To support mutilple network hosts, This could randomly
- # select from multiple networks instead of just
- # returning the one. It could also potentially be done
- # in the scheduler.
- return self.db.network_get_by_bridge(context,
- FLAGS.flat_network_bridge)
+ def get_network_host(self, context):
+ """Get the network host for the current context."""
+ network_ref = self.db.network_get_by_bridge(context,
+ FLAGS.flat_network_bridge)
+ # NOTE(vish): If the network has no host, use the network_host flag.
+ # This could eventually be a a db lookup of some sort, but
+ # a flag is easy to handle for now.
+ host = network_ref['host']
+ if not host:
+ topic = self.db.queue_get_for(context,
+ FLAGS.network_topic,
+ FLAGS.network_host)
+ if FLAGS.fake_call:
+ return self.set_network_host(context, network_ref['id'])
+ host = rpc.call(context,
+ FLAGS.network_topic,
+ {"method": "set_network_host",
+ "args": {"network_id": network_ref['id']}})
+ return host
def _on_set_network_host(self, context, network_id):
"""Called when this host becomes the host for a network."""
@@ -364,10 +387,16 @@ class FlatDHCPManager(FlatManager):
self.driver.ensure_bridge(network_ref['bridge'],
FLAGS.flat_interface)
- def setup_fixed_ip(self, context, address):
+ def allocate_fixed_ip(self, context, instance_id, *args, **kwargs):
"""Setup dhcp for this network."""
+ address = super(FlatDHCPManager, self).allocate_fixed_ip(context,
+ instance_id,
+ *args,
+ **kwargs)
network_ref = db.fixed_ip_get_network(context, address)
- self.driver.update_dhcp(context, network_ref['id'])
+ if not FLAGS.fake_network:
+ self.driver.update_dhcp(context, network_ref['id'])
+ return address
def deallocate_fixed_ip(self, context, address, *args, **kwargs):
"""Returns a fixed ip to the pool."""
@@ -436,33 +465,20 @@ class VlanManager(NetworkManager):
network_ref['id'],
instance_id)
self.db.fixed_ip_update(context, address, {'allocated': True})
+ if not FLAGS.fake_network:
+ self.driver.update_dhcp(context, network_ref['id'])
return address
def deallocate_fixed_ip(self, context, address, *args, **kwargs):
"""Returns a fixed ip to the pool."""
self.db.fixed_ip_update(context, address, {'allocated': False})
- def setup_fixed_ip(self, context, address):
- """Sets forwarding rules and dhcp for fixed ip."""
- fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address)
- network_ref = self.db.fixed_ip_get_network(context, address)
- if self.db.instance_is_vpn(context, fixed_ip_ref['instance_id']):
- self.driver.ensure_vlan_forward(network_ref['vpn_public_address'],
- network_ref['vpn_public_port'],
- network_ref['vpn_private_address'])
- self.driver.update_dhcp(context, network_ref['id'])
-
def setup_compute_network(self, context, instance_id):
"""Sets up matching network for compute hosts."""
network_ref = db.network_get_by_instance(context, instance_id)
self.driver.ensure_vlan_bridge(network_ref['vlan'],
network_ref['bridge'])
- def restart_nets(self):
- """Ensure the network for each user is enabled."""
- # TODO(vish): Implement this
- pass
-
def create_networks(self, context, cidr, num_networks, network_size,
vlan_start, vpn_start):
"""Create networks based on parameters."""
@@ -489,21 +505,45 @@ class VlanManager(NetworkManager):
if network_ref:
self._create_fixed_ips(context, network_ref['id'])
- def get_network(self, context):
+ def get_network_host(self, context):
"""Get the network for the current context."""
- return self.db.project_get_network(context.elevated(),
- context.project_id)
+ network_ref = self.db.project_get_network(context.elevated(),
+ context.project_id)
+ # NOTE(vish): If the network has no host, do a call to get an
+ # available host. This should be changed to go through
+ # the scheduler at some point.
+ host = network_ref['host']
+ if not host:
+ if FLAGS.fake_call:
+ return self.set_network_host(context, network_ref['id'])
+ host = rpc.call(context,
+ FLAGS.network_topic,
+ {"method": "set_network_host",
+ "args": {"network_id": network_ref['id']}})
+
+ return host
def _on_set_network_host(self, context, network_id):
"""Called when this host becomes the host for a network."""
network_ref = self.db.network_get(context, network_id)
- net = {}
- net['vpn_public_address'] = FLAGS.vpn_ip
- db.network_update(context, network_id, net)
+ if not network_ref['vpn_public_address']:
+ net = {}
+ address = FLAGS.vpn_ip
+ net['vpn_public_address'] = address
+ db.network_update(context, network_id, net)
+ else:
+ address = network_ref['vpn_public_address']
self.driver.ensure_vlan_bridge(network_ref['vlan'],
network_ref['bridge'],
network_ref)
- self.driver.update_dhcp(context, network_id)
+ # NOTE(vish): only ensure this forward if the address hasn't been set
+ # manually.
+ if address == FLAGS.vpn_ip:
+ self.driver.ensure_vlan_forward(FLAGS.vpn_ip,
+ network_ref['vpn_public_port'],
+ network_ref['vpn_private_address'])
+ if not FLAGS.fake_network:
+ self.driver.update_dhcp(context, network_id)
@property
def _bottom_reserved_ips(self):
diff --git a/nova/rpc.py b/nova/rpc.py
index 6e2cf051a..844088348 100644
--- a/nova/rpc.py
+++ b/nova/rpc.py
@@ -245,7 +245,7 @@ def msg_reply(msg_id, reply=None, failure=None):
logging.error(_("Returning exception %s to caller"), message)
logging.error(tb)
failure = (failure[0].__name__, str(failure[1]), tb)
- conn = Connection.instance()
+ conn = Connection.instance(True)
publisher = DirectPublisher(connection=conn, msg_id=msg_id)
try:
publisher.send({'result': reply, 'failure': failure})
diff --git a/nova/test.py b/nova/test.py
index 7076f1bf4..db5826c04 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -38,9 +38,12 @@ from nova import fakerabbit
from nova import flags
from nova import rpc
from nova.network import manager as network_manager
+from nova.tests import fake_flags
FLAGS = flags.FLAGS
+flags.DEFINE_bool('flush_db', True,
+ 'Flush the database before running fake tests')
flags.DEFINE_bool('fake_tests', True,
'should we use everything for testing')
diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py
index 9caa8c9d0..e69de29bb 100644
--- a/nova/tests/api/__init__.py
+++ b/nova/tests/api/__init__.py
@@ -1,81 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 OpenStack LLC.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-Test for the root WSGI middleware for all API controllers.
-"""
-
-import unittest
-
-import stubout
-import webob
-import webob.dec
-
-import nova.exception
-from nova import api
-from nova.tests.api.fakes import APIStub
-
-
-class Test(unittest.TestCase):
-
- def setUp(self):
- self.stubs = stubout.StubOutForTesting()
-
- def tearDown(self):
- self.stubs.UnsetAll()
-
- def _request(self, url, subdomain, **kwargs):
- environ_keys = {'HTTP_HOST': '%s.example.com' % subdomain}
- environ_keys.update(kwargs)
- req = webob.Request.blank(url, environ_keys)
- return req.get_response(api.API('ec2'))
-
- def test_openstack(self):
- self.stubs.Set(api.openstack, 'API', APIStub)
- result = self._request('/v1.0/cloud', 'api')
- self.assertEqual(result.body, "/cloud")
-
- def test_ec2(self):
- self.stubs.Set(api.ec2, 'API', APIStub)
- result = self._request('/services/cloud', 'ec2')
- self.assertEqual(result.body, "/cloud")
-
- def test_not_found(self):
- self.stubs.Set(api.ec2, 'API', APIStub)
- self.stubs.Set(api.openstack, 'API', APIStub)
- result = self._request('/test/cloud', 'ec2')
- self.assertNotEqual(result.body, "/cloud")
-
- def test_query_api_versions(self):
- result = self._request('/', 'api')
- self.assertTrue('CURRENT' in result.body)
-
- def test_metadata(self):
- def go(url):
- result = self._request(url, 'ec2', REMOTE_ADDR='128.192.151.2')
- # Each should get to the ORM layer and fail to find the IP
- self.assertRaises(nova.exception.NotFound, go, '/latest/')
- self.assertRaises(nova.exception.NotFound, go, '/2009-04-04/')
- self.assertRaises(nova.exception.NotFound, go, '/1.0/')
-
- def test_ec2_root(self):
- result = self._request('/', 'ec2')
- self.assertTrue('2007-12-15\n' in result.body)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py
index 2e357febe..9e183bd0d 100644
--- a/nova/tests/api/openstack/__init__.py
+++ b/nova/tests/api/openstack/__init__.py
@@ -17,11 +17,16 @@
import unittest
-from nova.api.openstack import limited
-from nova.api.openstack import RateLimitingMiddleware
+from nova import context
+from nova import flags
+from nova.api.openstack.ratelimiting import RateLimitingMiddleware
+from nova.api.openstack.common import limited
from nova.tests.api.fakes import APIStub
+from nova import utils
from webob import Request
+FLAGS = flags.FLAGS
+
class RateLimitingMiddlewareTest(unittest.TestCase):
@@ -46,6 +51,8 @@ class RateLimitingMiddlewareTest(unittest.TestCase):
def exhaust(self, middleware, method, url, username, times):
req = Request.blank(url, dict(REQUEST_METHOD=method),
headers={'X-Auth-User': username})
+ req.environ['nova.context'] = context.RequestContext(username,
+ username)
for i in range(times):
resp = req.get_response(middleware)
self.assertEqual(resp.status_int, 200)
@@ -62,7 +69,7 @@ class RateLimitingMiddlewareTest(unittest.TestCase):
middleware = RateLimitingMiddleware(APIStub())
self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10)
self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10)
- self.assertTrue(set(middleware.limiter._levels) ==
+ self.assertTrue(set(middleware.limiter._levels) == \
set(['usr1:POST', 'usr1:POST servers', 'usr2:POST']))
def test_POST_servers_action_correctly_ratelimited(self):
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 21b8aac1c..79663e43a 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -29,8 +29,11 @@ from nova import exception as exc
from nova import flags
from nova import utils
import nova.api.openstack.auth
-from nova.image import service
+from nova.api.openstack import auth
+from nova.api.openstack import ratelimiting
from nova.image import glance
+from nova.image import local
+from nova.image import service
from nova.tests import fake_flags
from nova.wsgi import Router
@@ -51,10 +54,11 @@ class FakeRouter(Router):
return res
-def fake_auth_init(self):
+def fake_auth_init(self, application):
self.db = FakeAuthDatabase()
self.context = Context()
self.auth = FakeAuthManager()
+ self.application = application
@webob.dec.wsgify
@@ -75,28 +79,28 @@ def stub_out_image_service(stubs):
def fake_image_show(meh, context, id):
return dict(kernelId=1, ramdiskId=1)
- stubs.Set(nova.image.local.LocalImageService, 'show', fake_image_show)
+ stubs.Set(local.LocalImageService, 'show', fake_image_show)
def stub_out_auth(stubs):
def fake_auth_init(self, app):
self.application = app
- stubs.Set(nova.api.openstack.AuthMiddleware,
+ stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__init__', fake_auth_init)
- stubs.Set(nova.api.openstack.AuthMiddleware,
+ stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__call__', fake_wsgi)
def stub_out_rate_limiting(stubs):
def fake_rate_init(self, app):
- super(nova.api.openstack.RateLimitingMiddleware, self).__init__(app)
+ super(ratelimiting.RateLimitingMiddleware, self).__init__(app)
self.application = app
- stubs.Set(nova.api.openstack.RateLimitingMiddleware,
+ stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware,
'__init__', fake_rate_init)
- stubs.Set(nova.api.openstack.RateLimitingMiddleware,
+ stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware,
'__call__', fake_wsgi)
@@ -173,7 +177,7 @@ class FakeToken(object):
class FakeRequestContext(object):
- def __init__(self, user, project):
+ def __init__(self, user, project, *args, **kwargs):
self.user_id = 1
self.project_id = 1
diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py
index 7b427c2db..489a1dfbf 100644
--- a/nova/tests/api/openstack/test_auth.py
+++ b/nova/tests/api/openstack/test_auth.py
@@ -34,7 +34,7 @@ class Test(unittest.TestCase):
def setUp(self):
self.stubs = stubout.StubOutForTesting()
- self.stubs.Set(nova.api.openstack.auth.BasicApiAuthManager,
+ self.stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__init__', fakes.fake_auth_init)
self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
fakes.FakeAuthManager.auth_data = {}
@@ -131,7 +131,7 @@ class Test(unittest.TestCase):
class TestLimiter(unittest.TestCase):
def setUp(self):
self.stubs = stubout.StubOutForTesting()
- self.stubs.Set(nova.api.openstack.auth.BasicApiAuthManager,
+ self.stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__init__', fakes.fake_auth_init)
self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
fakes.FakeAuthManager.auth_data = {}
diff --git a/nova/tests/api/test.py b/nova/tests/api/test.py
new file mode 100644
index 000000000..9caa8c9d0
--- /dev/null
+++ b/nova/tests/api/test.py
@@ -0,0 +1,81 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Test for the root WSGI middleware for all API controllers.
+"""
+
+import unittest
+
+import stubout
+import webob
+import webob.dec
+
+import nova.exception
+from nova import api
+from nova.tests.api.fakes import APIStub
+
+
+class Test(unittest.TestCase):
+
+ def setUp(self):
+ self.stubs = stubout.StubOutForTesting()
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+
+ def _request(self, url, subdomain, **kwargs):
+ environ_keys = {'HTTP_HOST': '%s.example.com' % subdomain}
+ environ_keys.update(kwargs)
+ req = webob.Request.blank(url, environ_keys)
+ return req.get_response(api.API('ec2'))
+
+ def test_openstack(self):
+ self.stubs.Set(api.openstack, 'API', APIStub)
+ result = self._request('/v1.0/cloud', 'api')
+ self.assertEqual(result.body, "/cloud")
+
+ def test_ec2(self):
+ self.stubs.Set(api.ec2, 'API', APIStub)
+ result = self._request('/services/cloud', 'ec2')
+ self.assertEqual(result.body, "/cloud")
+
+ def test_not_found(self):
+ self.stubs.Set(api.ec2, 'API', APIStub)
+ self.stubs.Set(api.openstack, 'API', APIStub)
+ result = self._request('/test/cloud', 'ec2')
+ self.assertNotEqual(result.body, "/cloud")
+
+ def test_query_api_versions(self):
+ result = self._request('/', 'api')
+ self.assertTrue('CURRENT' in result.body)
+
+ def test_metadata(self):
+ def go(url):
+ result = self._request(url, 'ec2', REMOTE_ADDR='128.192.151.2')
+ # Each should get to the ORM layer and fail to find the IP
+ self.assertRaises(nova.exception.NotFound, go, '/latest/')
+ self.assertRaises(nova.exception.NotFound, go, '/2009-04-04/')
+ self.assertRaises(nova.exception.NotFound, go, '/1.0/')
+
+ def test_ec2_root(self):
+ result = self._request('/', 'ec2')
+ self.assertTrue('2007-12-15\n' in result.body)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/nova/tests/api_integration.py b/nova/tests/api_integration.py
deleted file mode 100644
index 54403c655..000000000
--- a/nova/tests/api_integration.py
+++ /dev/null
@@ -1,54 +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.
-
-
-import boto
-from boto.ec2.regioninfo import RegionInfo
-import unittest
-
-
-ACCESS_KEY = 'fake'
-SECRET_KEY = 'fake'
-CLC_IP = '127.0.0.1'
-CLC_PORT = 8773
-REGION = 'test'
-
-
-def get_connection():
- return boto.connect_ec2(
- aws_access_key_id=ACCESS_KEY,
- aws_secret_access_key=SECRET_KEY,
- is_secure=False,
- region=RegionInfo(None, REGION, CLC_IP),
- port=CLC_PORT,
- path='/services/Cloud',
- debug=99)
-
-
-class APIIntegrationTests(unittest.TestCase):
- def test_001_get_all_images(self):
- conn = get_connection()
- res = conn.get_all_images()
-
-
-if __name__ == '__main__':
- unittest.main()
-
-#print conn.get_all_key_pairs()
-#print conn.create_key_pair
-#print conn.create_security_group('name', 'description')
diff --git a/nova/tests/db/__init__.py b/nova/tests/db/__init__.py
new file mode 100644
index 000000000..2d43aac42
--- /dev/null
+++ b/nova/tests/db/__init__.py
@@ -0,0 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+
+"""
+:mod:`db` -- Stubs for DB API
+=============================
+"""
diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py
new file mode 100644
index 000000000..05bdd172e
--- /dev/null
+++ b/nova/tests/db/fakes.py
@@ -0,0 +1,75 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Stubouts, mocks and fixtures for the test suite"""
+
+import time
+
+from nova import db
+from nova import utils
+from nova.compute import instance_types
+
+
+def stub_out_db_instance_api(stubs):
+ """ Stubs out the db API for creating Instances """
+
+ class FakeModel(object):
+ """ Stubs out for model """
+ def __init__(self, values):
+ self.values = values
+
+ def __getattr__(self, name):
+ return self.values[name]
+
+ def __getitem__(self, key):
+ if key in self.values:
+ return self.values[key]
+ else:
+ raise NotImplementedError()
+
+ def fake_instance_create(values):
+ """ Stubs out the db.instance_create method """
+
+ type_data = instance_types.INSTANCE_TYPES[values['instance_type']]
+
+ base_options = {
+ 'name': values['name'],
+ 'id': values['id'],
+ 'reservation_id': utils.generate_uid('r'),
+ 'image_id': values['image_id'],
+ 'kernel_id': values['kernel_id'],
+ 'ramdisk_id': values['ramdisk_id'],
+ 'state_description': 'scheduling',
+ 'user_id': values['user_id'],
+ 'project_id': values['project_id'],
+ 'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
+ 'instance_type': values['instance_type'],
+ 'memory_mb': type_data['memory_mb'],
+ 'mac_address': values['mac_address'],
+ 'vcpus': type_data['vcpus'],
+ 'local_gb': type_data['local_gb'],
+ }
+ return FakeModel(base_options)
+
+ def fake_network_get_by_instance(context, instance_id):
+ fields = {
+ 'bridge': 'xenbr0',
+ }
+ return FakeModel(fields)
+
+ stubs.Set(db, 'instance_create', fake_instance_create)
+ stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance)
diff --git a/nova/tests/access_unittest.py b/nova/tests/test_access.py
index 58fdea3b5..58fdea3b5 100644
--- a/nova/tests/access_unittest.py
+++ b/nova/tests/test_access.py
diff --git a/nova/tests/api_unittest.py b/nova/tests/test_api.py
index 33d4cb294..33d4cb294 100644
--- a/nova/tests/api_unittest.py
+++ b/nova/tests/test_api.py
diff --git a/nova/tests/auth_unittest.py b/nova/tests/test_auth.py
index 15d40bc53..15d40bc53 100644
--- a/nova/tests/auth_unittest.py
+++ b/nova/tests/test_auth.py
diff --git a/nova/tests/cloud_unittest.py b/nova/tests/test_cloud.py
index 53a762310..70d2c44da 100644
--- a/nova/tests/cloud_unittest.py
+++ b/nova/tests/test_cloud.py
@@ -22,20 +22,18 @@ import logging
from M2Crypto import BIO
from M2Crypto import RSA
import os
-import StringIO
import tempfile
import time
from eventlet import greenthread
-from xml.etree import ElementTree
from nova import context
from nova import crypto
from nova import db
from nova import flags
from nova import rpc
+from nova import service
from nova import test
-from nova import utils
from nova.auth import manager
from nova.compute import power_state
from nova.api.ec2 import cloud
@@ -54,7 +52,8 @@ os.makedirs(IMAGES_PATH)
class CloudTestCase(test.TestCase):
def setUp(self):
super(CloudTestCase, self).setUp()
- self.flags(connection_type='fake', images_path=IMAGES_PATH)
+ self.flags(connection_type='fake',
+ images_path=IMAGES_PATH)
self.conn = rpc.Connection.instance()
logging.getLogger().setLevel(logging.DEBUG)
@@ -62,27 +61,23 @@ class CloudTestCase(test.TestCase):
# set up our cloud
self.cloud = cloud.CloudController()
- # set up a service
- self.compute = utils.import_object(FLAGS.compute_manager)
- self.compute_consumer = rpc.AdapterConsumer(connection=self.conn,
- topic=FLAGS.compute_topic,
- proxy=self.compute)
- self.compute_consumer.attach_to_eventlet()
- self.network = utils.import_object(FLAGS.network_manager)
- self.network_consumer = rpc.AdapterConsumer(connection=self.conn,
- topic=FLAGS.network_topic,
- proxy=self.network)
- self.network_consumer.attach_to_eventlet()
+ # set up services
+ self.compute = service.Service.create(binary='nova-compute')
+ self.compute.start()
+ self.network = service.Service.create(binary='nova-network')
+ self.network.start()
self.manager = manager.AuthManager()
self.user = self.manager.create_user('admin', 'admin', 'admin', True)
self.project = self.manager.create_project('proj', 'admin', 'proj')
self.context = context.RequestContext(user=self.user,
- project=self.project)
+ project=self.project)
def tearDown(self):
self.manager.delete_project(self.project)
self.manager.delete_user(self.user)
+ self.compute.kill()
+ self.network.kill()
super(CloudTestCase, self).tearDown()
def _create_key(self, name):
@@ -109,12 +104,13 @@ class CloudTestCase(test.TestCase):
{'address': address,
'host': FLAGS.host})
self.cloud.allocate_address(self.context)
- inst = db.instance_create(self.context, {})
+ inst = db.instance_create(self.context, {'host': FLAGS.host})
fixed = self.network.allocate_fixed_ip(self.context, inst['id'])
ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id'])
self.cloud.associate_address(self.context,
instance_id=ec2_id,
public_ip=address)
+ greenthread.sleep(0.3)
self.cloud.disassociate_address(self.context,
public_ip=address)
self.cloud.release_address(self.context,
diff --git a/nova/tests/compute_unittest.py b/nova/tests/test_compute.py
index 187ca31de..348bb3351 100644
--- a/nova/tests/compute_unittest.py
+++ b/nova/tests/test_compute.py
@@ -41,6 +41,7 @@ class ComputeTestCase(test.TestCase):
logging.getLogger().setLevel(logging.DEBUG)
super(ComputeTestCase, self).setUp()
self.flags(connection_type='fake',
+ stub_network=True,
network_manager='nova.network.manager.FlatManager')
self.compute = utils.import_object(FLAGS.compute_manager)
self.compute_api = compute_api.ComputeAPI()
diff --git a/nova/tests/flags_unittest.py b/nova/tests/test_flags.py
index 707300fcf..707300fcf 100644
--- a/nova/tests/flags_unittest.py
+++ b/nova/tests/test_flags.py
diff --git a/nova/tests/test_middleware.py b/nova/tests/test_middleware.py
new file mode 100644
index 000000000..0febf52d6
--- /dev/null
+++ b/nova/tests/test_middleware.py
@@ -0,0 +1,86 @@
+# 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.
+
+import datetime
+import webob
+import webob.dec
+import webob.exc
+
+from nova.api import ec2
+from nova import flags
+from nova import test
+from nova import utils
+
+
+FLAGS = flags.FLAGS
+
+
+@webob.dec.wsgify
+def conditional_forbid(req):
+ """Helper wsgi app returns 403 if param 'die' is 1."""
+ if 'die' in req.params and req.params['die'] == '1':
+ raise webob.exc.HTTPForbidden()
+ return 'OK'
+
+
+class LockoutTestCase(test.TrialTestCase):
+ """Test case for the Lockout middleware."""
+ def setUp(self): # pylint: disable-msg=C0103
+ super(LockoutTestCase, self).setUp()
+ utils.set_time_override()
+ self.lockout = ec2.Lockout(conditional_forbid)
+
+ def tearDown(self): # pylint: disable-msg=C0103
+ utils.clear_time_override()
+ super(LockoutTestCase, self).tearDown()
+
+ def _send_bad_attempts(self, access_key, num_attempts=1):
+ """Fail x."""
+ for i in xrange(num_attempts):
+ req = webob.Request.blank('/?AWSAccessKeyId=%s&die=1' % access_key)
+ self.assertEqual(req.get_response(self.lockout).status_int, 403)
+
+ def _is_locked_out(self, access_key):
+ """Sends a test request to see if key is locked out."""
+ req = webob.Request.blank('/?AWSAccessKeyId=%s' % access_key)
+ return (req.get_response(self.lockout).status_int == 403)
+
+ def test_lockout(self):
+ self._send_bad_attempts('test', FLAGS.lockout_attempts)
+ self.assertTrue(self._is_locked_out('test'))
+
+ def test_timeout(self):
+ self._send_bad_attempts('test', FLAGS.lockout_attempts)
+ self.assertTrue(self._is_locked_out('test'))
+ utils.advance_time_seconds(FLAGS.lockout_minutes * 60)
+ self.assertFalse(self._is_locked_out('test'))
+
+ def test_multiple_keys(self):
+ self._send_bad_attempts('test1', FLAGS.lockout_attempts)
+ self.assertTrue(self._is_locked_out('test1'))
+ self.assertFalse(self._is_locked_out('test2'))
+ utils.advance_time_seconds(FLAGS.lockout_minutes * 60)
+ self.assertFalse(self._is_locked_out('test1'))
+ self.assertFalse(self._is_locked_out('test2'))
+
+ def test_window_timeout(self):
+ self._send_bad_attempts('test', FLAGS.lockout_attempts - 1)
+ self.assertFalse(self._is_locked_out('test'))
+ utils.advance_time_seconds(FLAGS.lockout_window * 60)
+ self._send_bad_attempts('test', FLAGS.lockout_attempts - 1)
+ self.assertFalse(self._is_locked_out('test'))
diff --git a/nova/tests/misc_unittest.py b/nova/tests/test_misc.py
index 3d947427a..33c1777d5 100644
--- a/nova/tests/misc_unittest.py
+++ b/nova/tests/test_misc.py
@@ -22,13 +22,13 @@ from nova.utils import parse_mailmap, str_dict_replace
class ProjectTestCase(test.TestCase):
def test_authors_up_to_date(self):
- if os.path.exists('../.bzr'):
+ if os.path.exists('.bzr'):
contributors = set()
- mailmap = parse_mailmap('../.mailmap')
+ mailmap = parse_mailmap('.mailmap')
import bzrlib.workingtree
- tree = bzrlib.workingtree.WorkingTree.open('..')
+ tree = bzrlib.workingtree.WorkingTree.open('.')
tree.lock_read()
try:
parents = tree.get_parent_ids()
@@ -42,7 +42,7 @@ class ProjectTestCase(test.TestCase):
email = author.split(' ')[-1]
contributors.add(str_dict_replace(email, mailmap))
- authors_file = open('../Authors', 'r').read()
+ authors_file = open('Authors', 'r').read()
missing = set()
for contributor in contributors:
diff --git a/nova/tests/network_unittest.py b/nova/tests/test_network.py
index bcac20585..96473ac7c 100644
--- a/nova/tests/network_unittest.py
+++ b/nova/tests/test_network.py
@@ -26,6 +26,7 @@ from nova import context
from nova import db
from nova import exception
from nova import flags
+from nova import service
from nova import test
from nova import utils
from nova.auth import manager
@@ -40,6 +41,7 @@ class NetworkTestCase(test.TestCase):
# NOTE(vish): if you change these flags, make sure to change the
# flags in the corresponding section in nova-dhcpbridge
self.flags(connection_type='fake',
+ fake_call=True,
fake_network=True,
network_size=16,
num_networks=5)
@@ -56,16 +58,13 @@ class NetworkTestCase(test.TestCase):
# create the necessary network data for the project
user_context = context.RequestContext(project=self.projects[i],
user=self.user)
- network_ref = self.network.get_network(user_context)
- self.network.set_network_host(context.get_admin_context(),
- network_ref['id'])
+ host = self.network.get_network_host(user_context.elevated())
instance_ref = self._create_instance(0)
self.instance_id = instance_ref['id']
instance_ref = self._create_instance(1)
self.instance2_id = instance_ref['id']
def tearDown(self):
- super(NetworkTestCase, self).tearDown()
# TODO(termie): this should really be instantiating clean datastores
# in between runs, one failure kills all the tests
db.instance_destroy(context.get_admin_context(), self.instance_id)
@@ -73,6 +72,7 @@ class NetworkTestCase(test.TestCase):
for project in self.projects:
self.manager.delete_project(project)
self.manager.delete_user(self.user)
+ super(NetworkTestCase, self).tearDown()
def _create_instance(self, project_num, mac=None):
if not mac:
diff --git a/nova/tests/quota_unittest.py b/nova/tests/test_quota.py
index 8cf2a5e54..8cf2a5e54 100644
--- a/nova/tests/quota_unittest.py
+++ b/nova/tests/test_quota.py
diff --git a/nova/tests/rpc_unittest.py b/nova/tests/test_rpc.py
index a2495e65a..6ea2edcab 100644
--- a/nova/tests/rpc_unittest.py
+++ b/nova/tests/test_rpc.py
@@ -33,7 +33,7 @@ class RpcTestCase(test.TestCase):
"""Test cases for rpc"""
def setUp(self):
super(RpcTestCase, self).setUp()
- self.conn = rpc.Connection.instance()
+ self.conn = rpc.Connection.instance(True)
self.receiver = TestReceiver()
self.consumer = rpc.AdapterConsumer(connection=self.conn,
topic='test',
@@ -79,6 +79,33 @@ class RpcTestCase(test.TestCase):
except rpc.RemoteError as exc:
self.assertEqual(int(exc.value), value)
+ def test_nested_calls(self):
+ """Test that we can do an rpc.call inside another call"""
+ class Nested(object):
+ @staticmethod
+ def echo(context, queue, value):
+ """Calls echo in the passed queue"""
+ logging.debug("Nested received %s, %s", queue, value)
+ ret = rpc.call(context,
+ queue,
+ {"method": "echo",
+ "args": {"value": value}})
+ logging.debug("Nested return %s", ret)
+ return value
+
+ nested = Nested()
+ conn = rpc.Connection.instance(True)
+ consumer = rpc.AdapterConsumer(connection=conn,
+ topic='nested',
+ proxy=nested)
+ consumer.attach_to_eventlet()
+ value = 42
+ result = rpc.call(self.context,
+ 'nested', {"method": "echo",
+ "args": {"queue": "test",
+ "value": value}})
+ self.assertEqual(value, result)
+
class TestReceiver(object):
"""Simple Proxy class so the consumer has methods to call
diff --git a/nova/tests/scheduler_unittest.py b/nova/tests/test_scheduler.py
index dbc195a6e..487e7b9e6 100644
--- a/nova/tests/scheduler_unittest.py
+++ b/nova/tests/test_scheduler.py
@@ -51,7 +51,7 @@ class SchedulerTestCase(test.TestCase):
"""Test case for scheduler"""
def setUp(self):
super(SchedulerTestCase, self).setUp()
- self.flags(scheduler_driver='nova.tests.scheduler_unittest.TestDriver')
+ self.flags(scheduler_driver='nova.tests.test_scheduler.TestDriver')
def test_fallback(self):
scheduler = manager.SchedulerManager()
@@ -121,6 +121,7 @@ class SimpleDriverTestCase(test.TestCase):
def setUp(self):
super(SimpleDriverTestCase, self).setUp()
self.flags(connection_type='fake',
+ stub_network=True,
max_cores=4,
max_gigabytes=4,
network_manager='nova.network.manager.FlatManager',
diff --git a/nova/tests/service_unittest.py b/nova/tests/test_service.py
index 47c092f8e..b30838ad7 100644
--- a/nova/tests/service_unittest.py
+++ b/nova/tests/test_service.py
@@ -30,7 +30,7 @@ from nova import service
from nova import manager
FLAGS = flags.FLAGS
-flags.DEFINE_string("fake_manager", "nova.tests.service_unittest.FakeManager",
+flags.DEFINE_string("fake_manager", "nova.tests.test_service.FakeManager",
"Manager for testing")
@@ -52,14 +52,14 @@ class ServiceManagerTestCase(test.TestCase):
serv = service.Service('test',
'test',
'test',
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
self.assertRaises(AttributeError, getattr, serv, 'test_method')
def test_message_gets_to_manager(self):
serv = service.Service('test',
'test',
'test',
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
serv.start()
self.assertEqual(serv.test_method(), 'manager')
@@ -67,7 +67,7 @@ class ServiceManagerTestCase(test.TestCase):
serv = ExtendedService('test',
'test',
'test',
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
serv.start()
self.assertEqual(serv.test_method(), 'service')
@@ -156,7 +156,7 @@ class ServiceTestCase(test.TestCase):
serv = service.Service(host,
binary,
topic,
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
serv.start()
serv.report_state()
@@ -186,7 +186,7 @@ class ServiceTestCase(test.TestCase):
serv = service.Service(host,
binary,
topic,
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
serv.start()
serv.report_state()
self.assert_(serv.model_disconnected)
@@ -219,7 +219,7 @@ class ServiceTestCase(test.TestCase):
serv = service.Service(host,
binary,
topic,
- 'nova.tests.service_unittest.FakeManager')
+ 'nova.tests.test_service.FakeManager')
serv.start()
serv.model_disconnected = True
serv.report_state()
diff --git a/nova/tests/twistd_unittest.py b/nova/tests/test_twistd.py
index 75007b9c8..75007b9c8 100644
--- a/nova/tests/twistd_unittest.py
+++ b/nova/tests/test_twistd.py
diff --git a/nova/tests/virt_unittest.py b/nova/tests/test_virt.py
index cb35db1e1..8dab8de2f 100644
--- a/nova/tests/virt_unittest.py
+++ b/nova/tests/test_virt.py
@@ -33,6 +33,7 @@ flags.DECLARE('instances_path', 'nova.compute.manager')
class LibvirtConnTestCase(test.TestCase):
def setUp(self):
super(LibvirtConnTestCase, self).setUp()
+ self.flags(fake_call=True)
self.manager = manager.AuthManager()
self.user = self.manager.create_user('fake', 'fake', 'fake',
admin=True)
@@ -52,45 +53,43 @@ class LibvirtConnTestCase(test.TestCase):
def test_xml_and_uri_no_ramdisk_no_kernel(self):
instance_data = dict(self.test_instance)
- self.do_test_xml_and_uri(instance_data,
- expect_kernel=False, expect_ramdisk=False)
+ self._check_xml_and_uri(instance_data,
+ expect_kernel=False, expect_ramdisk=False)
def test_xml_and_uri_no_ramdisk(self):
instance_data = dict(self.test_instance)
instance_data['kernel_id'] = 'aki-deadbeef'
- self.do_test_xml_and_uri(instance_data,
- expect_kernel=True, expect_ramdisk=False)
+ self._check_xml_and_uri(instance_data,
+ expect_kernel=True, expect_ramdisk=False)
def test_xml_and_uri_no_kernel(self):
instance_data = dict(self.test_instance)
instance_data['ramdisk_id'] = 'ari-deadbeef'
- self.do_test_xml_and_uri(instance_data,
- expect_kernel=False, expect_ramdisk=False)
+ self._check_xml_and_uri(instance_data,
+ expect_kernel=False, expect_ramdisk=False)
def test_xml_and_uri(self):
instance_data = dict(self.test_instance)
instance_data['ramdisk_id'] = 'ari-deadbeef'
instance_data['kernel_id'] = 'aki-deadbeef'
- self.do_test_xml_and_uri(instance_data,
- expect_kernel=True, expect_ramdisk=True)
+ self._check_xml_and_uri(instance_data,
+ expect_kernel=True, expect_ramdisk=True)
def test_xml_and_uri_rescue(self):
instance_data = dict(self.test_instance)
instance_data['ramdisk_id'] = 'ari-deadbeef'
instance_data['kernel_id'] = 'aki-deadbeef'
- self.do_test_xml_and_uri(instance_data,
- expect_kernel=True, expect_ramdisk=True,
- rescue=True)
+ self._check_xml_and_uri(instance_data, expect_kernel=True,
+ expect_ramdisk=True, rescue=True)
- def do_test_xml_and_uri(self, instance,
- expect_ramdisk, expect_kernel,
- rescue=False):
+ def _check_xml_and_uri(self, instance, expect_ramdisk, expect_kernel,
+ rescue=False):
user_context = context.RequestContext(project=self.project,
user=self.user)
instance_ref = db.instance_create(user_context, instance)
- network_ref = self.network.get_network(user_context)
- self.network.set_network_host(context.get_admin_context(),
- network_ref['id'])
+ host = self.network.get_network_host(user_context.elevated())
+ network_ref = db.project_get_network(context.get_admin_context(),
+ self.project.id)
fixed_ip = {'address': self.test_ip,
'network_id': network_ref['id']}
@@ -338,7 +337,7 @@ class NWFilterTestCase(test.TestCase):
self.security_group.id)
instance = db.instance_get(self.context, inst_id)
- d = self.fw.setup_nwfilters_for_instance(instance)
+ self.fw.setup_base_nwfilters()
+ self.fw.setup_nwfilters_for_instance(instance)
_ensure_all_called()
self.teardown_security_group()
- return d
diff --git a/nova/tests/volume_unittest.py b/nova/tests/test_volume.py
index b13455fb0..b13455fb0 100644
--- a/nova/tests/volume_unittest.py
+++ b/nova/tests/test_volume.py
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
new file mode 100644
index 000000000..b5d3ea395
--- /dev/null
+++ b/nova/tests/test_xenapi.py
@@ -0,0 +1,219 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+
+"""
+Test suite for XenAPI
+"""
+
+import stubout
+
+from nova import db
+from nova import context
+from nova import flags
+from nova import test
+from nova import utils
+from nova.auth import manager
+from nova.compute import instance_types
+from nova.compute import power_state
+from nova.virt import xenapi_conn
+from nova.virt.xenapi import fake
+from nova.virt.xenapi import volume_utils
+from nova.tests.db import fakes
+from nova.tests.xenapi import stubs
+
+FLAGS = flags.FLAGS
+
+
+class XenAPIVolumeTestCase(test.TestCase):
+ """
+ Unit tests for Volume operations
+ """
+ def setUp(self):
+ super(XenAPIVolumeTestCase, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+ FLAGS.target_host = '127.0.0.1'
+ FLAGS.xenapi_connection_url = 'test_url'
+ FLAGS.xenapi_connection_password = 'test_pass'
+ fakes.stub_out_db_instance_api(self.stubs)
+ fake.reset()
+ self.values = {'name': 1, 'id': 1,
+ 'project_id': 'fake',
+ 'user_id': 'fake',
+ 'image_id': 1,
+ 'kernel_id': 2,
+ 'ramdisk_id': 3,
+ 'instance_type': 'm1.large',
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
+ }
+
+ def _create_volume(self, size='0'):
+ """Create a volume object."""
+ vol = {}
+ vol['size'] = size
+ vol['user_id'] = 'fake'
+ vol['project_id'] = 'fake'
+ vol['host'] = 'localhost'
+ vol['availability_zone'] = FLAGS.storage_availability_zone
+ vol['status'] = "creating"
+ vol['attach_status'] = "detached"
+ return db.volume_create(context.get_admin_context(), vol)
+
+ def test_create_iscsi_storage(self):
+ """ This shows how to test helper classes' methods """
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
+ session = xenapi_conn.XenAPISession('test_url', 'root', 'test_pass')
+ helper = volume_utils.VolumeHelper
+ helper.XenAPI = session.get_imported_xenapi()
+ vol = self._create_volume()
+ info = helper.parse_volume_info(vol['ec2_id'], '/dev/sdc')
+ label = 'SR-%s' % vol['ec2_id']
+ description = 'Test-SR'
+ sr_ref = helper.create_iscsi_storage(session, info, label, description)
+ srs = fake.get_all('SR')
+ self.assertEqual(sr_ref, srs[0])
+ db.volume_destroy(context.get_admin_context(), vol['id'])
+
+ def test_parse_volume_info_raise_exception(self):
+ """ This shows how to test helper classes' methods """
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
+ session = xenapi_conn.XenAPISession('test_url', 'root', 'test_pass')
+ helper = volume_utils.VolumeHelper
+ helper.XenAPI = session.get_imported_xenapi()
+ vol = self._create_volume()
+ # oops, wrong mount point!
+ self.assertRaises(volume_utils.StorageError,
+ helper.parse_volume_info,
+ vol['ec2_id'],
+ '/dev/sd')
+ db.volume_destroy(context.get_admin_context(), vol['id'])
+
+ def test_attach_volume(self):
+ """ This shows how to test Ops classes' methods """
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVolumeTests)
+ conn = xenapi_conn.get_connection(False)
+ volume = self._create_volume()
+ instance = db.instance_create(self.values)
+ fake.create_vm(instance.name, 'Running')
+ result = conn.attach_volume(instance.name, volume['ec2_id'],
+ '/dev/sdc')
+
+ def check():
+ # check that the VM has a VBD attached to it
+ # Get XenAPI reference for the VM
+ vms = fake.get_all('VM')
+ # Get XenAPI record for VBD
+ vbds = fake.get_all('VBD')
+ vbd = fake.get_record('VBD', vbds[0])
+ vm_ref = vbd['VM']
+ self.assertEqual(vm_ref, vms[0])
+
+ check()
+
+ def test_attach_volume_raise_exception(self):
+ """ This shows how to test when exceptions are raised """
+ stubs.stubout_session(self.stubs,
+ stubs.FakeSessionForVolumeFailedTests)
+ conn = xenapi_conn.get_connection(False)
+ volume = self._create_volume()
+ instance = db.instance_create(self.values)
+ fake.create_vm(instance.name, 'Running')
+ self.assertRaises(Exception,
+ conn.attach_volume,
+ instance.name,
+ volume['ec2_id'],
+ '/dev/sdc')
+
+ def tearDown(self):
+ super(XenAPIVolumeTestCase, self).tearDown()
+ self.stubs.UnsetAll()
+
+
+class XenAPIVMTestCase(test.TestCase):
+ """
+ Unit tests for VM operations
+ """
+ def setUp(self):
+ super(XenAPIVMTestCase, self).setUp()
+ self.manager = manager.AuthManager()
+ self.user = self.manager.create_user('fake', 'fake', 'fake',
+ admin=True)
+ self.project = self.manager.create_project('fake', 'fake', 'fake')
+ self.network = utils.import_object(FLAGS.network_manager)
+ self.stubs = stubout.StubOutForTesting()
+ FLAGS.xenapi_connection_url = 'test_url'
+ FLAGS.xenapi_connection_password = 'test_pass'
+ fake.reset()
+ fakes.stub_out_db_instance_api(self.stubs)
+ fake.create_network('fake', FLAGS.flat_network_bridge)
+
+ def test_list_instances_0(self):
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
+ conn = xenapi_conn.get_connection(False)
+ instances = conn.list_instances()
+ self.assertEquals(instances, [])
+
+ def test_spawn(self):
+ stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
+ values = {'name': 1, 'id': 1,
+ 'project_id': self.project.id,
+ 'user_id': self.user.id,
+ 'image_id': 1,
+ 'kernel_id': 2,
+ 'ramdisk_id': 3,
+ 'instance_type': 'm1.large',
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
+ }
+ conn = xenapi_conn.get_connection(False)
+ instance = db.instance_create(values)
+ conn.spawn(instance)
+
+ def check():
+ instances = conn.list_instances()
+ self.assertEquals(instances, [1])
+
+ # Get Nova record for VM
+ vm_info = conn.get_info(1)
+
+ # Get XenAPI record for VM
+ vms = fake.get_all('VM')
+ vm = fake.get_record('VM', vms[0])
+
+ # Check that m1.large above turned into the right thing.
+ instance_type = instance_types.INSTANCE_TYPES['m1.large']
+ mem_kib = long(instance_type['memory_mb']) << 10
+ mem_bytes = str(mem_kib << 10)
+ vcpus = instance_type['vcpus']
+ self.assertEquals(vm_info['max_mem'], mem_kib)
+ self.assertEquals(vm_info['mem'], mem_kib)
+ self.assertEquals(vm['memory_static_max'], mem_bytes)
+ self.assertEquals(vm['memory_dynamic_max'], mem_bytes)
+ self.assertEquals(vm['memory_dynamic_min'], mem_bytes)
+ self.assertEquals(vm['VCPUs_max'], str(vcpus))
+ self.assertEquals(vm['VCPUs_at_startup'], str(vcpus))
+
+ # Check that the VM is running according to Nova
+ self.assertEquals(vm_info['state'], power_state.RUNNING)
+
+ # Check that the VM is running according to XenAPI.
+ self.assertEquals(vm['power_state'], 'Running')
+
+ check()
+
+ def tearDown(self):
+ super(XenAPIVMTestCase, self).tearDown()
+ self.manager.delete_project(self.project)
+ self.manager.delete_user(self.user)
+ self.stubs.UnsetAll()
diff --git a/nova/tests/xenapi/__init__.py b/nova/tests/xenapi/__init__.py
new file mode 100644
index 000000000..1dd02bdc1
--- /dev/null
+++ b/nova/tests/xenapi/__init__.py
@@ -0,0 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+
+"""
+:mod:`xenapi` -- Stubs for XenAPI
+=================================
+"""
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
new file mode 100644
index 000000000..1dacad6a3
--- /dev/null
+++ b/nova/tests/xenapi/stubs.py
@@ -0,0 +1,94 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+
+"""Stubouts, mocks and fixtures for the test suite"""
+
+from nova.virt import xenapi_conn
+from nova.virt.xenapi import fake
+
+
+def stubout_session(stubs, cls):
+ """ Stubs out two methods from XenAPISession """
+ def fake_import(self):
+ """ Stubs out get_imported_xenapi of XenAPISession """
+ fake_module = 'nova.virt.xenapi.fake'
+ from_list = ['fake']
+ return __import__(fake_module, globals(), locals(), from_list, -1)
+
+ stubs.Set(xenapi_conn.XenAPISession, '_create_session',
+ lambda s, url: cls(url))
+ stubs.Set(xenapi_conn.XenAPISession, 'get_imported_xenapi',
+ fake_import)
+
+
+class FakeSessionForVMTests(fake.SessionBase):
+ """ Stubs out a XenAPISession for VM tests """
+ def __init__(self, uri):
+ super(FakeSessionForVMTests, self).__init__(uri)
+
+ def network_get_all_records_where(self, _1, _2):
+ return self.xenapi.network.get_all_records()
+
+ def host_call_plugin(self, _1, _2, _3, _4, _5):
+ return ''
+
+ def VM_start(self, _1, ref, _2, _3):
+ vm = fake.get_record('VM', ref)
+ if vm['power_state'] != 'Halted':
+ raise fake.Failure(['VM_BAD_POWER_STATE', ref, 'Halted',
+ vm['power_state']])
+ vm['power_state'] = 'Running'
+ vm['is_a_template'] = False
+ vm['is_control_domain'] = False
+
+
+class FakeSessionForVolumeTests(fake.SessionBase):
+ """ Stubs out a XenAPISession for Volume tests """
+ def __init__(self, uri):
+ super(FakeSessionForVolumeTests, self).__init__(uri)
+
+ def VBD_plug(self, _1, ref):
+ rec = fake.get_record('VBD', ref)
+ rec['currently-attached'] = True
+
+ def VDI_introduce(self, _1, uuid, _2, _3, _4, _5,
+ _6, _7, _8, _9, _10, _11):
+ valid_vdi = False
+ refs = fake.get_all('VDI')
+ for ref in refs:
+ rec = fake.get_record('VDI', ref)
+ if rec['uuid'] == uuid:
+ valid_vdi = True
+ if not valid_vdi:
+ raise fake.Failure([['INVALID_VDI', 'session', self._session]])
+
+
+class FakeSessionForVolumeFailedTests(FakeSessionForVolumeTests):
+ """ Stubs out a XenAPISession for Volume tests: it injects failures """
+ def __init__(self, uri):
+ super(FakeSessionForVolumeFailedTests, self).__init__(uri)
+
+ def VDI_introduce(self, _1, uuid, _2, _3, _4, _5,
+ _6, _7, _8, _9, _10, _11):
+ # This is for testing failure
+ raise fake.Failure([['INVALID_VDI', 'session', self._session]])
+
+ def PBD_unplug(self, _1, ref):
+ rec = fake.get_record('PBD', ref)
+ rec['currently-attached'] = False
+
+ def SR_forget(self, _1, ref):
+ pass
diff --git a/nova/utils.py b/nova/utils.py
index 30fd12db0..b9045a50c 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -29,6 +29,7 @@ import subprocess
import socket
import struct
import sys
+import time
from xml.sax import saxutils
from eventlet import event
@@ -205,13 +206,51 @@ def get_my_ip():
return "127.0.0.1"
+def utcnow():
+ """Overridable version of datetime.datetime.utcnow."""
+ if utcnow.override_time:
+ return utcnow.override_time
+ return datetime.datetime.utcnow()
+
+
+utcnow.override_time = None
+
+
+def utcnow_ts():
+ """Timestamp version of our utcnow function."""
+ return time.mktime(utcnow().timetuple())
+
+
+def set_time_override(override_time=datetime.datetime.utcnow()):
+ """Override utils.utcnow to return a constant time."""
+ utcnow.override_time = override_time
+
+
+def advance_time_delta(timedelta):
+ """Advance overriden time using a datetime.timedelta."""
+ assert(not utcnow.override_time is None)
+ utcnow.override_time += timedelta
+
+
+def advance_time_seconds(seconds):
+ """Advance overriden time by seconds."""
+ advance_time_delta(datetime.timedelta(0, seconds))
+
+
+def clear_time_override():
+ """Remove the overridden time."""
+ utcnow.override_time = None
+
+
def isotime(at=None):
+ """Returns iso formatted utcnow."""
if not at:
- at = datetime.datetime.utcnow()
+ at = utcnow()
return at.strftime(TIME_FORMAT)
def parse_isotime(timestr):
+ """Turn an iso formatted time back into a datetime"""
return datetime.datetime.strptime(timestr, TIME_FORMAT)
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 73e273edd..238acf798 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -76,6 +76,12 @@ class FakeConnection(object):
cls._instance = cls()
return cls._instance
+ def init_host(self):
+ """
+ Initialize anything that is necessary for the driver to function
+ """
+ return
+
def list_instances(self):
"""
Return the names of all the instances known to the virtualization
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index 13d087330..3fb2243da 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -66,6 +66,9 @@
<filterref filter="nova-instance-${name}">
<parameter name="IP" value="${ip_address}" />
<parameter name="DHCPSERVER" value="${dhcp_server}" />
+#if $getVar('extra_params', False)
+ ${extra_params}
+#end if
</filterref>
</interface>
<serial type="file">
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index eb5a6889b..651b2af93 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -40,6 +40,7 @@ import logging
import os
import shutil
+from eventlet import greenthread
from eventlet import event
from eventlet import tpool
@@ -96,6 +97,11 @@ def get_connection(read_only):
return LibvirtConnection(read_only)
+def _get_net_and_mask(cidr):
+ net = IPy.IP(cidr)
+ return str(net.net()), str(net.netmask())
+
+
class LibvirtConnection(object):
def __init__(self, read_only):
@@ -105,6 +111,9 @@ class LibvirtConnection(object):
self._wrapped_conn = None
self.read_only = read_only
+ def init_host(self):
+ NWFilterFirewall(self._conn).setup_base_nwfilters()
+
@property
def _conn(self):
if not self._wrapped_conn or not self._test_connection():
@@ -183,7 +192,8 @@ class LibvirtConnection(object):
# everything has been vetted a bit
def _wait_for_timer():
timer_done.wait()
- self._cleanup(instance)
+ if cleanup:
+ self._cleanup(instance)
done.send()
greenthread.spawn(_wait_for_timer)
@@ -490,8 +500,8 @@ class LibvirtConnection(object):
# TODO(termie): cache?
logging.debug(_('instance %s: starting toXML method'),
instance['name'])
- network = db.project_get_network(context.get_admin_context(),
- instance['project_id'])
+ network = db.network_get_by_instance(context.get_admin_context(),
+ instance['id'])
# FIXME(vish): stick this in db
instance_type = instance['instance_type']
instance_type = instance_types.INSTANCE_TYPES[instance_type]
@@ -499,6 +509,15 @@ class LibvirtConnection(object):
instance['id'])
# Assume that the gateway also acts as the dhcp server.
dhcp_server = network['gateway']
+
+ if FLAGS.allow_project_net_traffic:
+ net, mask = _get_net_and_mask(network['cidr'])
+ extra_params = ("<parameter name=\"PROJNET\" value=\"%s\" />\n"
+ "<parameter name=\"PROJMASK\" value=\"%s\" />\n"
+ ) % (net, mask)
+ else:
+ extra_params = "\n"
+
xml_info = {'type': FLAGS.libvirt_type,
'name': instance['name'],
'basepath': os.path.join(FLAGS.instances_path,
@@ -509,6 +528,7 @@ class LibvirtConnection(object):
'mac_address': instance['mac_address'],
'ip_address': ip_address,
'dhcp_server': dhcp_server,
+ 'extra_params': extra_params,
'rescue': rescue}
if not rescue:
if instance['kernel_id']:
@@ -716,6 +736,14 @@ class NWFilterFirewall(object):
</rule>
</filter>'''
+ nova_vpn_filter = '''<filter name='nova-vpn' chain='root'>
+ <uuid>2086015e-cf03-11df-8c5d-080027c27973</uuid>
+ <filterref filter='allow-dhcp-server'/>
+ <filterref filter='nova-allow-dhcp-server'/>
+ <filterref filter='nova-base-ipv4'/>
+ <filterref filter='nova-base-ipv6'/>
+ </filter>'''
+
def nova_base_ipv4_filter(self):
retval = "<filter name='nova-base-ipv4' chain='ipv4'>"
for protocol in ['tcp', 'udp', 'icmp']:
@@ -740,12 +768,12 @@ class NWFilterFirewall(object):
retval += '</filter>'
return retval
- def nova_project_filter(self, project, net, mask):
- retval = "<filter name='nova-project-%s' chain='ipv4'>" % project
+ def nova_project_filter(self):
+ retval = "<filter name='nova-project' chain='ipv4'>"
for protocol in ['tcp', 'udp', 'icmp']:
retval += """<rule action='accept' direction='in' priority='200'>
- <%s srcipaddr='%s' srcipmask='%s' />
- </rule>""" % (protocol, net, mask)
+ <%s srcipaddr='$PROJNET' srcipmask='$PROJMASK' />
+ </rule>""" % protocol
retval += '</filter>'
return retval
@@ -756,10 +784,14 @@ class NWFilterFirewall(object):
# execute in a native thread and block current greenthread until done
tpool.execute(self._conn.nwfilterDefineXML, xml)
- @staticmethod
- def _get_net_and_mask(cidr):
- net = IPy.IP(cidr)
- return str(net.net()), str(net.netmask())
+ def setup_base_nwfilters(self):
+ self._define_filter(self.nova_base_ipv4_filter)
+ self._define_filter(self.nova_base_ipv6_filter)
+ self._define_filter(self.nova_dhcp_filter)
+ self._define_filter(self.nova_base_filter)
+ self._define_filter(self.nova_vpn_filter)
+ if FLAGS.allow_project_net_traffic:
+ self._define_filter(self.nova_project_filter)
def setup_nwfilters_for_instance(self, instance):
"""
@@ -768,31 +800,22 @@ class NWFilterFirewall(object):
the base filter are all in place.
"""
- self._define_filter(self.nova_base_ipv4_filter)
- self._define_filter(self.nova_base_ipv6_filter)
- self._define_filter(self.nova_dhcp_filter)
- self._define_filter(self.nova_base_filter)
+ nwfilter_xml = ("<filter name='nova-instance-%s' chain='root'>\n"
+ ) % instance['name']
- nwfilter_xml = "<filter name='nova-instance-%s' chain='root'>\n" \
- " <filterref filter='nova-base' />\n" % \
- instance['name']
+ if instance['image_id'] == FLAGS.vpn_image_id:
+ nwfilter_xml += " <filterref filter='nova-vpn' />\n"
+ else:
+ nwfilter_xml += " <filterref filter='nova-base' />\n"
if FLAGS.allow_project_net_traffic:
- network_ref = db.project_get_network(context.get_admin_context(),
- instance['project_id'])
- net, mask = self._get_net_and_mask(network_ref['cidr'])
- project_filter = self.nova_project_filter(instance['project_id'],
- net, mask)
- self._define_filter(project_filter)
-
- nwfilter_xml += " <filterref filter='nova-project-%s' />\n" % \
- instance['project_id']
+ nwfilter_xml += " <filterref filter='nova-project' />\n"
for security_group in instance.security_groups:
self.ensure_security_group_filter(security_group['id'])
- nwfilter_xml += " <filterref filter='nova-secgroup-%d' />\n" % \
- security_group['id']
+ nwfilter_xml += (" <filterref filter='nova-secgroup-%d' />\n"
+ ) % security_group['id']
nwfilter_xml += "</filter>"
self._define_filter(nwfilter_xml)
@@ -808,7 +831,7 @@ class NWFilterFirewall(object):
for rule in security_group.rules:
rule_xml += "<rule action='accept' direction='in' priority='300'>"
if rule.cidr:
- net, mask = self._get_net_and_mask(rule.cidr)
+ net, mask = _get_net_and_mask(rule.cidr)
rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
(rule.protocol, net, mask)
if rule.protocol in ['tcp', 'udp']:
diff --git a/nova/virt/xenapi/__init__.py b/nova/virt/xenapi/__init__.py
index 3d598c463..c75162f08 100644
--- a/nova/virt/xenapi/__init__.py
+++ b/nova/virt/xenapi/__init__.py
@@ -13,3 +13,18 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+
+"""
+:mod:`xenapi` -- Nova support for XenServer and XCP through XenAPI
+==================================================================
+"""
+
+
+class HelperBase(object):
+ """
+ The base for helper classes. This adds the XenAPI class attribute
+ """
+ XenAPI = None
+
+ def __init__(self):
+ return
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
new file mode 100644
index 000000000..7a6c9ee71
--- /dev/null
+++ b/nova/virt/xenapi/fake.py
@@ -0,0 +1,388 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+#
+#============================================================================
+#
+# Parts of this file are based upon xmlrpclib.py, the XML-RPC client
+# interface included in the Python distribution.
+#
+# Copyright (c) 1999-2002 by Secret Labs AB
+# Copyright (c) 1999-2002 by Fredrik Lundh
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of
+# Secret Labs AB or the author not be used in advertising or publicity
+# pertaining to distribution of the software without specific, written
+# prior permission.
+#
+# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
+# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
+# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
+# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
+# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+# OF THIS SOFTWARE.
+# --------------------------------------------------------------------
+
+
+"""
+A fake XenAPI SDK.
+"""
+
+
+import datetime
+import logging
+import uuid
+
+from nova import exception
+
+
+_CLASSES = ['host', 'network', 'session', 'SR', 'VBD',\
+ 'PBD', 'VDI', 'VIF', 'VM', 'task']
+
+_db_content = {}
+
+
+def reset():
+ for c in _CLASSES:
+ _db_content[c] = {}
+ create_host('fake')
+
+
+def create_host(name_label):
+ return _create_object('host', {
+ 'name_label': name_label,
+ })
+
+
+def create_network(name_label, bridge):
+ return _create_object('network', {
+ 'name_label': name_label,
+ 'bridge': bridge,
+ })
+
+
+def create_vm(name_label, status,
+ is_a_template=False, is_control_domain=False):
+ return _create_object('VM', {
+ 'name_label': name_label,
+ 'power-state': status,
+ 'is_a_template': is_a_template,
+ 'is_control_domain': is_control_domain,
+ })
+
+
+def create_vdi(name_label, read_only, sr_ref, sharable):
+ return _create_object('VDI', {
+ 'name_label': name_label,
+ 'read_only': read_only,
+ 'SR': sr_ref,
+ 'type': '',
+ 'name_description': '',
+ 'sharable': sharable,
+ 'other_config': {},
+ 'location': '',
+ 'xenstore_data': '',
+ 'sm_config': {},
+ 'VBDs': {},
+ })
+
+
+def create_pbd(config, sr_ref, attached):
+ return _create_object('PBD', {
+ 'device-config': config,
+ 'SR': sr_ref,
+ 'currently-attached': attached,
+ })
+
+
+def create_task(name_label):
+ return _create_object('task', {
+ 'name_label': name_label,
+ 'status': 'pending',
+ })
+
+
+def _create_object(table, obj):
+ ref = str(uuid.uuid4())
+ obj['uuid'] = str(uuid.uuid4())
+ _db_content[table][ref] = obj
+ return ref
+
+
+def _create_sr(table, obj):
+ sr_type = obj[6]
+ # Forces fake to support iscsi only
+ if sr_type != 'iscsi':
+ raise Failure(['SR_UNKNOWN_DRIVER', sr_type])
+ sr_ref = _create_object(table, obj[2])
+ vdi_ref = create_vdi('', False, sr_ref, False)
+ pbd_ref = create_pbd('', sr_ref, True)
+ _db_content['SR'][sr_ref]['VDIs'] = [vdi_ref]
+ _db_content['SR'][sr_ref]['PBDs'] = [pbd_ref]
+ _db_content['VDI'][vdi_ref]['SR'] = sr_ref
+ _db_content['PBD'][pbd_ref]['SR'] = sr_ref
+ return sr_ref
+
+
+def get_all(table):
+ return _db_content[table].keys()
+
+
+def get_all_records(table):
+ return _db_content[table]
+
+
+def get_record(table, ref):
+ if ref in _db_content[table]:
+ return _db_content[table].get(ref)
+ else:
+ raise Failure(['HANDLE_INVALID', table, ref])
+
+
+def check_for_session_leaks():
+ if len(_db_content['session']) > 0:
+ raise exception.Error('Sessions have leaked: %s' %
+ _db_content['session'])
+
+
+class Failure(Exception):
+ def __init__(self, details):
+ self.details = details
+
+ def __str__(self):
+ try:
+ return str(self.details)
+ except Exception, exc:
+ return "XenAPI Fake Failure: %s" % str(self.details)
+
+ def _details_map(self):
+ return dict([(str(i), self.details[i])
+ for i in range(len(self.details))])
+
+
+class SessionBase(object):
+ """
+ Base class for Fake Sessions
+ """
+
+ def __init__(self, uri):
+ self._session = None
+
+ def xenapi_request(self, methodname, params):
+ if methodname.startswith('login'):
+ self._login(methodname, params)
+ return None
+ elif methodname == 'logout' or methodname == 'session.logout':
+ self._logout()
+ return None
+ else:
+ full_params = (self._session,) + params
+ meth = getattr(self, methodname, None)
+ if meth is None:
+ logging.warn('Raising NotImplemented')
+ raise NotImplementedError(
+ 'xenapi.fake does not have an implementation for %s' %
+ methodname)
+ return meth(*full_params)
+
+ def _login(self, method, params):
+ self._session = str(uuid.uuid4())
+ _db_content['session'][self._session] = {
+ 'uuid': str(uuid.uuid4()),
+ 'this_host': _db_content['host'].keys()[0],
+ }
+
+ def _logout(self):
+ s = self._session
+ self._session = None
+ if s not in _db_content['session']:
+ raise exception.Error(
+ "Logging out a session that is invalid or already logged "
+ "out: %s" % s)
+ del _db_content['session'][s]
+
+ def __getattr__(self, name):
+ if name == 'handle':
+ return self._session
+ elif name == 'xenapi':
+ return _Dispatcher(self.xenapi_request, None)
+ elif name.startswith('login') or name.startswith('slave_local'):
+ return lambda *params: self._login(name, params)
+ elif name.startswith('Async'):
+ return lambda *params: self._async(name, params)
+ elif '.' in name:
+ impl = getattr(self, name.replace('.', '_'))
+ if impl is not None:
+ def callit(*params):
+ logging.warn('Calling %s %s', name, impl)
+ self._check_session(params)
+ return impl(*params)
+ return callit
+ if self._is_gettersetter(name, True):
+ logging.warn('Calling getter %s', name)
+ return lambda *params: self._getter(name, params)
+ elif self._is_create(name):
+ return lambda *params: self._create(name, params)
+ else:
+ return None
+
+ def _is_gettersetter(self, name, getter):
+ bits = name.split('.')
+ return (len(bits) == 2 and
+ bits[0] in _CLASSES and
+ bits[1].startswith(getter and 'get_' or 'set_'))
+
+ def _is_create(self, name):
+ bits = name.split('.')
+ return (len(bits) == 2 and
+ bits[0] in _CLASSES and
+ bits[1] == 'create')
+
+ def _getter(self, name, params):
+ self._check_session(params)
+ (cls, func) = name.split('.')
+
+ if func == 'get_all':
+ self._check_arg_count(params, 1)
+ return get_all(cls)
+
+ if func == 'get_all_records':
+ self._check_arg_count(params, 1)
+ return get_all_records(cls)
+
+ if func == 'get_record':
+ self._check_arg_count(params, 2)
+ return get_record(cls, params[1])
+
+ if (func == 'get_by_name_label' or
+ func == 'get_by_uuid'):
+ self._check_arg_count(params, 2)
+ return self._get_by_field(
+ _db_content[cls], func[len('get_by_'):], params[1])
+
+ if len(params) == 2:
+ field = func[len('get_'):]
+ ref = params[1]
+
+ if (ref in _db_content[cls] and
+ field in _db_content[cls][ref]):
+ return _db_content[cls][ref][field]
+
+ logging.error('Raising NotImplemented')
+ raise NotImplementedError(
+ 'xenapi.fake does not have an implementation for %s or it has '
+ 'been called with the wrong number of arguments' % name)
+
+ def _setter(self, name, params):
+ self._check_session(params)
+ (cls, func) = name.split('.')
+
+ if len(params) == 3:
+ field = func[len('set_'):]
+ ref = params[1]
+ val = params[2]
+
+ if (ref in _db_content[cls] and
+ field in _db_content[cls][ref]):
+ _db_content[cls][ref][field] = val
+
+ logging.warn('Raising NotImplemented')
+ raise NotImplementedError(
+ 'xenapi.fake does not have an implementation for %s or it has '
+ 'been called with the wrong number of arguments or the database '
+ 'is missing that field' % name)
+
+ def _create(self, name, params):
+ self._check_session(params)
+ is_sr_create = name == 'SR.create'
+ # Storage Repositories have a different API
+ expected = is_sr_create and 10 or 2
+ self._check_arg_count(params, expected)
+ (cls, _) = name.split('.')
+ ref = is_sr_create and \
+ _create_sr(cls, params) or _create_object(cls, params[1])
+ obj = get_record(cls, ref)
+
+ # Add RO fields
+ if cls == 'VM':
+ obj['power_state'] = 'Halted'
+
+ return ref
+
+ def _async(self, name, params):
+ task_ref = create_task(name)
+ task = _db_content['task'][task_ref]
+ func = name[len('Async.'):]
+ try:
+ task['result'] = self.xenapi_request(func, params[1:])
+ task['status'] = 'success'
+ except Failure, exc:
+ task['error_info'] = exc.details
+ task['status'] = 'failed'
+ task['finished'] = datetime.datetime.now()
+ return task_ref
+
+ def _check_session(self, params):
+ if (self._session is None or
+ self._session not in _db_content['session']):
+ raise Failure(['HANDLE_INVALID', 'session', self._session])
+ if len(params) == 0 or params[0] != self._session:
+ logging.warn('Raising NotImplemented')
+ raise NotImplementedError('Call to XenAPI without using .xenapi')
+
+ def _check_arg_count(self, params, expected):
+ actual = len(params)
+ if actual != expected:
+ raise Failure(['MESSAGE_PARAMETER_COUNT_MISMATCH',
+ expected, actual])
+
+ def _get_by_field(self, recs, k, v):
+ result = []
+ for ref, rec in recs.iteritems():
+ if rec.get(k) == v:
+ result.append(ref)
+ return result
+
+
+# Based upon _Method from xmlrpclib.
+class _Dispatcher:
+ def __init__(self, send, name):
+ self.__send = send
+ self.__name = name
+
+ def __repr__(self):
+ if self.__name:
+ return '<xenapi.fake._Dispatcher for %s>' % self.__name
+ else:
+ return '<xenapi.fake._Dispatcher>'
+
+ def __getattr__(self, name):
+ if self.__name is None:
+ return _Dispatcher(self.__send, name)
+ else:
+ return _Dispatcher(self.__send, "%s.%s" % (self.__name, name))
+
+ def __call__(self, *args):
+ return self.__send(self.__name, args)
diff --git a/nova/virt/xenapi/network_utils.py b/nova/virt/xenapi/network_utils.py
index ce2c68ce0..c0406d8f0 100644
--- a/nova/virt/xenapi/network_utils.py
+++ b/nova/virt/xenapi/network_utils.py
@@ -21,22 +21,23 @@ their lookup functions.
"""
-class NetworkHelper():
+from nova.virt.xenapi import HelperBase
+
+
+class NetworkHelper(HelperBase):
"""
The class that wraps the helper methods together.
"""
- def __init__(self):
- return
-
@classmethod
def find_network_with_bridge(cls, session, bridge):
- """ Return the network on which the bridge is attached, if found."""
+ """Return the network on which the bridge is attached, if found."""
expr = 'field "bridge" = "%s"' % bridge
networks = session.call_xenapi('network.get_all_records_where', expr)
if len(networks) == 1:
return networks.keys()[0]
elif len(networks) > 1:
- raise Exception('Found non-unique network for bridge %s' % bridge)
+ raise Exception(_('Found non-unique network'
+ ' for bridge %s') % bridge)
else:
- raise Exception('Found no network for bridge %s' % bridge)
+ raise Exception(_('Found no network for bridge %s') % bridge)
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index badaaedc1..89e02c917 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -23,12 +23,14 @@ import logging
import urllib
from xml.dom import minidom
+from nova import exception
from nova import flags
-from nova import utils
from nova.auth.manager import AuthManager
from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import images
+from nova.virt.xenapi import HelperBase
+from nova.virt.xenapi.volume_utils import StorageError
FLAGS = flags.FLAGS
@@ -40,33 +42,16 @@ XENAPI_POWER_STATE = {
'Suspended': power_state.SHUTDOWN, # FIXME
'Crashed': power_state.CRASHED}
-XenAPI = None
-
-class VMHelper():
+class VMHelper(HelperBase):
"""
The class that wraps the helper methods together.
"""
- def __init__(self):
- return
-
- @classmethod
- def late_import(cls):
- """
- Load the XenAPI module in for helper class, if required.
- This is to avoid to install the XenAPI library when other
- hypervisors are used
- """
- global XenAPI
- if XenAPI is None:
- XenAPI = __import__('XenAPI')
-
@classmethod
def create_vm(cls, session, instance, kernel, ramdisk):
"""Create a VM record. Returns a Deferred that gives the new
VM reference."""
-
instance_type = instance_types.INSTANCE_TYPES[instance.instance_type]
mem = str(long(instance_type['memory_mb']) * 1024 * 1024)
vcpus = str(instance_type['vcpus'])
@@ -99,16 +84,15 @@ class VMHelper():
'user_version': '0',
'other_config': {},
}
- logging.debug('Created VM %s...', instance.name)
+ logging.debug(_('Created VM %s...'), instance.name)
vm_ref = session.call_xenapi('VM.create', rec)
- logging.debug('Created VM %s as %s.', instance.name, vm_ref)
+ logging.debug(_('Created VM %s as %s.'), instance.name, vm_ref)
return vm_ref
@classmethod
def create_vbd(cls, session, vm_ref, vdi_ref, userdevice, bootable):
"""Create a VBD record. Returns a Deferred that gives the new
VBD reference."""
-
vbd_rec = {}
vbd_rec['VM'] = vm_ref
vbd_rec['VDI'] = vdi_ref
@@ -122,17 +106,53 @@ class VMHelper():
vbd_rec['qos_algorithm_type'] = ''
vbd_rec['qos_algorithm_params'] = {}
vbd_rec['qos_supported_algorithms'] = []
- logging.debug('Creating VBD for VM %s, VDI %s ... ', vm_ref, vdi_ref)
+ logging.debug(_('Creating VBD for VM %s, VDI %s ... '),
+ vm_ref, vdi_ref)
vbd_ref = session.call_xenapi('VBD.create', vbd_rec)
- logging.debug('Created VBD %s for VM %s, VDI %s.', vbd_ref, vm_ref,
+ logging.debug(_('Created VBD %s for VM %s, VDI %s.'), vbd_ref, vm_ref,
vdi_ref)
return vbd_ref
@classmethod
+ def find_vbd_by_number(cls, session, vm_ref, number):
+ """Get the VBD reference from the device number"""
+ vbds = session.get_xenapi().VM.get_VBDs(vm_ref)
+ if vbds:
+ for vbd in vbds:
+ try:
+ vbd_rec = session.get_xenapi().VBD.get_record(vbd)
+ if vbd_rec['userdevice'] == str(number):
+ return vbd
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('VBD not found in instance %s') % vm_ref)
+
+ @classmethod
+ def unplug_vbd(cls, session, vbd_ref):
+ """Unplug VBD from VM"""
+ try:
+ vbd_ref = session.call_xenapi('VBD.unplug', vbd_ref)
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ if exc.details[0] != 'DEVICE_ALREADY_DETACHED':
+ raise StorageError(_('Unable to unplug VBD %s') % vbd_ref)
+
+ @classmethod
+ def destroy_vbd(cls, session, vbd_ref):
+ """Destroy VBD from host database"""
+ try:
+ task = session.call_xenapi('Async.VBD.destroy', vbd_ref)
+ #FIXME(armando): find a solution to missing instance_id
+ #with Josh Kearney
+ session.wait_for_task(0, task)
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to destroy VBD %s') % vbd_ref)
+
+ @classmethod
def create_vif(cls, session, vm_ref, network_ref, mac_address):
"""Create a VIF record. Returns a Deferred that gives the new
VIF reference."""
-
vif_rec = {}
vif_rec['device'] = '0'
vif_rec['network'] = network_ref
@@ -142,10 +162,10 @@ class VMHelper():
vif_rec['other_config'] = {}
vif_rec['qos_algorithm_type'] = ''
vif_rec['qos_algorithm_params'] = {}
- logging.debug('Creating VIF for VM %s, network %s ... ', vm_ref,
+ logging.debug(_('Creating VIF for VM %s, network %s.'), vm_ref,
network_ref)
vif_ref = session.call_xenapi('VIF.create', vif_rec)
- logging.debug('Created VIF %s for VM %s, network %s.', vif_ref,
+ logging.debug(_('Created VIF %s for VM %s, network %s.'), vif_ref,
vm_ref, network_ref)
return vif_ref
@@ -158,7 +178,7 @@ class VMHelper():
url = images.image_url(image)
access = AuthManager().get_access_key(user, project)
- logging.debug("Asking xapi to fetch %s as %s", url, access)
+ logging.debug(_("Asking xapi to fetch %s as %s"), url, access)
fn = use_sr and 'get_vdi' or 'get_kernel'
args = {}
args['src_url'] = url
@@ -167,34 +187,26 @@ class VMHelper():
if use_sr:
args['add_partition'] = 'true'
task = session.async_call_plugin('objectstore', fn, args)
- uuid = session.wait_for_task(task)
+ #FIXME(armando): find a solution to missing instance_id
+ #with Josh Kearney
+ uuid = session.wait_for_task(0, task)
return uuid
@classmethod
def lookup(cls, session, i):
- """ Look the instance i up, and returns it if available """
- return VMHelper.lookup_blocking(session, i)
-
- @classmethod
- def lookup_blocking(cls, session, i):
- """ Synchronous lookup """
+ """Look the instance i up, and returns it if available"""
vms = session.get_xenapi().VM.get_by_name_label(i)
n = len(vms)
if n == 0:
return None
elif n > 1:
- raise Exception('duplicate name found: %s' % i)
+ raise exception.Duplicate(_('duplicate name found: %s') % i)
else:
return vms[0]
@classmethod
def lookup_vm_vdis(cls, session, vm):
- """ Look for the VDIs that are attached to the VM """
- return VMHelper.lookup_vm_vdis_blocking(session, vm)
-
- @classmethod
- def lookup_vm_vdis_blocking(cls, session, vm):
- """ Synchronous lookup_vm_vdis """
+ """Look for the VDIs that are attached to the VM"""
# Firstly we get the VBDs, then the VDIs.
# TODO(Armando): do we leave the read-only devices?
vbds = session.get_xenapi().VM.get_VBDs(vm)
@@ -205,8 +217,9 @@ class VMHelper():
vdi = session.get_xenapi().VBD.get_VDI(vbd)
# Test valid VDI
record = session.get_xenapi().VDI.get_record(vdi)
- logging.debug('VDI %s is still available', record['uuid'])
- except XenAPI.Failure, exc:
+ logging.debug(_('VDI %s is still available'),
+ record['uuid'])
+ except cls.XenAPI.Failure, exc:
logging.warn(exc)
else:
vdis.append(vdi)
@@ -217,6 +230,7 @@ class VMHelper():
@classmethod
def compile_info(cls, record):
+ """Fill record with VM status information"""
return {'state': XENAPI_POWER_STATE[record['power_state']],
'max_mem': long(record['memory_static_max']) >> 10,
'mem': long(record['memory_dynamic_max']) >> 10,
@@ -240,7 +254,7 @@ class VMHelper():
# Name and Value
diags[ref[0].firstChild.data] = ref[6].firstChild.data
return diags
- except XenAPI.Failure as e:
+ except cls.XenAPI.Failure as e:
return {"Unable to retrieve diagnostics": e}
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 3b00ce8bf..39bb0f1e8 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -22,13 +22,14 @@ import logging
from nova import db
from nova import context
+from nova import exception
+from nova import utils
from nova.auth.manager import AuthManager
+from nova.compute import power_state
from nova.virt.xenapi.network_utils import NetworkHelper
from nova.virt.xenapi.vm_utils import VMHelper
-XenAPI = None
-
class VMOps(object):
"""
@@ -36,12 +37,9 @@ class VMOps(object):
"""
def __init__(self, session):
- global XenAPI
- if XenAPI is None:
- XenAPI = __import__('XenAPI')
+ self.XenAPI = session.get_imported_xenapi()
self._session = session
- # Load XenAPI module in the helper class
- VMHelper.late_import()
+ VMHelper.XenAPI = self.XenAPI
def list_instances(self):
"""List VM instances"""
@@ -56,11 +54,11 @@ class VMOps(object):
"""Create VM instance"""
vm = VMHelper.lookup(self._session, instance.name)
if vm is not None:
- raise Exception('Attempted to create non-unique name %s' %
- instance.name)
+ raise exception.Duplicate(_('Attempted to create'
+ ' non-unique name %s') % instance.name)
- bridge = db.project_get_network(context.get_admin_context(),
- instance.project_id).bridge
+ bridge = db.network_get_by_instance(context.get_admin_context(),
+ instance['id'])['bridge']
network_ref = \
NetworkHelper.find_network_with_bridge(self._session, bridge)
@@ -79,17 +77,41 @@ class VMOps(object):
if network_ref:
VMHelper.create_vif(self._session, vm_ref,
network_ref, instance.mac_address)
- logging.debug('Starting VM %s...', vm_ref)
+ logging.debug(_('Starting VM %s...'), vm_ref)
self._session.call_xenapi('VM.start', vm_ref, False, False)
- logging.info('Spawning VM %s created %s.', instance.name,
+ logging.info(_('Spawning VM %s created %s.'), instance.name,
vm_ref)
+ # NOTE(armando): Do we really need to do this in virt?
+ timer = utils.LoopingCall(f=None)
+
+ def _wait_for_boot():
+ try:
+ state = self.get_info(instance['name'])['state']
+ db.instance_set_state(context.get_admin_context(),
+ instance['id'], state)
+ if state == power_state.RUNNING:
+ logging.debug(_('Instance %s: booted'), instance['name'])
+ timer.stop()
+ except Exception, exc:
+ logging.warn(exc)
+ logging.exception(_('instance %s: failed to boot'),
+ instance['name'])
+ db.instance_set_state(context.get_admin_context(),
+ instance['id'],
+ power_state.SHUTDOWN)
+ timer.stop()
+
+ timer.f = _wait_for_boot
+ return timer.start(interval=0.5, now=True)
+
def reboot(self, instance):
"""Reboot VM instance"""
instance_name = instance.name
vm = VMHelper.lookup(self._session, instance_name)
if vm is None:
- raise Exception('instance not present %s' % instance_name)
+ raise exception.NotFound(_('instance not'
+ ' found %s') % instance_name)
task = self._session.call_xenapi('Async.VM.clean_reboot', vm)
self._session.wait_for_task(instance.id, task)
@@ -116,6 +138,7 @@ class VMOps(object):
self._session.wait_for_task(instance.id, task)
except XenAPI.Failure, exc:
logging.warn(exc)
+ # VM Destroy
try:
task = self._session.call_xenapi('Async.VM.destroy', vm)
self._session.wait_for_task(instance.id, task)
@@ -135,7 +158,8 @@ class VMOps(object):
instance_name = instance.name
vm = VMHelper.lookup(self._session, instance_name)
if vm is None:
- raise Exception('instance not present %s' % instance_name)
+ raise exception.NotFound(_('Instance not'
+ ' found %s') % instance_name)
task = self._session.call_xenapi('Async.VM.pause', vm)
self._wait_with_callback(instance.id, task, callback)
@@ -144,15 +168,17 @@ class VMOps(object):
instance_name = instance.name
vm = VMHelper.lookup(self._session, instance_name)
if vm is None:
- raise Exception('instance not present %s' % instance_name)
+ raise exception.NotFound(_('Instance not'
+ ' found %s') % instance_name)
task = self._session.call_xenapi('Async.VM.unpause', vm)
self._wait_with_callback(instance.id, task, callback)
def get_info(self, instance_id):
"""Return data about VM instance"""
- vm = VMHelper.lookup_blocking(self._session, instance_id)
+ vm = VMHelper.lookup(self._session, instance_id)
if vm is None:
- raise Exception('instance not present %s' % instance_id)
+ raise exception.NotFound(_('Instance not'
+ ' found %s') % instance_id)
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_info(rec)
@@ -160,7 +186,7 @@ class VMOps(object):
"""Return data about VM diagnostics"""
vm = VMHelper.lookup(self._session, instance_id)
if vm is None:
- raise Exception("instance not present %s" % instance_id)
+ raise exception.NotFound(_("Instance not found %s") % instance_id)
rec = self._session.get_xenapi().VM.get_record(vm)
return VMHelper.compile_diagnostics(self._session, rec)
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
new file mode 100644
index 000000000..a0c0a67d4
--- /dev/null
+++ b/nova/virt/xenapi/volume_utils.py
@@ -0,0 +1,268 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+# 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.
+
+"""
+Helper methods for operations related to the management of volumes,
+and storage repositories
+"""
+
+import re
+import string
+import logging
+
+from nova import db
+from nova import context
+from nova import exception
+from nova import flags
+from nova import utils
+from nova.virt.xenapi import HelperBase
+
+FLAGS = flags.FLAGS
+
+
+class StorageError(Exception):
+ """To raise errors related to SR, VDI, PBD, and VBD commands"""
+
+ def __init__(self, message=None):
+ super(StorageError, self).__init__(message)
+
+
+class VolumeHelper(HelperBase):
+ """
+ The class that wraps the helper methods together.
+ """
+
+ @classmethod
+ def create_iscsi_storage(cls, session, info, label, description):
+ """
+ Create an iSCSI storage repository that will be used to mount
+ the volume for the specified instance
+ """
+ sr_ref = session.get_xenapi().SR.get_by_name_label(label)
+ if len(sr_ref) == 0:
+ logging.debug('Introducing %s...', label)
+ record = {}
+ if 'chapuser' in info and 'chappassword' in info:
+ record = {'target': info['targetHost'],
+ 'port': info['targetPort'],
+ 'targetIQN': info['targetIQN'],
+ 'chapuser': info['chapuser'],
+ 'chappassword': info['chappassword']
+ }
+ else:
+ record = {'target': info['targetHost'],
+ 'port': info['targetPort'],
+ 'targetIQN': info['targetIQN']
+ }
+ try:
+ sr_ref = session.get_xenapi().SR.create(
+ session.get_xenapi_host(),
+ record,
+ '0', label, description, 'iscsi', '', False, {})
+ logging.debug('Introduced %s as %s.', label, sr_ref)
+ return sr_ref
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to create Storage Repository'))
+ else:
+ return sr_ref[0]
+
+ @classmethod
+ def find_sr_from_vbd(cls, session, vbd_ref):
+ """Find the SR reference from the VBD reference"""
+ try:
+ vdi_ref = session.get_xenapi().VBD.get_VDI(vbd_ref)
+ sr_ref = session.get_xenapi().VDI.get_SR(vdi_ref)
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to find SR from VBD %s') % vbd_ref)
+ return sr_ref
+
+ @classmethod
+ def destroy_iscsi_storage(cls, session, sr_ref):
+ """Forget the SR whilst preserving the state of the disk"""
+ logging.debug("Forgetting SR %s ... ", sr_ref)
+ pbds = []
+ try:
+ pbds = session.get_xenapi().SR.get_PBDs(sr_ref)
+ except cls.XenAPI.Failure, exc:
+ logging.warn('Ignoring exception %s when getting PBDs for %s',
+ exc, sr_ref)
+ for pbd in pbds:
+ try:
+ session.get_xenapi().PBD.unplug(pbd)
+ except cls.XenAPI.Failure, exc:
+ logging.warn('Ignoring exception %s when unplugging PBD %s',
+ exc, pbd)
+ try:
+ session.get_xenapi().SR.forget(sr_ref)
+ logging.debug("Forgetting SR %s done.", sr_ref)
+ except cls.XenAPI.Failure, exc:
+ logging.warn('Ignoring exception %s when forgetting SR %s',
+ exc, sr_ref)
+
+ @classmethod
+ def introduce_vdi(cls, session, sr_ref):
+ """Introduce VDI in the host"""
+ try:
+ vdis = session.get_xenapi().SR.get_VDIs(sr_ref)
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to introduce VDI on SR %s') % sr_ref)
+ try:
+ vdi_rec = session.get_xenapi().VDI.get_record(vdis[0])
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to get record'
+ ' of VDI %s on') % vdis[0])
+ else:
+ try:
+ return session.get_xenapi().VDI.introduce(
+ vdi_rec['uuid'],
+ vdi_rec['name_label'],
+ vdi_rec['name_description'],
+ vdi_rec['SR'],
+ vdi_rec['type'],
+ vdi_rec['sharable'],
+ vdi_rec['read_only'],
+ vdi_rec['other_config'],
+ vdi_rec['location'],
+ vdi_rec['xenstore_data'],
+ vdi_rec['sm_config'])
+ except cls.XenAPI.Failure, exc:
+ logging.warn(exc)
+ raise StorageError(_('Unable to introduce VDI for SR %s')
+ % sr_ref)
+
+ @classmethod
+ def parse_volume_info(cls, device_path, mountpoint):
+ """
+ Parse device_path and mountpoint as they can be used by XenAPI.
+ In particular, the mountpoint (e.g. /dev/sdc) must be translated
+ into a numeric literal.
+ FIXME(armando):
+ As for device_path, currently cannot be used as it is,
+ because it does not contain target information. As for interim
+ solution, target details are passed either via Flags or obtained
+ by iscsiadm. Long-term solution is to add a few more fields to the
+ db in the iscsi_target table with the necessary info and modify
+ the iscsi driver to set them.
+ """
+ device_number = VolumeHelper.mountpoint_to_number(mountpoint)
+ volume_id = _get_volume_id(device_path)
+ (iscsi_name, iscsi_portal) = _get_target(volume_id)
+ target_host = _get_target_host(iscsi_portal)
+ target_port = _get_target_port(iscsi_portal)
+ target_iqn = _get_iqn(iscsi_name, volume_id)
+ logging.debug('(vol_id,number,host,port,iqn): (%s,%s,%s,%s)',
+ volume_id,
+ target_host,
+ target_port,
+ target_iqn)
+ if (device_number < 0) or \
+ (volume_id is None) or \
+ (target_host is None) or \
+ (target_iqn is None):
+ raise StorageError(_('Unable to obtain target information %s, %s')
+ % (device_path, mountpoint))
+ volume_info = {}
+ volume_info['deviceNumber'] = device_number
+ volume_info['volumeId'] = volume_id
+ volume_info['targetHost'] = target_host
+ volume_info['targetPort'] = target_port
+ volume_info['targetIQN'] = target_iqn
+ return volume_info
+
+ @classmethod
+ def mountpoint_to_number(cls, mountpoint):
+ """Translate a mountpoint like /dev/sdc into a numeric"""
+ if mountpoint.startswith('/dev/'):
+ mountpoint = mountpoint[5:]
+ if re.match('^[hs]d[a-p]$', mountpoint):
+ return (ord(mountpoint[2:3]) - ord('a'))
+ elif re.match('^vd[a-p]$', mountpoint):
+ return (ord(mountpoint[2:3]) - ord('a'))
+ elif re.match('^[0-9]+$', mountpoint):
+ return string.atoi(mountpoint, 10)
+ else:
+ logging.warn('Mountpoint cannot be translated: %s', mountpoint)
+ return -1
+
+
+def _get_volume_id(path):
+ """Retrieve the volume id from device_path"""
+ # n must contain at least the volume_id
+ # /vol- is for remote volumes
+ # -vol- is for local volumes
+ # see compute/manager->setup_compute_volume
+ volume_id = path[path.find('/vol-') + 1:]
+ if volume_id == path:
+ volume_id = path[path.find('-vol-') + 1:].replace('--', '-')
+ return volume_id
+
+
+def _get_target_host(iscsi_string):
+ """Retrieve target host"""
+ if iscsi_string:
+ return iscsi_string[0:iscsi_string.find(':')]
+ elif iscsi_string is None or FLAGS.target_host:
+ return FLAGS.target_host
+
+
+def _get_target_port(iscsi_string):
+ """Retrieve target port"""
+ if iscsi_string:
+ return iscsi_string[iscsi_string.find(':') + 1:]
+ elif iscsi_string is None or FLAGS.target_port:
+ return FLAGS.target_port
+
+
+def _get_iqn(iscsi_string, id):
+ """Retrieve target IQN"""
+ if iscsi_string:
+ return iscsi_string
+ elif iscsi_string is None or FLAGS.iqn_prefix:
+ volume_id = _get_volume_id(id)
+ return '%s:%s' % (FLAGS.iqn_prefix, volume_id)
+
+
+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)
+ result = (None, None)
+ try:
+ (r, _e) = utils.execute("sudo iscsiadm -m discovery -t "
+ "sendtargets -p %s" %
+ volume_ref['host'])
+ except exception.ProcessExecutionError, exc:
+ logging.warn(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)
+ return result
diff --git a/nova/virt/xenapi/volumeops.py b/nova/virt/xenapi/volumeops.py
index 1943ccab0..fdeb2506c 100644
--- a/nova/virt/xenapi/volumeops.py
+++ b/nova/virt/xenapi/volumeops.py
@@ -17,17 +17,110 @@
"""
Management class for Storage-related functions (attach, detach, etc).
"""
+import logging
+
+from nova import exception
+from nova.virt.xenapi.vm_utils import VMHelper
+from nova.virt.xenapi.volume_utils import VolumeHelper
+from nova.virt.xenapi.volume_utils import StorageError
class VolumeOps(object):
+ """
+ Management class for Volume-related tasks
+ """
def __init__(self, session):
+ self.XenAPI = session.get_imported_xenapi()
self._session = session
+ # Load XenAPI module in the helper classes respectively
+ VolumeHelper.XenAPI = self.XenAPI
+ VMHelper.XenAPI = self.XenAPI
def attach_volume(self, instance_name, device_path, mountpoint):
- # FIXME: that's going to be sorted when iscsi-xenapi lands in branch
- return True
+ """Attach volume storage to VM instance"""
+ # Before we start, check that the VM exists
+ vm_ref = VMHelper.lookup(self._session, instance_name)
+ if vm_ref is None:
+ raise exception.NotFound(_('Instance %s not found')
+ % instance_name)
+ # NOTE: No Resource Pool concept so far
+ logging.debug(_("Attach_volume: %s, %s, %s"),
+ instance_name, device_path, mountpoint)
+ # Create the iSCSI SR, and the PDB through which hosts access SRs.
+ # But first, retrieve target info, like Host, IQN, LUN and SCSIID
+ vol_rec = VolumeHelper.parse_volume_info(device_path, mountpoint)
+ label = 'SR-%s' % vol_rec['volumeId']
+ description = 'Disk-for:%s' % instance_name
+ # Create SR
+ sr_ref = VolumeHelper.create_iscsi_storage(self._session,
+ vol_rec,
+ label,
+ description)
+ # Introduce VDI and attach VBD to VM
+ try:
+ vdi_ref = VolumeHelper.introduce_vdi(self._session, sr_ref)
+ except StorageError, exc:
+ logging.warn(exc)
+ VolumeHelper.destroy_iscsi_storage(self._session, sr_ref)
+ raise Exception(_('Unable to create VDI on SR %s for instance %s')
+ % (sr_ref,
+ instance_name))
+ else:
+ try:
+ vbd_ref = VMHelper.create_vbd(self._session,
+ vm_ref, vdi_ref,
+ vol_rec['deviceNumber'],
+ False)
+ except self.XenAPI.Failure, exc:
+ logging.warn(exc)
+ VolumeHelper.destroy_iscsi_storage(self._session, sr_ref)
+ raise Exception(_('Unable to use SR %s for instance %s')
+ % (sr_ref,
+ instance_name))
+ else:
+ try:
+ task = self._session.call_xenapi('Async.VBD.plug',
+ vbd_ref)
+ self._session.wait_for_task(vol_rec['deviceNumber'], task)
+ except self.XenAPI.Failure, exc:
+ logging.warn(exc)
+ VolumeHelper.destroy_iscsi_storage(self._session,
+ sr_ref)
+ raise Exception(_('Unable to attach volume to instance %s')
+ % instance_name)
+ logging.info(_('Mountpoint %s attached to instance %s'),
+ mountpoint, instance_name)
def detach_volume(self, instance_name, mountpoint):
- # FIXME: that's going to be sorted when iscsi-xenapi lands in branch
- return True
+ """Detach volume storage to VM instance"""
+ # Before we start, check that the VM exists
+ vm_ref = VMHelper.lookup(self._session, instance_name)
+ if vm_ref is None:
+ raise exception.NotFound(_('Instance %s not found')
+ % instance_name)
+ # Detach VBD from VM
+ logging.debug(_("Detach_volume: %s, %s"), instance_name, mountpoint)
+ device_number = VolumeHelper.mountpoint_to_number(mountpoint)
+ try:
+ vbd_ref = VMHelper.find_vbd_by_number(self._session,
+ vm_ref, device_number)
+ except StorageError, exc:
+ logging.warn(exc)
+ raise Exception(_('Unable to locate volume %s') % mountpoint)
+ else:
+ try:
+ sr_ref = VolumeHelper.find_sr_from_vbd(self._session,
+ vbd_ref)
+ VMHelper.unplug_vbd(self._session, vbd_ref)
+ except StorageError, exc:
+ logging.warn(exc)
+ raise Exception(_('Unable to detach volume %s') % mountpoint)
+ try:
+ VMHelper.destroy_vbd(self._session, vbd_ref)
+ except StorageError, exc:
+ logging.warn(exc)
+ # Forget SR
+ VolumeHelper.destroy_iscsi_storage(self._session, sr_ref)
+ logging.info(_('Mountpoint %s detached from instance %s'),
+ mountpoint, instance_name)
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 146e2f153..11c66c974 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -44,7 +44,10 @@ reactor thread if the VM.get_by_name_label or VM.get_record calls block.
:xenapi_task_poll_interval: The interval (seconds) used for polling of
remote tasks (Async.VM.start, etc)
(default: 0.5).
-
+:target_host: the iSCSI Target Host IP address, i.e. the IP
+ address for the nova-volume host
+:target_port: iSCSI Target Port, 3260 Default
+:iqn_prefix: IQN Prefix, e.g. 'iqn.2010-10.org.openstack'
"""
import logging
@@ -62,6 +65,7 @@ from nova.virt.xenapi.vmops import VMOps
from nova.virt.xenapi.volumeops import VolumeOps
FLAGS = flags.FLAGS
+
flags.DEFINE_string('xenapi_connection_url',
None,
'URL for connection to XenServer/Xen Cloud Platform.'
@@ -79,18 +83,20 @@ flags.DEFINE_float('xenapi_task_poll_interval',
'The interval used for polling of remote tasks '
'(Async.VM.start, etc). Used only if '
'connection_type=xenapi.')
-
-XenAPI = None
+flags.DEFINE_string('target_host',
+ None,
+ 'iSCSI Target Host')
+flags.DEFINE_string('target_port',
+ '3260',
+ 'iSCSI Target Port, 3260 Default')
+flags.DEFINE_string('iqn_prefix',
+ 'iqn.2010-10.org.openstack',
+ 'IQN Prefix')
def get_connection(_):
"""Note that XenAPI doesn't have a read-only connection mode, so
the read_only parameter is ignored."""
- # This is loaded late so that there's no need to install this
- # library when not using XenAPI.
- global XenAPI
- if XenAPI is None:
- XenAPI = __import__('XenAPI')
url = FLAGS.xenapi_connection_url
username = FLAGS.xenapi_connection_username
password = FLAGS.xenapi_connection_password
@@ -110,6 +116,13 @@ class XenAPIConnection(object):
self._vmops = VMOps(session)
self._volumeops = VolumeOps(session)
+ def init_host(self):
+ #FIXME(armando): implement this
+ #NOTE(armando): would we need a method
+ #to call when shutting down the host?
+ #e.g. to do session logout?
+ pass
+
def list_instances(self):
"""List VM instances"""
return self._vmops.list_instances()
@@ -161,9 +174,14 @@ class XenAPISession(object):
"""The session to invoke XenAPI SDK calls"""
def __init__(self, url, user, pw):
- self._session = XenAPI.Session(url)
+ self.XenAPI = self.get_imported_xenapi()
+ self._session = self._create_session(url)
self._session.login_with_password(user, pw)
+ def get_imported_xenapi(self):
+ """Stubout point. This can be replaced with a mock xenapi module."""
+ return __import__('XenAPI')
+
def get_xenapi(self):
"""Return the xenapi object"""
return self._session.xenapi
@@ -181,29 +199,33 @@ class XenAPISession(object):
def async_call_plugin(self, plugin, fn, args):
"""Call Async.host.call_plugin on a background thread."""
- return tpool.execute(_unwrap_plugin_exceptions,
+ return tpool.execute(self._unwrap_plugin_exceptions,
self._session.xenapi.Async.host.call_plugin,
self.get_xenapi_host(), plugin, fn, args)
- def wait_for_task(self, instance_id, task):
- """Return a Deferred that will give the result of the given task.
- The task is polled until it completes."""
+ def wait_for_task(self, id, task):
+ """Return the result of the given task. The task is polled
+ until it completes."""
done = event.Event()
- loop = utils.LoopingCall(self._poll_task, instance_id, task, done)
+ loop = utils.LoopingCall(self._poll_task, id, task, done)
loop.start(FLAGS.xenapi_task_poll_interval, now=True)
rv = done.wait()
loop.stop()
return rv
- def _poll_task(self, instance_id, task, done):
+ def _create_session(self, url):
+ """Stubout point. This can be replaced with a mock session."""
+ return self.XenAPI.Session(url)
+
+ def _poll_task(self, id, task, done):
"""Poll the given XenAPI task, and fire the given Deferred if we
get a result."""
try:
name = self._session.xenapi.task.get_name_label(task)
status = self._session.xenapi.task.get_status(task)
action = dict(
- instance_id=int(instance_id),
+ id=int(id),
action=name,
error=None)
if status == "pending":
@@ -223,33 +245,32 @@ class XenAPISession(object):
task,
status,
error_info))
- done.send_exception(XenAPI.Failure(error_info))
+ done.send_exception(self.XenAPI.Failure(error_info))
db.instance_action_create(context.get_admin_context(), action)
- except XenAPI.Failure, exc:
+ except self.XenAPI.Failure, exc:
logging.warn(exc)
done.send_exception(*sys.exc_info())
-
-def _unwrap_plugin_exceptions(func, *args, **kwargs):
- """Parse exception details"""
- try:
- return func(*args, **kwargs)
- except XenAPI.Failure, exc:
- logging.debug(_("Got exception: %s"), exc)
- if (len(exc.details) == 4 and
- exc.details[0] == 'XENAPI_PLUGIN_EXCEPTION' and
- exc.details[2] == 'Failure'):
- params = None
- try:
- params = eval(exc.details[3])
- except:
- raise exc
- raise XenAPI.Failure(params)
- else:
+ def _unwrap_plugin_exceptions(self, func, *args, **kwargs):
+ """Parse exception details"""
+ try:
+ return func(*args, **kwargs)
+ except self.XenAPI.Failure, exc:
+ logging.debug(_("Got exception: %s"), exc)
+ if (len(exc.details) == 4 and
+ exc.details[0] == 'XENAPI_PLUGIN_EXCEPTION' and
+ exc.details[2] == 'Failure'):
+ params = None
+ try:
+ params = eval(exc.details[3])
+ except:
+ raise exc
+ raise self.XenAPI.Failure(params)
+ else:
+ raise
+ except xmlrpclib.ProtocolError, exc:
+ logging.debug(_("Got exception: %s"), exc)
raise
- except xmlrpclib.ProtocolError, exc:
- logging.debug(_("Got exception: %s"), exc)
- raise
def _parse_xmlrpc_value(val):
diff --git a/run_tests.py b/run_tests.py
deleted file mode 100644
index 6a4b7f1ab..000000000
--- a/run_tests.py
+++ /dev/null
@@ -1,125 +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.
-
-"""
-This is our basic test running framework based on Twisted's Trial.
-
-Usage Examples:
-
- # to run all the tests
- python run_tests.py
-
- # to run a specific test suite imported here
- python run_tests.py NodeConnectionTestCase
-
- # to run a specific test imported here
- python run_tests.py NodeConnectionTestCase.test_reboot
-
- # to run some test suites elsewhere
- python run_tests.py nova.tests.node_unittest
- python run_tests.py nova.tests.node_unittest.NodeConnectionTestCase
-
-Due to our use of multiprocessing it we frequently get some ignorable
-'Interrupted system call' exceptions after test completion.
-
-"""
-
-import eventlet
-eventlet.monkey_patch()
-
-import __main__
-import gettext
-import os
-import sys
-
-gettext.install('nova', unicode=1)
-
-from twisted.scripts import trial as trial_script
-
-from nova import flags
-from nova import twistd
-
-from nova.tests.access_unittest import *
-from nova.tests.api_unittest import *
-from nova.tests.auth_unittest import *
-from nova.tests.cloud_unittest import *
-from nova.tests.compute_unittest import *
-from nova.tests.flags_unittest import *
-from nova.tests.misc_unittest import *
-from nova.tests.network_unittest import *
-#from nova.tests.objectstore_unittest import *
-from nova.tests.quota_unittest import *
-from nova.tests.rpc_unittest import *
-from nova.tests.scheduler_unittest import *
-from nova.tests.service_unittest import *
-from nova.tests.twistd_unittest import *
-from nova.tests.virt_unittest import *
-from nova.tests.volume_unittest import *
-
-
-FLAGS = flags.FLAGS
-flags.DEFINE_bool('flush_db', True,
- 'Flush the database before running fake tests')
-flags.DEFINE_string('tests_stderr', 'run_tests.err.log',
- 'Path to where to pipe STDERR during test runs.'
- ' Default = "run_tests.err.log"')
-
-
-if __name__ == '__main__':
- OptionsClass = twistd.WrapTwistedOptions(trial_script.Options)
- config = OptionsClass()
- argv = config.parseOptions()
-
- FLAGS.verbose = True
-
- # TODO(termie): these should make a call instead of doing work on import
- if FLAGS.fake_tests:
- from nova.tests.fake_flags import *
- else:
- from nova.tests.real_flags import *
-
- # Establish redirect for STDERR
- sys.stderr.flush()
- err = open(FLAGS.tests_stderr, 'w+', 0)
- os.dup2(err.fileno(), sys.stderr.fileno())
-
- if len(argv) == 1 and len(config['tests']) == 0:
- # If no tests were specified run the ones imported in this file
- # NOTE(termie): "tests" is not a flag, just some Trial related stuff
- config['tests'].update(['__main__'])
- elif len(config['tests']):
- # If we specified tests check first whether they are in __main__
- for arg in config['tests']:
- key = arg.split('.')[0]
- if hasattr(__main__, key):
- config['tests'].remove(arg)
- config['tests'].add('__main__.%s' % arg)
-
- trial_script._initialDebugSetup(config)
- trialRunner = trial_script._makeRunner(config)
- suite = trial_script._getSuite(config)
- if config['until-failure']:
- test_result = trialRunner.runUntilFailure(suite)
- else:
- test_result = trialRunner.run(suite)
- if config.tracer:
- sys.settrace(None)
- results = config.tracer.results()
- results.write_results(show_missing=1, summary=False,
- coverdir=config.coverdir)
- sys.exit(not test_result.wasSuccessful())
diff --git a/run_tests.sh b/run_tests.sh
index a11dcd7cc..67214996d 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -36,7 +36,8 @@ done
if [ $never_venv -eq 1 ]; then
# Just run the test suites in current environment
- python run_tests.py
+ rm -f nova.sqlite
+ nosetests -v
exit
fi
@@ -47,7 +48,8 @@ if [ $force -eq 1 ]; then
fi
if [ -e ${venv} ]; then
- ${with_venv} python run_tests.py $@
+ ${with_venv} rm -f nova.sqlite
+ ${with_venv} nosetests -v $@
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
@@ -59,9 +61,11 @@ else
# Install the virtualenv and run the test suite in it
python tools/install_venv.py
else
- python run_tests.py
+ rm -f nova.sqlite
+ nosetests -v
exit
fi
fi
- ${with_venv} python run_tests.py $@
+ ${with_venv} rm -f nova.sqlite
+ ${with_venv} nosetests -v $@
fi
diff --git a/setup.py b/setup.py
index d88bc1e6f..1abf4d9fe 100644
--- a/setup.py
+++ b/setup.py
@@ -58,6 +58,7 @@ setup(name='nova',
'build_sphinx' : local_BuildDoc },
packages=find_packages(exclude=['bin', 'smoketests']),
include_package_data=True,
+ test_suite='nose.collector',
scripts=['bin/nova-api',
'bin/nova-compute',
'bin/nova-dhcpbridge',