merge trunk
This commit is contained in:
commit
4b5bd4b4a8
2
.mailmap
2
.mailmap
|
@ -27,3 +27,5 @@
|
|||
<vishvananda@gmail.com> <root@ubuntu>
|
||||
<sleepsonthefloor@gmail.com> <root@tonbuntu>
|
||||
<rlane@wikimedia.org> <laner@controller>
|
||||
<corywright@gmail.com> <cory.wright@rackspace.com>
|
||||
<ant@openstack.org> <amesserl@rackspace.com>
|
||||
|
|
5
Authors
5
Authors
|
@ -1,9 +1,12 @@
|
|||
Andy Smith <code@term.ie>
|
||||
Anne Gentle <anne@openstack.org>
|
||||
Anthony Young <sleepsonthefloor@gmail.com>
|
||||
Antony Messerli <ant@openstack.org>
|
||||
Armando Migliaccio <Armando.Migliaccio@eu.citrix.com>
|
||||
Chris Behrens <cbehrens@codestud.com>
|
||||
Chmouel Boudjnah <chmouel@chmouel.com>
|
||||
Cory Wright <corywright@gmail.com>
|
||||
David Pravec <David.Pravec@danix.org>
|
||||
Dean Troyer <dtroyer@gmail.com>
|
||||
Devin Carlen <devin.carlen@gmail.com>
|
||||
Ed Leafe <ed@leafe.com>
|
||||
|
@ -26,6 +29,7 @@ Paul Voccio <paul@openstack.org>
|
|||
Rick Clark <rick@openstack.org>
|
||||
Ryan Lane <rlane@wikimedia.org>
|
||||
Ryan Lucio <rlucio@internap.com>
|
||||
Salvatore Orlando <salvatore.orlando@eu.citrix.com>
|
||||
Sandy Walsh <sandy.walsh@rackspace.com>
|
||||
Soren Hansen <soren.hansen@rackspace.com>
|
||||
Thierry Carrez <thierry@openstack.org>
|
||||
|
@ -34,3 +38,4 @@ Trey Morris <trey.morris@rackspace.com>
|
|||
Vishvananda Ishaya <vishvananda@gmail.com>
|
||||
Youcef Laribi <Youcef.Laribi@eu.citrix.com>
|
||||
Zhixue Wu <Zhixue.Wu@citrix.com>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -30,6 +30,8 @@ if [ -f /etc/default/nova-iptables ] ; then
|
|||
. /etc/default/nova-iptables
|
||||
fi
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
API_PORT=${API_PORT:-"8773"}
|
||||
|
||||
if [ ! -n "$IP" ]; then
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -176,6 +93,8 @@ class APIRouter(wsgi.Router):
|
|||
logging.debug("Including admin operations in API.")
|
||||
server_members['pause'] = 'POST'
|
||||
server_members['unpause'] = 'POST'
|
||||
server_members['suspend'] = 'POST'
|
||||
server_members['resume'] = 'POST'
|
||||
|
||||
mapper.resource("server", "servers", controller=servers.Controller(),
|
||||
collection={'detail': 'GET'},
|
||||
|
@ -194,22 +113,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]
|
||||
|
|
|
@ -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 AuthMiddleware(wsgi.Middleware):
|
||||
"""Authorize the openstack API request or return an HTTP Forbidden."""
|
||||
|
||||
|
||||
class BasicApiAuthManager(object):
|
||||
""" Implements a somewhat rudimentary version of OpenStack Auth"""
|
||||
|
||||
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
|
||||
|
|
|
@ -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]
|
|
@ -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):
|
||||
|
|
|
@ -22,12 +22,73 @@ 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
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
def _translate_keys(item):
|
||||
"""
|
||||
Maps key names to Rackspace-like attributes for return
|
||||
also pares down attributes to those we want
|
||||
item is a dict
|
||||
|
||||
Note: should be removed when the set of keys expected by the api
|
||||
and the set of keys returned by the image service are equivalent
|
||||
|
||||
"""
|
||||
# TODO(tr3buchet): this map is specific to s3 object store,
|
||||
# replace with a list of keys for _filter_keys later
|
||||
mapped_keys = {'status': 'imageState',
|
||||
'id': 'imageId',
|
||||
'name': 'imageLocation'}
|
||||
|
||||
mapped_item = {}
|
||||
# TODO(tr3buchet):
|
||||
# this chunk of code works with s3 and the local image service/glance
|
||||
# when we switch to glance/local image service it can be replaced with
|
||||
# a call to _filter_keys, and mapped_keys can be changed to a list
|
||||
try:
|
||||
for k, v in mapped_keys.iteritems():
|
||||
# map s3 fields
|
||||
mapped_item[k] = item[v]
|
||||
except KeyError:
|
||||
# return only the fields api expects
|
||||
mapped_item = _filter_keys(item, mapped_keys.keys())
|
||||
|
||||
return mapped_item
|
||||
|
||||
|
||||
def _translate_status(item):
|
||||
"""
|
||||
Translates status of image to match current Rackspace api bindings
|
||||
item is a dict
|
||||
|
||||
Note: should be removed when the set of statuses expected by the api
|
||||
and the set of statuses returned by the image service are equivalent
|
||||
|
||||
"""
|
||||
status_mapping = {
|
||||
'pending': 'queued',
|
||||
'decrypting': 'preparing',
|
||||
'untarring': 'saving',
|
||||
'available': 'active'}
|
||||
item['status'] = status_mapping[item['status']]
|
||||
return item
|
||||
|
||||
|
||||
def _filter_keys(item, keys):
|
||||
"""
|
||||
Filters all model attributes except for keys
|
||||
item is a dict
|
||||
|
||||
"""
|
||||
return dict((k, v) for k, v in item.iteritems() if k in keys)
|
||||
|
||||
|
||||
class Controller(wsgi.Controller):
|
||||
|
||||
_serialization_metadata = {
|
||||
|
@ -40,24 +101,25 @@ class Controller(wsgi.Controller):
|
|||
self._service = utils.import_object(FLAGS.image_service)
|
||||
|
||||
def index(self, req):
|
||||
"""Return all public images in brief."""
|
||||
return dict(images=[dict(id=img['id'], name=img['name'])
|
||||
for img in self.detail(req)['images']])
|
||||
"""Return all public images in brief"""
|
||||
items = self._service.index(req.environ['nova.context'])
|
||||
items = common.limited(items, req)
|
||||
items = [_filter_keys(item, ('id', 'name')) for item in items]
|
||||
return dict(images=items)
|
||||
|
||||
def detail(self, req):
|
||||
"""Return all public images in detail."""
|
||||
"""Return all public images in detail"""
|
||||
try:
|
||||
images = self._service.detail(req.environ['nova.context'])
|
||||
images = nova.api.openstack.limited(images, req)
|
||||
items = self._service.detail(req.environ['nova.context'])
|
||||
except NotImplementedError:
|
||||
# Emulate detail() using repeated calls to show()
|
||||
images = self._service.index(ctxt)
|
||||
images = nova.api.openstack.limited(images, req)
|
||||
images = [self._service.show(ctxt, i['id']) for i in images]
|
||||
return dict(images=images)
|
||||
items = self._service.index(req.environ['nova.context'])
|
||||
items = common.limited(items, req)
|
||||
items = [_translate_keys(item) for item in items]
|
||||
items = [_translate_status(item) for item in items]
|
||||
return dict(images=items)
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given image id."""
|
||||
"""Return data about the given image id"""
|
||||
return dict(image=self._service.show(req.environ['nova.context'], id))
|
||||
|
||||
def delete(self, req, id):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
@ -45,7 +46,8 @@ def _entity_detail(inst):
|
|||
power_state.NOSTATE: 'build',
|
||||
power_state.RUNNING: 'active',
|
||||
power_state.BLOCKED: 'active',
|
||||
power_state.PAUSED: 'suspended',
|
||||
power_state.SUSPENDED: 'suspended',
|
||||
power_state.PAUSED: 'error',
|
||||
power_state.SHUTDOWN: 'active',
|
||||
power_state.SHUTOFF: 'active',
|
||||
power_state.CRASHED: 'error'}
|
||||
|
@ -98,7 +100,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)
|
||||
|
||||
|
@ -181,7 +183,7 @@ class Controller(wsgi.Controller):
|
|||
self.compute_api.pause(ctxt, id)
|
||||
except:
|
||||
readable = traceback.format_exc()
|
||||
logging.error("Compute.api::pause %s", readable)
|
||||
logging.error(_("Compute.api::pause %s"), readable)
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
|
@ -192,7 +194,29 @@ class Controller(wsgi.Controller):
|
|||
self.compute_api.unpause(ctxt, id)
|
||||
except:
|
||||
readable = traceback.format_exc()
|
||||
logging.error("Compute.api::unpause %s", readable)
|
||||
logging.error(_("Compute.api::unpause %s"), readable)
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
def suspend(self, req, id):
|
||||
"""permit admins to suspend the server"""
|
||||
context = req.environ['nova.context']
|
||||
try:
|
||||
self.compute_api.suspend(context, id)
|
||||
except:
|
||||
readable = traceback.format_exc()
|
||||
logging.error(_("compute.api::suspend %s"), readable)
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
def resume(self, req, id):
|
||||
"""permit admins to resume the server from suspend"""
|
||||
context = req.environ['nova.context']
|
||||
try:
|
||||
self.compute_api.resume(context, id)
|
||||
except:
|
||||
readable = traceback.format_exc()
|
||||
logging.error(_("compute.api::resume %s"), readable)
|
||||
return faults.Fault(exc.HTTPUnprocessableEntity())
|
||||
return exc.HTTPAccepted()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
#
|
||||
#
|
||||
|
||||
|
@ -30,55 +32,19 @@ attributetype (
|
|||
SINGLE-VALUE
|
||||
)
|
||||
|
||||
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 )
|
||||
)
|
||||
|
|
|
@ -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 ) )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
# This gets zipped and run on the cloudpipe-managed OpenVPN server
|
||||
|
||||
export LC_ALL=C
|
||||
export VPN_IP=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f2 | awk '{print $$1}'`
|
||||
export BROADCAST=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f3 | awk '{print $$1}'`
|
||||
export DHCP_MASK=`ifconfig | grep 'inet addr:'| grep -v '127.0.0.1' | cut -d: -f4 | awk '{print $$1}'`
|
||||
|
|
|
@ -91,15 +91,16 @@ class ComputeAPI(base.Base):
|
|||
is_vpn = image_id == FLAGS.vpn_image_id
|
||||
if not is_vpn:
|
||||
image = self.image_service.show(context, image_id)
|
||||
|
||||
# If kernel_id/ramdisk_id isn't explicitly set in API call
|
||||
# we take the defaults from the image's metadata
|
||||
if kernel_id is None:
|
||||
kernel_id = image.get('kernelId', None)
|
||||
if ramdisk_id is None:
|
||||
ramdisk_id = image.get('ramdiskId', None)
|
||||
|
||||
# Make sure we have access to kernel and ramdisk
|
||||
#No kernel and ramdisk for raw images
|
||||
if kernel_id == str(FLAGS.null_kernel):
|
||||
kernel_id = None
|
||||
ramdisk_id = None
|
||||
logging.debug("Creating a raw instance")
|
||||
# Make sure we have access to kernel and ramdisk (if not raw)
|
||||
if kernel_id:
|
||||
self.image_service.show(context, kernel_id)
|
||||
if ramdisk_id:
|
||||
|
@ -283,6 +284,24 @@ class ComputeAPI(base.Base):
|
|||
{"method": "unpause_instance",
|
||||
"args": {"instance_id": instance['id']}})
|
||||
|
||||
def suspend(self, context, instance_id):
|
||||
"""suspend the instance with instance_id"""
|
||||
instance = self.db.instance_get_by_internal_id(context, instance_id)
|
||||
host = instance['host']
|
||||
rpc.cast(context,
|
||||
self.db.queue_get_for(context, FLAGS.compute_topic, host),
|
||||
{"method": "suspend_instance",
|
||||
"args": {"instance_id": instance['id']}})
|
||||
|
||||
def resume(self, context, instance_id):
|
||||
"""resume the instance with instance_id"""
|
||||
instance = self.db.instance_get_by_internal_id(context, instance_id)
|
||||
host = instance['host']
|
||||
rpc.cast(context,
|
||||
self.db.queue_get_for(context, FLAGS.compute_topic, host),
|
||||
{"method": "resume_instance",
|
||||
"args": {"instance_id": instance['id']}})
|
||||
|
||||
def rescue(self, context, instance_id):
|
||||
"""Rescue the given instance."""
|
||||
instance = self.db.instance_get_by_internal_id(context, instance_id)
|
||||
|
|
|
@ -296,6 +296,39 @@ class ComputeManager(manager.Manager):
|
|||
instance_id,
|
||||
result))
|
||||
|
||||
@exception.wrap_exception
|
||||
def suspend_instance(self, context, instance_id):
|
||||
"""suspend the instance with instance_id"""
|
||||
context = context.elevated()
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
|
||||
logging.debug(_('instance %s: suspending'),
|
||||
instance_ref['internal_id'])
|
||||
self.db.instance_set_state(context, instance_id,
|
||||
power_state.NOSTATE,
|
||||
'suspending')
|
||||
self.driver.suspend(instance_ref,
|
||||
lambda result: self._update_state_callback(self,
|
||||
context,
|
||||
instance_id,
|
||||
result))
|
||||
|
||||
@exception.wrap_exception
|
||||
def resume_instance(self, context, instance_id):
|
||||
"""resume the suspended instance with instance_id"""
|
||||
context = context.elevated()
|
||||
instance_ref = self.db.instance_get(context, instance_id)
|
||||
|
||||
logging.debug(_('instance %s: resuming'), instance_ref['internal_id'])
|
||||
self.db.instance_set_state(context, instance_id,
|
||||
power_state.NOSTATE,
|
||||
'resuming')
|
||||
self.driver.resume(instance_ref,
|
||||
lambda result: self._update_state_callback(self,
|
||||
context,
|
||||
instance_id,
|
||||
result))
|
||||
|
||||
@exception.wrap_exception
|
||||
def get_console_output(self, context, instance_id):
|
||||
"""Send the console output for an instance."""
|
||||
|
|
|
@ -26,6 +26,7 @@ PAUSED = 0x03
|
|||
SHUTDOWN = 0x04
|
||||
SHUTOFF = 0x05
|
||||
CRASHED = 0x06
|
||||
SUSPENDED = 0x07
|
||||
|
||||
|
||||
def name(code):
|
||||
|
@ -36,5 +37,6 @@ def name(code):
|
|||
PAUSED: 'paused',
|
||||
SHUTDOWN: 'shutdown',
|
||||
SHUTOFF: 'shutdown',
|
||||
CRASHED: 'crashed'}
|
||||
CRASHED: 'crashed',
|
||||
SUSPENDED: 'suspended'}
|
||||
return d[code]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1175,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).\
|
||||
|
@ -1190,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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -223,6 +223,20 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase):
|
|||
res = req.get_response(nova.api.API('os'))
|
||||
res_dict = json.loads(res.body)
|
||||
|
||||
def _is_equivalent_subset(x, y):
|
||||
if set(x) <= set(y):
|
||||
for k, v in x.iteritems():
|
||||
if x[k] != y[k]:
|
||||
if x[k] == 'active' and y[k] == 'available':
|
||||
continue
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
for image in res_dict['images']:
|
||||
self.assertEquals(1, self.IMAGE_FIXTURES.count(image),
|
||||
"image %s not in fixtures!" % str(image))
|
||||
for image_fixture in self.IMAGE_FIXTURES:
|
||||
if _is_equivalent_subset(image, image_fixture):
|
||||
break
|
||||
else:
|
||||
self.assertEquals(1, 2, "image %s not in fixtures!" %
|
||||
str(image))
|
||||
|
|
|
@ -88,9 +88,13 @@ class ServersTest(unittest.TestCase):
|
|||
self.stubs.Set(nova.db.api, 'instance_get_floating_address',
|
||||
instance_address)
|
||||
self.stubs.Set(nova.compute.api.ComputeAPI, 'pause',
|
||||
fake_compute_api)
|
||||
fake_compute_api)
|
||||
self.stubs.Set(nova.compute.api.ComputeAPI, 'unpause',
|
||||
fake_compute_api)
|
||||
fake_compute_api)
|
||||
self.stubs.Set(nova.compute.api.ComputeAPI, 'suspend',
|
||||
fake_compute_api)
|
||||
self.stubs.Set(nova.compute.api.ComputeAPI, 'resume',
|
||||
fake_compute_api)
|
||||
self.allow_admin = FLAGS.allow_admin_api
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -246,6 +250,30 @@ class ServersTest(unittest.TestCase):
|
|||
res = req.get_response(nova.api.API('os'))
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_server_suspend(self):
|
||||
FLAGS.allow_admin_api = True
|
||||
body = dict(server=dict(
|
||||
name='server_test', imageId=2, flavorId=2, metadata={},
|
||||
personality={}))
|
||||
req = webob.Request.blank('/v1.0/servers/1/suspend')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
res = req.get_response(nova.api.API('os'))
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_server_resume(self):
|
||||
FLAGS.allow_admin_api = True
|
||||
body = dict(server=dict(
|
||||
name='server_test', imageId=2, flavorId=2, metadata={},
|
||||
personality={}))
|
||||
req = webob.Request.blank('/v1.0/servers/1/resume')
|
||||
req.method = 'POST'
|
||||
req.content_type = 'application/json'
|
||||
req.body = json.dumps(body)
|
||||
res = req.get_response(nova.api.API('os'))
|
||||
self.assertEqual(res.status_int, 202)
|
||||
|
||||
def test_server_reboot(self):
|
||||
body = dict(server=dict(
|
||||
name='server_test', imageId=2, flavorId=2, metadata={},
|
||||
|
|
|
@ -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()
|
|
@ -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')
|
|
@ -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
|
||||
=============================
|
||||
"""
|
|
@ -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)
|
|
@ -101,13 +101,13 @@ class ComputeTestCase(test.TestCase):
|
|||
self.compute.run_instance(self.context, instance_id)
|
||||
|
||||
instances = db.instance_get_all(context.get_admin_context())
|
||||
logging.info("Running instances: %s", instances)
|
||||
logging.info(_("Running instances: %s"), instances)
|
||||
self.assertEqual(len(instances), 1)
|
||||
|
||||
self.compute.terminate_instance(self.context, instance_id)
|
||||
|
||||
instances = db.instance_get_all(context.get_admin_context())
|
||||
logging.info("After terminating instances: %s", instances)
|
||||
logging.info(_("After terminating instances: %s"), instances)
|
||||
self.assertEqual(len(instances), 0)
|
||||
|
||||
def test_run_terminate_timestamps(self):
|
||||
|
@ -136,6 +136,14 @@ class ComputeTestCase(test.TestCase):
|
|||
self.compute.unpause_instance(self.context, instance_id)
|
||||
self.compute.terminate_instance(self.context, instance_id)
|
||||
|
||||
def test_suspend(self):
|
||||
"""ensure instance can be suspended"""
|
||||
instance_id = self._create_instance()
|
||||
self.compute.run_instance(self.context, instance_id)
|
||||
self.compute.suspend_instance(self.context, instance_id)
|
||||
self.compute.resume_instance(self.context, instance_id)
|
||||
self.compute.terminate_instance(self.context, instance_id)
|
||||
|
||||
def test_reboot(self):
|
||||
"""Ensure instance can be rebooted"""
|
||||
instance_id = self._create_instance()
|
|
@ -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:
|
|
@ -48,7 +48,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()
|
|
@ -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()
|
|
@ -33,6 +33,7 @@ flags.DECLARE('instances_path', 'nova.compute.manager')
|
|||
class LibvirtConnTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(LibvirtConnTestCase, self).setUp()
|
||||
libvirt_conn._late_load_cheetah()
|
||||
self.flags(fake_call=True)
|
||||
self.manager = manager.AuthManager()
|
||||
self.user = self.manager.create_user('fake', 'fake', 'fake',
|
||||
|
@ -53,39 +54,37 @@ 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)
|
||||
|
@ -159,7 +158,6 @@ class LibvirtConnTestCase(test.TestCase):
|
|||
(lambda t: t.find('./devices/serial/source').get(
|
||||
'path').split('/')[1], 'console.log'),
|
||||
(lambda t: t.find('./memory').text, '2097152')]
|
||||
|
||||
if rescue:
|
||||
common_checks += [
|
||||
(lambda t: t.findall('./devices/disk/source')[0].get(
|
|
@ -0,0 +1,220 @@
|
|||
# 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)
|
||||
stubs.stub_out_get_target(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()
|
|
@ -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
|
||||
=================================
|
||||
"""
|
|
@ -0,0 +1,103 @@
|
|||
# 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
|
||||
from nova.virt.xenapi import volume_utils
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def stub_out_get_target(stubs):
|
||||
"""Stubs out _get_target in volume_utils"""
|
||||
def fake_get_target(volume_id):
|
||||
return (None, None)
|
||||
|
||||
stubs.Set(volume_utils, '_get_target', fake_get_target)
|
||||
|
||||
|
||||
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
|
|
@ -48,7 +48,8 @@ def import_class(import_str):
|
|||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ImportError, ValueError, AttributeError):
|
||||
except (ImportError, ValueError, AttributeError), exc:
|
||||
logging.debug(_('Inner Exception: %s'), exc)
|
||||
raise exception.NotFound(_('Class %s cannot be found') % class_str)
|
||||
|
||||
|
||||
|
|
|
@ -148,6 +148,18 @@ class FakeConnection(object):
|
|||
"""
|
||||
pass
|
||||
|
||||
def suspend(self, instance, callback):
|
||||
"""
|
||||
suspend the specified instance
|
||||
"""
|
||||
pass
|
||||
|
||||
def resume(self, instance, callback):
|
||||
"""
|
||||
resume the specified instance
|
||||
"""
|
||||
pass
|
||||
|
||||
def destroy(self, instance):
|
||||
"""
|
||||
Destroy (shutdown and delete) the specified instance.
|
||||
|
|
|
@ -63,10 +63,9 @@ from nova.compute import instance_types
|
|||
from nova.compute import power_state
|
||||
from nova.virt import images
|
||||
|
||||
from Cheetah.Template import Template
|
||||
|
||||
libvirt = None
|
||||
libxml2 = None
|
||||
Template = None
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
@ -96,15 +95,26 @@ flags.DEFINE_string('ajaxterm_portrange',
|
|||
def get_connection(read_only):
|
||||
# These are loaded late so that there's no need to install these
|
||||
# libraries when not using libvirt.
|
||||
# Cheetah is separate because the unit tests want to load Cheetah,
|
||||
# but not libvirt.
|
||||
global libvirt
|
||||
global libxml2
|
||||
if libvirt is None:
|
||||
libvirt = __import__('libvirt')
|
||||
if libxml2 is None:
|
||||
libxml2 = __import__('libxml2')
|
||||
_late_load_cheetah()
|
||||
return LibvirtConnection(read_only)
|
||||
|
||||
|
||||
def _late_load_cheetah():
|
||||
global Template
|
||||
if Template is None:
|
||||
t = __import__('Cheetah.Template', globals(), locals(), ['Template'],
|
||||
-1)
|
||||
Template = t.Template
|
||||
|
||||
|
||||
def _get_net_and_mask(cidr):
|
||||
net = IPy.IP(cidr)
|
||||
return str(net.net()), str(net.netmask())
|
||||
|
@ -287,6 +297,14 @@ class LibvirtConnection(object):
|
|||
def unpause(self, instance, callback):
|
||||
raise exception.APIError("unpause not supported for libvirt.")
|
||||
|
||||
@exception.wrap_exception
|
||||
def suspend(self, instance, callback):
|
||||
raise exception.APIError("suspend not supported for libvirt")
|
||||
|
||||
@exception.wrap_exception
|
||||
def resume(self, instance, callback):
|
||||
raise exception.APIError("resume not supported for libvirt")
|
||||
|
||||
@exception.wrap_exception
|
||||
def rescue(self, instance):
|
||||
self.destroy(instance, False)
|
||||
|
@ -559,9 +577,10 @@ class LibvirtConnection(object):
|
|||
|
||||
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)
|
||||
extra_params = ("<parameter name=\"PROJNET\" "
|
||||
"value=\"%s\" />\n"
|
||||
"<parameter name=\"PROJMASK\" "
|
||||
"value=\"%s\" />\n") % (net, mask)
|
||||
else:
|
||||
extra_params = "\n"
|
||||
|
||||
|
@ -847,8 +866,8 @@ class NWFilterFirewall(object):
|
|||
the base filter are all in place.
|
||||
"""
|
||||
|
||||
nwfilter_xml = ("<filter name='nova-instance-%s' chain='root'>\n"
|
||||
) % instance['name']
|
||||
nwfilter_xml = ("<filter name='nova-instance-%s' "
|
||||
"chain='root'>\n") % instance['name']
|
||||
|
||||
if instance['image_id'] == FLAGS.vpn_image_id:
|
||||
nwfilter_xml += " <filterref filter='nova-vpn' />\n"
|
||||
|
@ -861,8 +880,8 @@ class NWFilterFirewall(object):
|
|||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,389 @@
|
|||
# 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)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -37,35 +39,34 @@ XENAPI_POWER_STATE = {
|
|||
'Halted': power_state.SHUTDOWN,
|
||||
'Running': power_state.RUNNING,
|
||||
'Paused': power_state.PAUSED,
|
||||
'Suspended': power_state.SHUTDOWN, # FIXME
|
||||
'Suspended': power_state.SUSPENDED,
|
||||
'Crashed': power_state.CRASHED}
|
||||
|
||||
XenAPI = None
|
||||
|
||||
class ImageType:
|
||||
"""
|
||||
Enumeration class for distinguishing different image types
|
||||
0 - kernel/ramdisk image (goes on dom0's filesystem)
|
||||
1 - disk image (local SR, partitioned by objectstore plugin)
|
||||
2 - raw disk image (local SR, NOT partitioned by plugin)
|
||||
"""
|
||||
|
||||
KERNEL_RAMDISK = 0
|
||||
DISK = 1
|
||||
DISK_RAW = 2
|
||||
|
||||
|
||||
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):
|
||||
def create_vm(cls, session, instance, kernel, ramdisk, pv_kernel=False):
|
||||
"""Create a VM record. Returns a Deferred that gives the new
|
||||
VM reference."""
|
||||
VM reference.
|
||||
the pv_kernel flag indicates whether the guest is HVM or PV
|
||||
"""
|
||||
|
||||
instance_type = instance_types.INSTANCE_TYPES[instance.instance_type]
|
||||
mem = str(long(instance_type['memory_mb']) * 1024 * 1024)
|
||||
|
@ -85,9 +86,9 @@ class VMHelper():
|
|||
'actions_after_reboot': 'restart',
|
||||
'actions_after_crash': 'destroy',
|
||||
'PV_bootloader': '',
|
||||
'PV_kernel': kernel,
|
||||
'PV_ramdisk': ramdisk,
|
||||
'PV_args': 'root=/dev/xvda1',
|
||||
'PV_kernel': '',
|
||||
'PV_ramdisk': '',
|
||||
'PV_args': '',
|
||||
'PV_bootloader_args': '',
|
||||
'PV_legacy_args': '',
|
||||
'HVM_boot_policy': '',
|
||||
|
@ -99,16 +100,33 @@ class VMHelper():
|
|||
'user_version': '0',
|
||||
'other_config': {},
|
||||
}
|
||||
#Complete VM configuration record according to the image type
|
||||
#non-raw/raw with PV kernel/raw in HVM mode
|
||||
if instance.kernel_id:
|
||||
rec['PV_bootloader'] = ''
|
||||
rec['PV_kernel'] = kernel
|
||||
rec['PV_ramdisk'] = ramdisk
|
||||
rec['PV_args'] = 'root=/dev/xvda1'
|
||||
rec['PV_bootloader_args'] = ''
|
||||
rec['PV_legacy_args'] = ''
|
||||
else:
|
||||
if pv_kernel:
|
||||
rec['PV_args'] = 'noninteractive'
|
||||
rec['PV_bootloader'] = 'pygrub'
|
||||
else:
|
||||
rec['HVM_boot_policy'] = 'BIOS order'
|
||||
rec['HVM_boot_params'] = {'order': 'dc'}
|
||||
rec['platform'] = {'acpi': 'true', 'apic': 'true',
|
||||
'pae': 'true', 'viridian': 'true'}
|
||||
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 +140,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,59 +196,69 @@ 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
|
||||
|
||||
@classmethod
|
||||
def fetch_image(cls, session, image, user, project, use_sr):
|
||||
"""use_sr: True to put the image as a VDI in an SR, False to place
|
||||
it on dom0's filesystem. The former is for VM disks, the latter for
|
||||
its kernel and ramdisk (if external kernels are being used).
|
||||
Returns a Deferred that gives the new VDI UUID."""
|
||||
|
||||
def fetch_image(cls, session, image, user, project, type):
|
||||
"""
|
||||
type is interpreted as an ImageType instance
|
||||
"""
|
||||
url = images.image_url(image)
|
||||
access = AuthManager().get_access_key(user, project)
|
||||
logging.debug("Asking xapi to fetch %s as %s", url, access)
|
||||
fn = use_sr and 'get_vdi' or 'get_kernel'
|
||||
fn = (type != ImageType.KERNEL_RAMDISK) and 'get_vdi' or 'get_kernel'
|
||||
args = {}
|
||||
args['src_url'] = url
|
||||
args['username'] = access
|
||||
args['password'] = user.secret
|
||||
if use_sr:
|
||||
args['add_partition'] = 'false'
|
||||
args['raw'] = 'false'
|
||||
if type != ImageType.KERNEL_RAMDISK:
|
||||
args['add_partition'] = 'true'
|
||||
if type == ImageType.DISK_RAW:
|
||||
args['raw'] = '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)
|
||||
def lookup_image(cls, session, vdi_ref):
|
||||
logging.debug("Looking up vdi %s for PV kernel", vdi_ref)
|
||||
fn = "is_vdi_pv"
|
||||
args = {}
|
||||
args['vdi-ref'] = vdi_ref
|
||||
#TODO: Call proper function in plugin
|
||||
task = session.async_call_plugin('objectstore', fn, args)
|
||||
pv_str = session.wait_for_task(task)
|
||||
if pv_str.lower() == 'true':
|
||||
pv = True
|
||||
elif pv_str.lower() == 'false':
|
||||
pv = False
|
||||
logging.debug("PV Kernel in VDI:%d", pv)
|
||||
return pv
|
||||
|
||||
@classmethod
|
||||
def lookup_blocking(cls, session, i):
|
||||
""" Synchronous lookup """
|
||||
def lookup(cls, session, i):
|
||||
"""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 +269,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 +282,11 @@ class VMHelper():
|
|||
|
||||
@classmethod
|
||||
def compile_info(cls, record):
|
||||
"""Fill record with VM status information"""
|
||||
logging.info(_("(VM_UTILS) xenserver vm state -> |%s|"),
|
||||
record['power_state'])
|
||||
logging.info(_("(VM_UTILS) xenapi power_state -> |%s|"),
|
||||
XENAPI_POWER_STATE[record['power_state']])
|
||||
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 +310,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}
|
||||
|
||||
|
||||
|
|
|
@ -22,12 +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
|
||||
from nova.virt.xenapi.vm_utils import ImageType
|
||||
|
||||
|
||||
class VMOps(object):
|
||||
|
@ -36,12 +38,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,8 +55,8 @@ 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.network_get_by_instance(context.get_admin_context(),
|
||||
instance['id'])['bridge']
|
||||
|
@ -66,30 +65,68 @@ class VMOps(object):
|
|||
|
||||
user = AuthManager().get_user(instance.user_id)
|
||||
project = AuthManager().get_project(instance.project_id)
|
||||
vdi_uuid = VMHelper.fetch_image(
|
||||
self._session, instance.image_id, user, project, True)
|
||||
kernel = VMHelper.fetch_image(
|
||||
self._session, instance.kernel_id, user, project, False)
|
||||
ramdisk = VMHelper.fetch_image(
|
||||
self._session, instance.ramdisk_id, user, project, False)
|
||||
#if kernel is not present we must download a raw disk
|
||||
if instance.kernel_id:
|
||||
disk_image_type = ImageType.DISK
|
||||
else:
|
||||
disk_image_type = ImageType.DISK_RAW
|
||||
vdi_uuid = VMHelper.fetch_image(self._session,
|
||||
instance.image_id, user, project, disk_image_type)
|
||||
vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
|
||||
vm_ref = VMHelper.create_vm(
|
||||
self._session, instance, kernel, ramdisk)
|
||||
#Have a look at the VDI and see if it has a PV kernel
|
||||
pv_kernel = False
|
||||
if not instance.kernel_id:
|
||||
pv_kernel = VMHelper.lookup_image(self._session, vdi_ref)
|
||||
kernel = None
|
||||
if instance.kernel_id:
|
||||
kernel = VMHelper.fetch_image(self._session,
|
||||
instance.kernel_id, user, project, ImageType.KERNEL_RAMDISK)
|
||||
ramdisk = None
|
||||
if instance.ramdisk_id:
|
||||
ramdisk = VMHelper.fetch_image(self._session,
|
||||
instance.ramdisk_id, user, project, ImageType.KERNEL_RAMDISK)
|
||||
vm_ref = VMHelper.create_vm(self._session,
|
||||
instance, kernel, ramdisk, pv_kernel)
|
||||
VMHelper.create_vbd(self._session, vm_ref, vdi_ref, 0, True)
|
||||
|
||||
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 +153,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 +173,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 +183,37 @@ 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 suspend(self, instance, callback):
|
||||
"""suspend the specified instance"""
|
||||
instance_name = instance.name
|
||||
vm = VMHelper.lookup(self._session, instance_name)
|
||||
if vm is None:
|
||||
raise Exception(_("suspend: instance not present %s") %
|
||||
instance_name)
|
||||
task = self._session.call_xenapi('Async.VM.suspend', vm)
|
||||
self._wait_with_callback(task, callback)
|
||||
|
||||
def resume(self, instance, callback):
|
||||
"""resume the specified instance"""
|
||||
instance_name = instance.name
|
||||
vm = VMHelper.lookup(self._session, instance_name)
|
||||
if vm is None:
|
||||
raise Exception(_("resume: instance not present %s") %
|
||||
instance_name)
|
||||
task = self._session.call_xenapi('Async.VM.resume', vm, False, True)
|
||||
self._wait_with_callback(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 +221,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)
|
||||
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
# 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
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
@ -134,6 +147,14 @@ class XenAPIConnection(object):
|
|||
"""Unpause paused VM instance"""
|
||||
self._vmops.unpause(instance, callback)
|
||||
|
||||
def suspend(self, instance, callback):
|
||||
"""suspend the specified instance"""
|
||||
self._vmops.suspend(instance, callback)
|
||||
|
||||
def resume(self, instance, callback):
|
||||
"""resume the specified instance"""
|
||||
self._vmops.resume(instance, callback)
|
||||
|
||||
def get_info(self, instance_id):
|
||||
"""Return data about VM instance"""
|
||||
return self._vmops.get_info(instance_id)
|
||||
|
@ -161,9 +182,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 +207,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 +253,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):
|
||||
|
|
|
@ -270,7 +270,7 @@ class Serializer(object):
|
|||
needed to serialize a dictionary to that type.
|
||||
"""
|
||||
self.metadata = metadata or {}
|
||||
req = webob.Request(environ)
|
||||
req = webob.Request.blank('', environ)
|
||||
suffix = req.path_info.split('.')[-1].lower()
|
||||
if suffix == 'json':
|
||||
self.handler = self._to_json
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
Multi Tenancy Networking Protections in XenServer
|
||||
=================================================
|
||||
|
||||
The purpose of the vif_rules script is to allow multi-tenancy on a XenServer
|
||||
host. In a multi-tenant cloud environment a host machine needs to be able to
|
||||
enforce network isolation amongst guest instances, at both layer two and layer
|
||||
three. The rules prevent guests from taking and using unauthorized IP addresses,
|
||||
sniffing other guests traffic, and prevents ARP poisoning attacks. This current
|
||||
revision only supports IPv4, but will support IPv6 in the future.
|
||||
|
||||
Kernel Requirements
|
||||
===================
|
||||
|
||||
- physdev module
|
||||
- arptables support
|
||||
- ebtables support
|
||||
- iptables support
|
||||
|
||||
If the kernel doesn't support these, you will need to obtain the Source RPMS for
|
||||
the proper version of XenServer to recompile the dom0 kernel.
|
||||
|
||||
XenServer Requirements (32-bit dom0)
|
||||
====================================
|
||||
|
||||
- arptables 32-bit rpm
|
||||
- ebtables 32-bit rpm
|
||||
- python-simplejson
|
||||
|
||||
XenServer Environment Specific Notes
|
||||
====================================
|
||||
|
||||
- XenServer 5.5 U1 based on the 2.6.18 kernel didn't include physdev module
|
||||
support. Support for this had to be recompiled into the kernel.
|
||||
- XenServer 5.6 based on the 2.6.27 kernel didn't include physdev, ebtables, or
|
||||
arptables.
|
||||
- XenServer 5.6 FP1 didn't include physdev, ebtables, or arptables but they do
|
||||
have a Cloud Supplemental pack available to partners which swaps out the
|
||||
kernels for kernels that support the networking rules.
|
||||
|
||||
How it works - tl;dr
|
||||
====================
|
||||
|
||||
iptables, ebtables, and arptables drop rules are applied to all forward chains
|
||||
on the host. These are applied at boot time with an init script. They ensure
|
||||
all forwarded packets are dropped by default. Allow rules are then applied to
|
||||
the instances to ensure they have permission to talk on the internet.
|
||||
|
||||
How it works - Long
|
||||
===================
|
||||
|
||||
Any time an underprivileged domain or domU is started or stopped, it gets a
|
||||
unique domain id (dom_id). This dom_id is utilized in a number of places, one
|
||||
of which is it's assigned to the virtual interface (vif). The vifs are attached
|
||||
to the bridge that is attached to the physical network. For instance, if you
|
||||
had a public bridge attached to eth0 and your domain id was 5, your vif would be
|
||||
vif5.0.
|
||||
|
||||
The networking rules are applied to the VIF directly so they apply at the lowest
|
||||
level of the networking stack. Because the VIF changes along with the domain id
|
||||
on any start, stop, or reboot of the instance, the rules need to be removed and
|
||||
re-added any time that occurs.
|
||||
|
||||
Because the dom_id can change often, the vif_rules script is hooked into the
|
||||
/etc/xensource/scripts/vif script that gets called anytime an instance is
|
||||
started, or stopped, which includes pauses and resumes.
|
||||
|
||||
Examples of the rules ran for the host on boot:
|
||||
|
||||
iptables -P FORWARD DROP
|
||||
iptables -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT
|
||||
ebtables -P FORWARD DROP
|
||||
ebtables -A FORWARD -o eth0 -j ACCEPT
|
||||
arptables -P FORWARD DROP
|
||||
arptables -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT
|
||||
arptables -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
|
||||
|
||||
Examples of the rules that are ran per instance state change:
|
||||
|
||||
iptables -A FORWARD -m physdev --physdev-in vif1.0 -s 10.1.135.22/32 -j ACCEPT
|
||||
arptables -A FORWARD --opcode Request --in-interface "vif1.0" \
|
||||
--source-ip 10.1.135.22 -j ACCEPT
|
||||
arptables -A FORWARD --opcode Reply --in-interface "vif1.0" \
|
||||
--source-ip 10.1.135.22 --source-mac 9e:6e:cc:19:7f:fe -j ACCEPT
|
||||
ebtables -A FORWARD -p 0806 -o vif1.0 --arp-ip-dst 10.1.135.22 -j ACCEPT
|
||||
ebtables -A FORWARD -p 0800 -o vif1.0 --ip-dst 10.1.135.22 -j ACCEPT
|
||||
ebtables -I FORWARD 1 -s ! 9e:6e:cc:19:7f:fe -i vif1.0 -j DROP
|
||||
|
||||
Typically when you see a vif, it'll look like vif<domain id>.<network bridge>.
|
||||
vif2.1 for example would be domain 2 on the second interface.
|
||||
|
||||
The vif_rules.py script needs to pull information about the IPs and MAC
|
||||
addresses assigned to the instance. The current implementation assumes that
|
||||
information is put into the VM Record into the xenstore-data key in a JSON
|
||||
string. The vif_rules.py script reads out of the JSON string to determine the
|
||||
IPs, and MAC addresses to protect.
|
||||
|
||||
An example format is given below:
|
||||
|
||||
# xe vm-param-get uuid=<uuid> param-name=xenstore-data
|
||||
xenstore-data (MRW):
|
||||
vm-data/networking/4040fa7292e4:
|
||||
{"label": "public",
|
||||
"ips": [{"netmask":"255.255.255.0",
|
||||
"enabled":"1",
|
||||
"ip":"173.200.100.10"}],
|
||||
"mac":"40:40:fa:72:92:e4",
|
||||
"gateway":"173.200.100.1",
|
||||
"vm_id":"123456",
|
||||
"dns":["72.3.128.240","72.3.128.241"]};
|
||||
|
||||
vm-data/networking/40402321c9b8:
|
||||
{"label":"private",
|
||||
"ips":[{"netmask":"255.255.224.0",
|
||||
"enabled":"1",
|
||||
"ip":"10.177.10.10"}],
|
||||
"routes":[{"route":"10.176.0.0",
|
||||
"netmask":"255.248.0.0",
|
||||
"gateway":"10.177.10.1"},
|
||||
{"route":"10.191.192.0",
|
||||
"netmask":"255.255.192.0",
|
||||
"gateway":"10.177.10.1"}],
|
||||
"mac":"40:40:23:21:c9:b8"}
|
||||
|
||||
The key is used for two purposes. One, the vif_rules.py script will read from
|
||||
it to apply the rules needed after parsing the JSON. The second is that because
|
||||
it's put into the xenstore-data field, the xenstore will be populated with this
|
||||
data on boot. This allows a guest agent the ability to read out data about the
|
||||
instance and apply configurations as needed.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
- Copy host-rules into /etc/init.d/ and make sure to chmod +x host-rules.
|
||||
- Run 'chkconfig host-rules on' to add the init script to start up.
|
||||
- Copy vif_rules.py into /etc/xensource/scripts
|
||||
- Patch /etc/xensource/scripts/vif using the supplied patch file. It may vary
|
||||
for different versions of XenServer but it should be pretty self explanatory.
|
||||
It calls the vif_rules.py script on domain creation and tear down.
|
||||
- Run '/etc/init.d/host-rules start' to start up the host based rules.
|
||||
- The instance rules will then fire on creation of the VM as long as the correct
|
||||
JSON is in place.
|
||||
- You can check to see if the rules are in place with: iptables --list,
|
||||
arptables --list, or ebtables --list
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# host-rules Start/Stop the networking host rules
|
||||
#
|
||||
# chkconfig: 2345 85 15
|
||||
# description: Networking Host Rules for Multi Tenancy Protections
|
||||
|
||||
# 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.
|
||||
|
||||
IPTABLES=/sbin/iptables
|
||||
EBTABLES=/sbin/ebtables
|
||||
ARPTABLES=/sbin/arptables
|
||||
|
||||
iptables-up()
|
||||
{
|
||||
$IPTABLES -P FORWARD DROP
|
||||
$IPTABLES -A FORWARD -m physdev --physdev-in eth0 -j ACCEPT
|
||||
$IPTABLES -A FORWARD -m physdev --physdev-in eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
ebtables-up()
|
||||
{
|
||||
$EBTABLES -P FORWARD DROP
|
||||
$EBTABLES -A FORWARD -o eth0 -j ACCEPT
|
||||
$EBTABLES -A FORWARD -o eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
arptables-up()
|
||||
{
|
||||
$ARPTABLES -P FORWARD DROP
|
||||
$ARPTABLES -A FORWARD --opcode Request --in-interface eth0 -j ACCEPT
|
||||
$ARPTABLES -A FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
|
||||
$ARPTABLES -A FORWARD --opcode Request --in-interface eth1 -j ACCEPT
|
||||
$ARPTABLES -A FORWARD --opcode Reply --in-interface eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
iptables-down()
|
||||
{
|
||||
$IPTABLES -P FORWARD ACCEPT
|
||||
$IPTABLES -D FORWARD -m physdev --physdev-in eth0 -j ACCEPT
|
||||
$IPTABLES -D FORWARD -m physdev --physdev-in eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
ebtables-down()
|
||||
{
|
||||
$EBTABLES -P FORWARD ACCEPT
|
||||
$EBTABLES -D FORWARD -o eth0 -j ACCEPT
|
||||
$EBTABLES -D FORWARD -o eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
arptables-down()
|
||||
{
|
||||
$ARPTABLES -P FORWARD ACCEPT
|
||||
$ARPTABLES -D FORWARD --opcode Request --in-interface eth0 -j ACCEPT
|
||||
$ARPTABLES -D FORWARD --opcode Reply --in-interface eth0 -j ACCEPT
|
||||
$ARPTABLES -D FORWARD --opcode Request --in-interface eth1 -j ACCEPT
|
||||
$ARPTABLES -D FORWARD --opcode Reply --in-interface eth1 -j ACCEPT
|
||||
}
|
||||
|
||||
start()
|
||||
{
|
||||
iptables-up
|
||||
ebtables-up
|
||||
arptables-up
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
iptables-down
|
||||
ebtables-down
|
||||
arptables-down
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
RETVAL=$?
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
RETVAL=$?
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
RETVAL=$?
|
||||
;;
|
||||
*)
|
||||
echo $"Usage: $0 {start|stop|restart}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit $RETVAL
|
|
@ -0,0 +1,22 @@
|
|||
--- vif 2010-12-20 16:39:46.000000000 +0000
|
||||
+++ vif_modified 2010-11-19 23:24:37.000000000 +0000
|
||||
@@ -213,6 +213,7 @@
|
||||
|
||||
# xs-xen.pq.hq:91e986b8e49f netback-wait-for-hotplug
|
||||
xenstore-write "/local/domain/0/backend/vif/${DOMID}/${DEVID}/hotplug-status" "connected"
|
||||
+ python /etc/xensource/scripts/vif_rules.py ${DOMID} online 2>&1 > /dev/null
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -224,9 +225,11 @@
|
||||
|
||||
remove)
|
||||
if [ "${TYPE}" = "vif" ] ;then
|
||||
+ python /etc/xensource/scripts/vif_rules.py ${DOMID} offline 2>&1 > /dev/null
|
||||
xenstore-rm "${HOTPLUG}/hotplug"
|
||||
fi
|
||||
logger -t scripts-vif "${dev} has been removed"
|
||||
remove_from_bridge
|
||||
;;
|
||||
esac
|
||||
+
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This script is used to configure iptables, ebtables, and arptables rules on
|
||||
XenServer hosts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# This is written to Python 2.4, since that is what is available on XenServer
|
||||
import simplejson as json
|
||||
|
||||
|
||||
def main(dom_id, command, only_this_vif=None):
|
||||
xsls = execute("/usr/bin/xenstore-ls /local/domain/%s/vm-data/networking" \
|
||||
% dom_id, True)
|
||||
macs = [line.split("=")[0].strip() for line in xsls.splitlines()]
|
||||
|
||||
for mac in macs:
|
||||
xsr = "/usr/bin/xenstore-read /local/domain/%s/vm-data/networking/%s"
|
||||
xsread = execute(xsr % (dom_id, mac), True)
|
||||
data = json.loads(xsread)
|
||||
for ip in data['ips']:
|
||||
if data["label"] == "public":
|
||||
vif = "vif%s.0" % dom_id
|
||||
else:
|
||||
vif = "vif%s.1" % dom_id
|
||||
|
||||
if (only_this_vif is None) or (vif == only_this_vif):
|
||||
params = dict(IP=ip['ip'], VIF=vif, MAC=data['mac'])
|
||||
apply_ebtables_rules(command, params)
|
||||
apply_arptables_rules(command, params)
|
||||
apply_iptables_rules(command, params)
|
||||
|
||||
|
||||
def execute(command, return_stdout=False):
|
||||
devnull = open(os.devnull, 'w')
|
||||
proc = subprocess.Popen(command, shell=True, close_fds=True,
|
||||
stdout=subprocess.PIPE, stderr=devnull)
|
||||
devnull.close()
|
||||
if return_stdout:
|
||||
return proc.stdout.read()
|
||||
else:
|
||||
return None
|
||||
|
||||
# A note about adding rules:
|
||||
# Whenever we add any rule to iptables, arptables or ebtables we first
|
||||
# delete the same rule to ensure the rule only exists once.
|
||||
|
||||
|
||||
def apply_iptables_rules(command, params):
|
||||
iptables = lambda rule: execute("/sbin/iptables %s" % rule)
|
||||
|
||||
iptables("-D FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \
|
||||
-j ACCEPT" % params)
|
||||
if command == 'online':
|
||||
iptables("-A FORWARD -m physdev --physdev-in %(VIF)s -s %(IP)s \
|
||||
-j ACCEPT" % params)
|
||||
|
||||
|
||||
def apply_arptables_rules(command, params):
|
||||
arptables = lambda rule: execute("/sbin/arptables %s" % rule)
|
||||
|
||||
arptables("-D FORWARD --opcode Request --in-interface %(VIF)s \
|
||||
--source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
|
||||
arptables("-D FORWARD --opcode Reply --in-interface %(VIF)s \
|
||||
--source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
|
||||
if command == 'online':
|
||||
arptables("-A FORWARD --opcode Request --in-interface %(VIF)s \
|
||||
--source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
|
||||
arptables("-A FORWARD --opcode Reply --in-interface %(VIF)s \
|
||||
--source-ip %(IP)s --source-mac %(MAC)s -j ACCEPT" % params)
|
||||
|
||||
|
||||
def apply_ebtables_rules(command, params):
|
||||
ebtables = lambda rule: execute("/sbin/ebtables %s" % rule)
|
||||
|
||||
ebtables("-D FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s -j ACCEPT" %
|
||||
params)
|
||||
ebtables("-D FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s -j ACCEPT" %
|
||||
params)
|
||||
if command == 'online':
|
||||
ebtables("-A FORWARD -p 0806 -o %(VIF)s --arp-ip-dst %(IP)s \
|
||||
-j ACCEPT" % params)
|
||||
ebtables("-A FORWARD -p 0800 -o %(VIF)s --ip-dst %(IP)s \
|
||||
-j ACCEPT" % params)
|
||||
|
||||
ebtables("-D FORWARD -s ! %(MAC)s -i %(VIF)s -j DROP" % params)
|
||||
if command == 'online':
|
||||
ebtables("-I FORWARD 1 -s ! %(MAC)s -i %(VIF)s -j DROP" % params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print "usage: %s dom_id online|offline [vif]" % \
|
||||
os.path.basename(sys.argv[0])
|
||||
sys.exit(1)
|
||||
else:
|
||||
dom_id, command = sys.argv[1:3]
|
||||
vif = len(sys.argv) == 4 and sys.argv[3] or None
|
||||
main(dom_id, command, vif)
|
|
@ -43,24 +43,43 @@ SECTOR_SIZE = 512
|
|||
MBR_SIZE_SECTORS = 63
|
||||
MBR_SIZE_BYTES = MBR_SIZE_SECTORS * SECTOR_SIZE
|
||||
|
||||
def is_vdi_pv(session,args):
|
||||
logging.debug("Checking wheter VDI has PV kernel")
|
||||
vdi = exists(args, 'vdi-ref')
|
||||
pv=with_vdi_in_dom0(session, vdi, False,
|
||||
lambda dev: _is_vdi_pv('/dev/%s' % dev))
|
||||
if pv:
|
||||
return 'true'
|
||||
else:
|
||||
return 'false'
|
||||
|
||||
def _is_vdi_pv(dest):
|
||||
logging.debug("Running pygrub against %s",dest)
|
||||
output=os.popen('pygrub -qn %s' % dest)
|
||||
pv=False
|
||||
for line in output.readlines():
|
||||
#try to find kernel string
|
||||
m=re.search('(?<=kernel:)/.*(?:>)',line)
|
||||
if m:
|
||||
if m.group(0).find('xen')!=-1:
|
||||
pv=True
|
||||
logging.debug("PV:%d",pv)
|
||||
return pv
|
||||
|
||||
def get_vdi(session, args):
|
||||
src_url = exists(args, 'src_url')
|
||||
username = exists(args, 'username')
|
||||
password = exists(args, 'password')
|
||||
raw_image=validate_bool(args, 'raw', 'false')
|
||||
add_partition = validate_bool(args, 'add_partition', 'false')
|
||||
|
||||
(proto, netloc, url_path, _, _, _) = urlparse.urlparse(src_url)
|
||||
|
||||
sr = find_sr(session)
|
||||
if sr is None:
|
||||
raise Exception('Cannot find SR to write VDI to')
|
||||
|
||||
virtual_size = \
|
||||
get_content_length(proto, netloc, url_path, username, password)
|
||||
if virtual_size < 0:
|
||||
raise Exception('Cannot get VDI size')
|
||||
|
||||
vdi_size = virtual_size
|
||||
if add_partition:
|
||||
# Make room for MBR.
|
||||
|
@ -69,18 +88,19 @@ def get_vdi(session, args):
|
|||
vdi = create_vdi(session, sr, src_url, vdi_size, False)
|
||||
with_vdi_in_dom0(session, vdi, False,
|
||||
lambda dev: get_vdi_(proto, netloc, url_path,
|
||||
username, password, add_partition,
|
||||
username, password, add_partition,raw_image,
|
||||
virtual_size, '/dev/%s' % dev))
|
||||
return session.xenapi.VDI.get_uuid(vdi)
|
||||
|
||||
|
||||
def get_vdi_(proto, netloc, url_path, username, password, add_partition,
|
||||
def get_vdi_(proto, netloc, url_path, username, password, add_partition,raw_image,
|
||||
virtual_size, dest):
|
||||
|
||||
if add_partition:
|
||||
#Salvatore: vdi should not be partitioned for raw images
|
||||
if (add_partition and not raw_image):
|
||||
write_partition(virtual_size, dest)
|
||||
|
||||
offset = add_partition and MBR_SIZE_BYTES or 0
|
||||
offset = (add_partition and not raw_image and MBR_SIZE_BYTES) or 0
|
||||
get(proto, netloc, url_path, username, password, dest, offset)
|
||||
|
||||
|
||||
|
@ -228,4 +248,5 @@ def download_all(response, length, dest_file, offset):
|
|||
|
||||
if __name__ == '__main__':
|
||||
XenAPIPlugin.dispatch({'get_vdi': get_vdi,
|
||||
'get_kernel': get_kernel})
|
||||
'get_kernel': get_kernel,
|
||||
'is_vdi_pv': is_vdi_pv})
|
126
run_tests.py
126
run_tests.py
|
@ -1,126 +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.middleware_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())
|
16
run_tests.sh
16
run_tests.sh
|
@ -21,6 +21,7 @@ function process_option {
|
|||
-V|--virtual-env) let always_venv=1; let never_venv=0;;
|
||||
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
|
||||
-f|--force) let force=1;;
|
||||
*) noseargs="$noseargs $1"
|
||||
esac
|
||||
}
|
||||
|
||||
|
@ -29,14 +30,18 @@ with_venv=tools/with_venv.sh
|
|||
always_venv=0
|
||||
never_venv=0
|
||||
force=0
|
||||
noseargs=
|
||||
|
||||
for arg in "$@"; do
|
||||
process_option $arg
|
||||
done
|
||||
|
||||
NOSETESTS="nosetests -v $noseargs"
|
||||
|
||||
if [ $never_venv -eq 1 ]; then
|
||||
# Just run the test suites in current environment
|
||||
python run_tests.py
|
||||
rm -f nova.sqlite
|
||||
$NOSETESTS
|
||||
exit
|
||||
fi
|
||||
|
||||
|
@ -47,7 +52,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
|
||||
else
|
||||
if [ $always_venv -eq 1 ]; then
|
||||
# Automatically install the virtualenv
|
||||
|
@ -59,9 +65,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
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
${with_venv} python run_tests.py $@
|
||||
${with_venv} rm -f nova.sqlite
|
||||
${with_venv} $NOSETESTS
|
||||
fi
|
||||
|
|
1
setup.py
1
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',
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
sudo ifconfig -a | grep br | grep -v bridge | cut -f1 -d" " | xargs -n1 -ifoo ifconfig foo down
|
||||
sudo ifconfig -a | grep br | grep -v bridge | cut -f1 -d" " | xargs -n1 -ifoo brctl delbr foo
|
||||
sudo ifconfig -a | grep vlan | grep -v vlan124 | grep -v vlan5 | cut -f1 -d" " | xargs -n1 -ifoo ifconfig foo down
|
||||
|
|
|
@ -36,7 +36,7 @@ else
|
|||
# NOTE(vish): This will just get the first ip in the list, so if you
|
||||
# have more than one eth device set up, this will fail, and
|
||||
# you should explicitly pass in the ip of the instance
|
||||
IP=`ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'`
|
||||
IP=`LC_ALL=C ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'`
|
||||
fi
|
||||
|
||||
if [ -n "$3" ]; then
|
||||
|
|
Loading…
Reference in New Issue